sentry-issue
Fetches and digests Sentry issue data (summary, tags, stack trace, breadcrumbs, latest event) by short ID, numeric issue ID, or sentry.io URL, for any Sentry org the local token can access. Use when the user mentions a Sentry issue or short ID (e.g. MY-PROJECT-4X2), pastes a sent
Install
npx skills add https://github.com/domengabrovsek/agent-config/tree/main/skills/sentry-issue
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install domengabrovsek-claude@llmmart
git clone https://github.com/domengabrovsek/agent-config.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole domengabrovsek/claude collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Sentry Issue Investigation
Pull everything needed to debug a Sentry issue, starting from just a short ID.
Quick start
# from this skill's own directory
scripts/sentry-issue.sh MY-PROJECT-4X2
Accepts a short ID (PROJECT-ABC), a numeric issue ID, or a full issue URL
(https://<org>.sentry.io/issues/123456/). Prints a compact digest:
issue summary, tags, exception chain with in-app stack frames, request info,
last 10 breadcrumbs. Full JSON payloads are saved to
/tmp/sentry-issue-<id>.json and /tmp/sentry-event-<id>.json for follow-up
jq queries.
How auth and org resolution work
- Token:
SENTRY_AUTH_TOKENenv ->./.sentryclirc-> repo-root.sentryclirc->~/.sentryclirc. The token is never printed. - Org:
--org <slug>flag ->SENTRY_ORGenv ->.sentryclircorgkey -> auto-discovery viaGET /api/0/organizations/. With a single accessible org it is used directly; with multiple orgs, short IDs are probed against each, while numeric IDs require--org. - Server:
SENTRY_URLenv or.sentryclircurlkey for self-hosted; defaults tohttps://sentry.io(which proxies org-scoped routes to the correct region, including EU orgs).
Required token scopes: org:read, project:read, event:read.
Investigation workflow
Run the script with the identifier the user gave.
Read the digest: in-app frames (
[app]prefix) point at the failing code; tags likefunctionName,transaction,url,environmentlocate the runtime context.Open the implicated source files in the matching repo and trace the failure path. The Sentry project slug usually maps to a repo - check the slug against the repos you have locally.
For deeper context, query the saved JSON, e.g.:
jq '.entries[] | select(.type=="exception") | .data.values[0].stacktrace.frames[] | select(.inApp) | {filename, lineNo, context}' /tmp/sentry-event-<id>.json jq '{stats: .stats, firstRelease: .firstRelease.version, lastRelease: .lastRelease.version}' /tmp/sentry-issue-<id>.json jq '.contexts' /tmp/sentry-event-<id>.jsonFor a structured root-cause investigation, continue with the
/debugworkflow using the gathered evidence.
Going deeper
See REFERENCE.md for the full endpoint map and payload
anatomy: other events (oldest, by ID, walking previousEventID/nextEventID),
tag value distributions, attachments, issue search, source-context jq recipes,
and what is NOT available (frame-local variables, session replay). Use it
whenever the latest-event digest is not enough evidence.
- For ad-hoc API calls beyond the script, resolve the token the same way the
script does (inside command substitution) so it never appears in output
(review-time: secret handling depends on how the command is composed - not pattern-matchable by a hook)
Troubleshooting
- HTTP 403: wrong org slug or token lacks scopes - run
sentry-cli infoto inspect scopes (never print the token itself). - HTTP 404 on short ID: short ID belongs to a different org - pass
--org, or the issue was deleted. sentry-cli organizations listJSON parse error: known CLI 3.5.0 bug; the script's API-based discovery avoids it.
Files (claude)
-
scripts
-
sentry-issue.sh 4.8 KB
#!/usr/bin/env bash set -euo pipefail # Fetch a Sentry issue + latest event by short ID, numeric ID, or issue URL. # Org-agnostic: resolves token/org/url from env, project .sentryclirc, ~/.sentryclirc, # or auto-discovers the org via the API. The auth token is never printed. usage() { echo "usage: sentry-issue.sh <SHORT_ID|ISSUE_ID|SENTRY_URL> [--org <slug>]" >&2 exit 1 } [ $# -ge 1 ] || usage INPUT="$1"; shift ORG="${SENTRY_ORG:-}" while [ $# -gt 0 ]; do case "$1" in --org) ORG="$2"; shift 2 ;; *) usage ;; esac done rc_value() { local key="$1" file for file in ./.sentryclirc "$(git rev-parse --show-toplevel 2>/dev/null)/.sentryclirc" "$HOME/.sentryclirc"; do [ -f "$file" ] || continue local val val=$(grep -m1 -E "^${key}[[:space:]]*=" "$file" | cut -d= -f2- | tr -d ' ') || true if [ -n "$val" ]; then echo "$val"; return 0; fi done return 0 } TOKEN="${SENTRY_AUTH_TOKEN:-$(rc_value token)}" [ -n "$TOKEN" ] || { echo "error: no token found (SENTRY_AUTH_TOKEN, ./.sentryclirc, ~/.sentryclirc)" >&2; exit 1; } BASE="${SENTRY_URL:-$(rc_value url)}" BASE="${BASE:-https://sentry.io}" BASE="${BASE%/}" [ -n "$ORG" ] || ORG=$(rc_value org) api() { local path="$1" out="$2" curl -s -o "$out" -w '%{http_code}' -H "Authorization: Bearer $TOKEN" "$BASE/api/0$path" } api_ok() { local path="$1" out="$2" code code=$(api "$path" "$out") if [ "$code" != "200" ]; then echo "error: GET $path returned HTTP $code" >&2 jq -r '.detail // empty' "$out" >&2 2>/dev/null || true exit 1 fi } discover_orgs() { api_ok "/organizations/" /tmp/sentry-orgs.json jq -r '.[].slug' /tmp/sentry-orgs.json } ISSUE_ID="" if [[ "$INPUT" =~ ^https?:// ]]; then ISSUE_ID=$(echo "$INPUT" | grep -oE 'issues/[0-9]+' | cut -d/ -f2) || true [ -n "$ISSUE_ID" ] || { echo "error: could not extract issue ID from URL" >&2; exit 1; } if [ -z "$ORG" ]; then ORG=$(echo "$INPUT" | sed -nE 's#https?://([^./]+)\.sentry\.io.*#\1#p') fi elif [[ "$INPUT" =~ ^[0-9]+$ ]]; then ISSUE_ID="$INPUT" fi if [ -z "$ORG" ]; then ORGS=$(discover_orgs) COUNT=$(echo "$ORGS" | grep -c . || true) if [ "$COUNT" -eq 1 ]; then ORG="$ORGS" elif [ -n "$ISSUE_ID" ]; then echo "error: multiple orgs ($(echo "$ORGS" | tr '\n' ' ')) - pass --org" >&2 exit 1 else SHORT_ID=$(echo "$INPUT" | tr '[:lower:]' '[:upper:]') for candidate in $ORGS; do code=$(api "/organizations/$candidate/shortids/$SHORT_ID/" /tmp/sentry-shortid.json) if [ "$code" = "200" ]; then ORG="$candidate"; ISSUE_ID=$(jq -r '.groupId' /tmp/sentry-shortid.json); break; fi done [ -n "$ORG" ] || { echo "error: short ID $SHORT_ID not found in any org ($(echo "$ORGS" | tr '\n' ' '))" >&2; exit 1; } fi fi if [ -z "$ISSUE_ID" ]; then SHORT_ID=$(echo "$INPUT" | tr '[:lower:]' '[:upper:]') api_ok "/organizations/$ORG/shortids/$SHORT_ID/" /tmp/sentry-shortid.json ISSUE_ID=$(jq -r '.groupId' /tmp/sentry-shortid.json) fi ISSUE_JSON="/tmp/sentry-issue-$ISSUE_ID.json" EVENT_JSON="/tmp/sentry-event-$ISSUE_ID.json" api_ok "/organizations/$ORG/issues/$ISSUE_ID/" "$ISSUE_JSON" api_ok "/organizations/$ORG/issues/$ISSUE_ID/events/latest/" "$EVENT_JSON" echo "=== ISSUE ===" jq -r ' "shortId: \(.shortId)", "title: \(.title)", "culprit: \(.culprit)", "project: \(.project.slug)", "level: \(.level) status: \(.status) handled: \(.isUnhandled | if . then "no (unhandled)" else "yes" end)", "events: \(.count) users: \(.userCount)", "firstSeen: \(.firstSeen)", "lastSeen: \(.lastSeen)", "permalink: \(.permalink)" ' "$ISSUE_JSON" echo "" echo "=== LATEST EVENT ===" jq -r ' "eventID: \(.eventID)", "date: \(.dateCreated)", "message: \(.message // .title // "-")" ' "$EVENT_JSON" echo "" echo "--- tags ---" jq -r '.tags[] | "\(.key)=\(.value)"' "$EVENT_JSON" echo "" echo "--- exception ---" jq -r ' [.entries[] | select(.type=="exception") | .data.values[]] | reverse | .[] | "\(.type): \(.value)", ( (.stacktrace.frames // []) as $all | ([$all[] | select(.inApp == true)] | if length > 0 then . else $all[-10:] end) | reverse | .[] | " \(if .inApp then "[app] " else "" end)\(.filename):\(.lineNo) in \(.function)" ), "" ' "$EVENT_JSON" echo "--- request ---" jq -r ' [.entries[] | select(.type=="request") | .data] | .[0] // empty | "\(.method // "-") \(.url // "-")" ' "$EVENT_JSON" echo "" echo "--- breadcrumbs (last 10) ---" jq -r ' [.entries[] | select(.type=="breadcrumbs") | .data.values[]] | .[-10:][] | "\(.timestamp) [\(.category // .type)] \(.level // "-"): \(.message // ((.data // {}) | tostring))" ' "$EVENT_JSON" echo "" echo "--- extra context keys ---" jq -r '(.context // {}) | keys | join(", ")' "$EVENT_JSON" echo "" echo "Full JSON: $ISSUE_JSON and $EVENT_JSON (query with jq for more detail)"
-
-
REFERENCE.md 4.2 KB
# Sentry API Reference for Issue Investigation All endpoints are org-scoped under `https://sentry.io/api/0` (or `SENTRY_URL` for self-hosted). `sentry.io` proxies org-scoped routes to the correct region silo (verified with EU orgs), so no region URL handling is needed. The legacy non-org route `/api/0/issues/<id>/` 404s for EU-region orgs - never use it. Auth: `Authorization: Bearer <token>` header. Always resolve the token the way `scripts/sentry-issue.sh` does (env -> project rc -> home rc) inside a shell command substitution so it never appears in conversation output. Required scopes: `org:read`, `project:read`, `event:read`. ## Endpoint map | Need | Endpoint | | --- | --- | | Discover accessible orgs | `GET /organizations/` (slug + `links.regionUrl`) | | Short ID -> issue | `GET /organizations/<org>/shortids/<SHORT-ID>/` (`.groupId`) | | Issue summary | `GET /organizations/<org>/issues/<id>/` | | Latest / oldest event | `GET /organizations/<org>/issues/<id>/events/latest\|oldest/` | | List all events | `GET /organizations/<org>/issues/<id>/events/` (paginated via `Link` header) | | Specific event | `GET /organizations/<org>/issues/<id>/events/<eventID>/` | | Tag value distribution | `GET /organizations/<org>/issues/<id>/tags/<key>/` | | All tags overview | `GET /organizations/<org>/issues/<id>/tags/` | | Attachments | `GET /organizations/<org>/issues/<id>/attachments/` | | Search issues | `GET /organizations/<org>/issues/?query=<sentry-search>&project=<projectID>` | ## Issue payload (what to look for) - `count`, `userCount`, `firstSeen`, `lastSeen` - blast radius and timeline - `stats."24h"` / `stats."30d"` - spike vs steady drip - `firstRelease.version`, `lastRelease.version` - regression window - `isUnhandled` - crash vs caught-and-reported - `activity[]` - comments, status changes, regressions (who did what) - `permalink` - link for the user ## Event payload (what to look for) - `entries[]` by `type`: - `exception`: `.data.values[]` - exception chain (outermost last; reverse for display). Frames have `filename`, `lineNo`, `function`, `inApp`, and `context` - **the surrounding source lines**, so the failing code is readable without checking out the deployed commit. - `breadcrumbs`: `.data.values[]` - trail of console/http/query events before the failure. Often absent on cron/server events. - `request`: `.data` - method, URL, headers, data. - `message` / `threads`: present on non-exception events. - `contexts`: `cloud_resource` (Cloud Run service/region), `runtime`, `os`, `app`, `trace` (trace ID), `culture` - `tags[]`: `environment`, `functionName`, `transaction`, `url`, `release`... - `previousEventID` / `nextEventID` - walk the issue's events chronologically - `user`, `release`, `sdk`, `fingerprints`, `_meta` (PII-scrubbing info) ## Not available (do not hunt for these) - Local variable values per frame (`vars` is null unless the SDK enables `includeLocalVariables`) - SDK config, not a token limitation - Session Replay and profiling data (separate products, not in event JSON) - Anything requiring write scopes (resolve, assign, comment) - token is read-only by design ## jq recipes ```bash E=/tmp/sentry-event-<id>.json I=/tmp/sentry-issue-<id>.json # Failing source code with context lines jq -r '.entries[] | select(.type=="exception") | .data.values[-1].stacktrace.frames[] | select(.inApp) | .filename, (.context[] | "\(.[0])\t\(.[1])")' $E # Exception chain one-liner jq -r '[.entries[] | select(.type=="exception") | .data.values[] | "\(.type): \(.value)"] | reverse | .[]' $E # Spike or drip - hourly counts, last 24h jq -r '.stats."24h"[] | "\(.[0] | todate) \(.[1])"' $I # Regression window jq '{first: .firstRelease.version, last: .lastRelease.version}' $I # Where is it running jq '.contexts.cloud_resource' $E ``` ## Pitfalls - `sentry-cli organizations list` crashes on a JSON parse bug (CLI 3.5.0); use `GET /organizations/` instead. `sentry-cli` is otherwise only useful for `info` (verify auth/scopes) - it has no issue-detail commands. - Event payloads can be hundreds of KB - always save to `/tmp` and jq them; never cat the whole file into the conversation. - `message` is often empty on exception events - use `title`/`metadata`. -
SKILL.md 3.6 KB
--- name: sentry-issue description: "Fetches and digests Sentry issue data (summary, tags, stack trace, breadcrumbs, latest event) by short ID, numeric issue ID, or sentry.io URL, for any Sentry org the local token can access. Use when the user mentions a Sentry issue or short ID (e.g. MY-PROJECT-4X2), pastes a sentry.io issue URL, or asks to investigate a Sentry error." --- # Sentry Issue Investigation Pull everything needed to debug a Sentry issue, starting from just a short ID. ## Quick start ```bash # from this skill's own directory scripts/sentry-issue.sh MY-PROJECT-4X2 ``` Accepts a short ID (`PROJECT-ABC`), a numeric issue ID, or a full issue URL (`https://<org>.sentry.io/issues/123456/`). Prints a compact digest: issue summary, tags, exception chain with in-app stack frames, request info, last 10 breadcrumbs. Full JSON payloads are saved to `/tmp/sentry-issue-<id>.json` and `/tmp/sentry-event-<id>.json` for follow-up `jq` queries. ## How auth and org resolution work - **Token**: `SENTRY_AUTH_TOKEN` env -> `./.sentryclirc` -> repo-root `.sentryclirc` -> `~/.sentryclirc`. The token is never printed. - **Org**: `--org <slug>` flag -> `SENTRY_ORG` env -> `.sentryclirc` `org` key -> auto-discovery via `GET /api/0/organizations/`. With a single accessible org it is used directly; with multiple orgs, short IDs are probed against each, while numeric IDs require `--org`. - **Server**: `SENTRY_URL` env or `.sentryclirc` `url` key for self-hosted; defaults to `https://sentry.io` (which proxies org-scoped routes to the correct region, including EU orgs). Required token scopes: `org:read`, `project:read`, `event:read`. ## Investigation workflow 1. Run the script with the identifier the user gave. 2. Read the digest: in-app frames (`[app]` prefix) point at the failing code; tags like `functionName`, `transaction`, `url`, `environment` locate the runtime context. 3. Open the implicated source files in the matching repo and trace the failure path. The Sentry project slug usually maps to a repo - check the slug against the repos you have locally. 4. For deeper context, query the saved JSON, e.g.: ```bash jq '.entries[] | select(.type=="exception") | .data.values[0].stacktrace.frames[] | select(.inApp) | {filename, lineNo, context}' /tmp/sentry-event-<id>.json jq '{stats: .stats, firstRelease: .firstRelease.version, lastRelease: .lastRelease.version}' /tmp/sentry-issue-<id>.json jq '.contexts' /tmp/sentry-event-<id>.json ``` 5. For a structured root-cause investigation, continue with the `/debug` workflow using the gathered evidence. ## Going deeper See [REFERENCE.md](REFERENCE.md) for the full endpoint map and payload anatomy: other events (oldest, by ID, walking `previousEventID`/`nextEventID`), tag value distributions, attachments, issue search, source-context jq recipes, and what is NOT available (frame-local variables, session replay). Use it whenever the latest-event digest is not enough evidence. - For ad-hoc API calls beyond the script, resolve the token the same way the script does (inside command substitution) so it never appears in output `(review-time: secret handling depends on how the command is composed - not pattern-matchable by a hook)` ## Troubleshooting - **HTTP 403**: wrong org slug or token lacks scopes - run `sentry-cli info` to inspect scopes (never print the token itself). - **HTTP 404 on short ID**: short ID belongs to a different org - pass `--org`, or the issue was deleted. - **`sentry-cli organizations list` JSON parse error**: known CLI 3.5.0 bug; the script's API-based discovery avoids it.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.