Back to skills
SkillHub ClubAnalyze Data & AIFull StackBackendData / AI

sentry

Use when the user asks to inspect Sentry issues or events, summarize recent production errors, or pull basic Sentry health data via the Sentry API; perform read-only queries with the bundled script and require `SENTRY_AUTH_TOKEN`.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
945
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
A92.0

Install command

npx @skill-hub/cli install moizibnyousaf-ai-agent-skills-sentry

Repository

MoizIbnYousaf/Ai-Agent-Skills

Skill path: skills/sentry

Use when the user asks to inspect Sentry issues or events, summarize recent production errors, or pull basic Sentry health data via the Sentry API; perform read-only queries with the bundled script and require `SENTRY_AUTH_TOKEN`.

Open repository

Best for

Primary workflow: Analyze Data & AI.

Technical facets: Full Stack, Backend, Data / AI.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: MoizIbnYousaf.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install sentry into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/MoizIbnYousaf/Ai-Agent-Skills before adding sentry to shared team environments
  • Use sentry for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: "sentry"
description: "Use when the user asks to inspect Sentry issues or events, summarize recent production errors, or pull basic Sentry health data via the Sentry API; perform read-only queries with the bundled script and require `SENTRY_AUTH_TOKEN`."
---


# Sentry (Read-only Observability)

## Quick start

- If not already authenticated, ask the user to provide a valid `SENTRY_AUTH_TOKEN` (read-only scopes such as `project:read`, `event:read`) or to log in and create one before running commands.
- Set `SENTRY_AUTH_TOKEN` as an env var.
- Optional defaults: `SENTRY_ORG`, `SENTRY_PROJECT`, `SENTRY_BASE_URL`.
- Defaults: org/project `{your-org}`/`{your-project}`, time range `24h`, environment `prod`, limit 20 (max 50).
- Always call the Sentry API (no heuristics, no caching).

If the token is missing, give the user these steps:
1. Create a Sentry auth token: https://sentry.io/settings/account/api/auth-tokens/
2. Create a token with read-only scopes such as `project:read`, `event:read`, and `org:read`.
3. Set `SENTRY_AUTH_TOKEN` as an environment variable in their system.
4. Offer to guide them through setting the environment variable for their OS/shell if needed.
- Never ask the user to paste the full token in chat. Ask them to set it locally and confirm when ready.

## Core tasks (use bundled script)

Use `scripts/sentry_api.py` for deterministic API calls. It handles pagination and retries once on transient errors.

## Skill path (set once)

```bash
export CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
export SENTRY_API="$CODEX_HOME/skills/sentry/scripts/sentry_api.py"
```

User-scoped skills install under `$CODEX_HOME/skills` (default: `~/.codex/skills`).

### 1) List issues (ordered by most recent)

```bash
python3 "$SENTRY_API" \
  list-issues \
  --org {your-org} \
  --project {your-project} \
  --environment prod \
  --time-range 24h \
  --limit 20 \
  --query "is:unresolved"
```

### 2) Resolve an issue short ID to issue ID

```bash
python3 "$SENTRY_API" \
  list-issues \
  --org {your-org} \
  --project {your-project} \
  --query "ABC-123" \
  --limit 1
```

Use the returned `id` for issue detail or events.

### 3) Issue detail

```bash
python3 "$SENTRY_API" \
  issue-detail \
  1234567890
```

### 4) Issue events

```bash
python3 "$SENTRY_API" \
  issue-events \
  1234567890 \
  --limit 20
```

### 5) Event detail (no stack traces by default)

```bash
python3 "$SENTRY_API" \
  event-detail \
  --org {your-org} \
  --project {your-project} \
  abcdef1234567890
```

## API requirements

Always use these endpoints (GET only):

- List issues: `/api/0/projects/{org_slug}/{project_slug}/issues/`
- Issue detail: `/api/0/issues/{issue_id}/`
- Events for issue: `/api/0/issues/{issue_id}/events/`
- Event detail: `/api/0/projects/{org_slug}/{project_slug}/events/{event_id}/`

## Inputs and defaults

- `org_slug`, `project_slug`: default to `{your-org}`/`{your-project}` (avoid non-prod orgs).
- `time_range`: default `24h` (pass as `statsPeriod`).
- `environment`: default `prod`.
- `limit`: default 20, max 50 (paginate until limit reached).
- `search_query`: optional `query` parameter.
- `issue_short_id`: resolve via list-issues query first.

