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.
Install command
npx @skill-hub/cli install moizibnyousaf-ai-agent-skills-sentry
Repository
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 repositoryBest 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
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