Claude Skill

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

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download domengabrovsek-claude-skills_sentry-issue-48da5d0.zip · 6 KB
Part of domengabrovsek/claude — 41 skills

Install

skills CLI npx skills add https://github.com/domengabrovsek/agent-config/tree/main/skills/sentry-issue
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install domengabrovsek-claude@llmmart
Git 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_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.:

    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 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.
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.

No comments yet.

Reviews (0)

No reviews yet.

Related