## Output formatting rules

- Issue list: show title, short_id, status, first_seen, last_seen, count, environments, top_tags; order by most recent.
- Event detail: include culprit, timestamp, environment, release, url.
- If no results, state explicitly.
- Redact PII in output (emails, IPs). Do not print raw stack traces.
- Never echo auth tokens.

## Golden test inputs

- Org: `{your-org}`
- Project: `{your-project}`
- Issue short ID: `{ABC-123}`

Example prompt: “List the top 10 open issues for prod in the last 24h.”
Expected: ordered list with titles, short IDs, counts, last seen.


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### scripts/sentry_api.py

```python
#!/usr/bin/env python3
import argparse
import json
import os
import re
import sys
import time
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen

DEFAULT_BASE_URL = "https://sentry.io"
DEFAULT_ORG = "your-org"
DEFAULT_PROJECT = "your-project"
MAX_LIMIT = 50

EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
IP_RE = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")


def redact_string(value):
    value = EMAIL_RE.sub("[REDACTED_EMAIL]", value)
    value = IP_RE.sub("[REDACTED_IP]", value)
    return value


def redact_data(value):
    if isinstance(value, str):
        return redact_string(value)
    if isinstance(value, list):
        return [redact_data(item) for item in value]
    if isinstance(value, dict):
        redacted = {}
        for key, item in value.items():
            if key.lower() in {"email", "ip", "ip_address"}:
                redacted[key] = "[REDACTED]"
            else:
                redacted[key] = redact_data(item)
        return redacted
    return value


def next_cursor(link_header):
    if not link_header:
        return None
    for part in link_header.split(","):
        if 'rel="next"' in part and 'results="true"' in part:
            match = re.search(r'cursor="([^"]+)"', part)
            if match:
                return match.group(1)
    return None


def request_json(url, token, retries=1):
    req = Request(url)
    req.add_header("Authorization", f"Bearer {token}")
    req.add_header("Accept", "application/json")

    attempt = 0
    while True:
        try:
            with urlopen(req) as resp:
                body = resp.read().decode("utf-8")
                data = json.loads(body) if body else None
                return data, resp.headers
        except HTTPError as err:
            body = err.read().decode("utf-8", "ignore")
            if attempt < retries and (err.code >= 500 or err.code == 429):
                attempt += 1
                time.sleep(1)
                continue
            raise RuntimeError(f"HTTP {err.code} for {url}: {body or 'request failed'}") from err
        except URLError as err:
            if attempt < retries:
                attempt += 1
                time.sleep(1)
                continue
            raise RuntimeError(f"Network error for {url}: {err.reason}") from err


def build_url(base_url, path, params=None):
    base = base_url.rstrip("/")
    url = f"{base}{path}"
    if params:
        url = f"{url}?{urlencode(params, doseq=True)}"
    return url


def paged_get(base_url, path, params, token, limit):
    results = []
    cursor = None
    while len(results) < limit:
        page_params = dict(params)
        page_params["per_page"] = min(MAX_LIMIT, limit - len(results))
        if cursor:
            page_params["cursor"] = cursor
        url = build_url(base_url, path, page_params)
        data, headers = request_json(url, token)
        if not data:
            break
        results.extend(data)
        cursor = next_cursor(headers.get("Link"))
        if not cursor:
            break
    return results[:limit]


def require_org_project(org, project):
    if org == DEFAULT_ORG or project == DEFAULT_PROJECT:
        raise RuntimeError(
            "Missing org/project. Set SENTRY_ORG and SENTRY_PROJECT or pass --org/--project."
        )


def handle_list_issues(args, token, base_url):
    require_org_project(args.org, args.project)
    limit = min(args.limit, MAX_LIMIT)
    params = {
        "statsPeriod": args.time_range,
        "environment": args.environment,
    }
    if args.query:
        params["query"] = args.query

    path = f"/api/0/projects/{args.org}/{args.project}/issues/"
    issues = paged_get(base_url, path, params, token, limit)
    return issues


def handle_issue_detail(args, token, base_url):
    path = f"/api/0/issues/{args.issue_id}/"
    url = build_url(base_url, path)
    data, _ = request_json(url, token)
    return data


def handle_issue_events(args, token, base_url):
    limit = min(args.limit, MAX_LIMIT)
    path = f"/api/0/issues/{args.issue_id}/events/"
    events = paged_get(base_url, path, {}, token, limit)
    return events


def handle_event_detail(args, token, base_url):
    require_org_project(args.org, args.project)
    path = f"/api/0/projects/{args.org}/{args.project}/events/{args.event_id}/"
    url = build_url(base_url, path)
    data, _ = request_json(url, token)
    if data and not args.include_entries:
        data = dict(data)
        data.pop("entries", None)
    return data


def build_parser():
    parser = argparse.ArgumentParser(
        description="Read-only Sentry API helper for issues and events"
    )
    parser.add_argument(
        "--base-url",
        default=os.environ.get("SENTRY_BASE_URL", DEFAULT_BASE_URL),
        help="Sentry base URL (default: https://sentry.io)",
    )
    parser.add_argument(
        "--org",
        default=os.environ.get("SENTRY_ORG", DEFAULT_ORG),
        help="Sentry org slug",
    )
    parser.add_argument(
        "--project",
        default=os.environ.get("SENTRY_PROJECT", DEFAULT_PROJECT),
        help="Sentry project slug",
    )
    parser.add_argument(
        "--no-redact",
        action="store_true",
        help="Do not redact PII in output",
    )

    subparsers = parser.add_subparsers(dest="command", required=True)

    list_issues = subparsers.add_parser("list-issues", help="List issues")
    list_issues.add_argument("--time-range", default="24h")
    list_issues.add_argument("--environment", default="prod")
    list_issues.add_argument("--query", default="")
    list_issues.add_argument("--limit", type=int, default=20)

    issue_detail = subparsers.add_parser("issue-detail", help="Issue detail")
    issue_detail.add_argument("issue_id")

    issue_events = subparsers.add_parser("issue-events", help="Issue events")
    issue_events.add_argument("issue_id")
    issue_events.add_argument("--limit", type=int, default=20)

    event_detail = subparsers.add_parser("event-detail", help="Event detail")
    event_detail.add_argument("event_id")
    event_detail.add_argument(
        "--include-entries",
        action="store_true",
        help="Include event entries (may contain stack traces)",
    )

    return parser


def main():
    parser = build_parser()
    args = parser.parse_args()

    token = os.environ.get("SENTRY_AUTH_TOKEN")
    if not token:
        raise RuntimeError("Missing SENTRY_AUTH_TOKEN env var.")

    base_url = args.base_url

    if args.command == "list-issues":
        data = handle_list_issues(args, token, base_url)
    elif args.command == "issue-detail":
        data = handle_issue_detail(args, token, base_url)
    elif args.command == "issue-events":
        data = handle_issue_events(args, token, base_url)
    elif args.command == "event-detail":
        data = handle_event_detail(args, token, base_url)
    else:
        raise RuntimeError(f"Unknown command: {args.command}")

    if not args.no_redact:
        data = redact_data(data)

    print(json.dumps(data, indent=2, sort_keys=True))


if __name__ == "__main__":
    try:
        main()
    except RuntimeError as exc:
        print(f"Error: {exc}", file=sys.stderr)
        sys.exit(1)

```



---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### assets/sentry-small.svg

```svg
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 50 44" fill="currentColor" aria-hidden="true">
  <path d="M29,2.26a4.67,4.67,0,0,0-8,0L14.42,13.53A32.21,32.21,0,0,1,32.17,40.19H27.55A27.68,27.68,0,0,0,12.09,17.47L6,28a15.92,15.92,0,0,1,9.23,12.17H4.62A.76.76,0,0,1,4,39.06l2.94-5a10.74,10.74,0,0,0-3.36-1.9l-2.91,5a4.54,4.54,0,0,0,1.69,6.24A4.66,4.66,0,0,0,4.62,44H19.15a19.4,19.4,0,0,0-8-17.31l2.31-4A23.87,23.87,0,0,1,23.76,44H36.07a35.88,35.88,0,0,0-16.41-31.8l4.67-8a.77.77,0,0,1,1.05-.27c.53.29,20.29,34.77,20.66,35.17a.76.76,0,0,1-.68,1.13H40.6q.09,1.91,0,3.81h4.78A4.59,4.59,0,0,0,50,39.43a4.49,4.49,0,0,0-.62-2.28Z"/>
</svg>

```

### Binary Assets

- assets/sentry.png

sentry | SkillHub