From d4a9df538f635ae11cf47bb8e8cde1e63674b8de Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Thu, 9 Apr 2026 03:17:09 +0530 Subject: [PATCH] Switch Sentry skill from bundled Python script to Sentry CLI (#336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Replace the custom `sentry_api.py` script with native Sentry CLI commands, which handle authentication, org/project auto-detection, pagination, and retries out of the box. - Update `SKILL.md` with CLI-based workflows covering issue inspection, trace exploration, log streaming, and a generic API fallback for uncovered endpoints. - Update the agent description in `openai.yaml` to reflect the CLI-based approach. ## Motivation The bundled Python script was a thin wrapper around Sentry's REST API that required manual auth token setup and explicit org/project configuration. The Sentry CLI (`sentry-cli`) handles all of this natively — including interactive auth, DSN-based project detection, and structured JSON output — making the script redundant and the skill simpler to use. --- skills/.curated/sentry/SKILL.md | 123 +++++----- skills/.curated/sentry/scripts/sentry_api.py | 238 ------------------- 2 files changed, 60 insertions(+), 301 deletions(-) delete mode 100755 skills/.curated/sentry/scripts/sentry_api.py diff --git a/skills/.curated/sentry/SKILL.md b/skills/.curated/sentry/SKILL.md index 744f28a..c94f7ef 100644 --- a/skills/.curated/sentry/SKILL.md +++ b/skills/.curated/sentry/SKILL.md @@ -1,6 +1,6 @@ --- 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`." +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 CLI; perform read-only queries using the `sentry` command." --- @@ -8,102 +8,99 @@ description: "Use when the user asks to inspect Sentry issues or events, summari ## 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 not already authenticated, ask the user to run `sentry auth login` or set `SENTRY_AUTH_TOKEN` as an env var. +- The CLI auto-detects org/project from DSNs in `.env` files, source code, config defaults, and directory names. Only specify `/` if auto-detection fails or picks the wrong target. +- Defaults: time range `24h`, environment `production`, limit 20. +- Always use `--json` when processing output programmatically. Use `--json --fields` to select specific fields and reduce output size. +- Use `sentry schema ` to discover API endpoints quickly. -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. +If the CLI is not installed, give the user these steps: +1. Install the Sentry CLI: `curl https://cli.sentry.dev/install -fsS | bash` +2. Authenticate: `sentry auth login` +3. Confirm authentication: `sentry auth status` - 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) +## Core tasks (use Sentry CLI) -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`). +Use the `sentry` CLI for all queries. It handles authentication, org/project detection, pagination, and retries automatically. Use `--json` for machine-readable output. ### 1) List issues (ordered by most recent) ```bash -python3 "$SENTRY_API" \ - list-issues \ - --org {your-org} \ - --project {your-project} \ - --environment prod \ - --time-range 24h \ +sentry issue list \ + --query "is:unresolved environment:production" \ + --period 24h \ --limit 20 \ - --query "is:unresolved" + --json --fields shortId,title,priority,level,status ``` -### 2) Resolve an issue short ID to issue ID +If auto-detection doesn't resolve org/project, pass them explicitly: +```bash +sentry issue list {your-org}/{your-project} \ + --query "is:unresolved environment:production" \ + --period 24h \ + --limit 20 \ + --json +``` + +### 2) Resolve an issue short ID to issue detail ```bash -python3 "$SENTRY_API" \ - list-issues \ - --org {your-org} \ - --project {your-project} \ - --query "ABC-123" \ - --limit 1 +sentry issue view {ABC-123} --json ``` -Use the returned `id` for issue detail or events. +Use the short ID format (e.g., `ABC-123`), not the numeric ID. ### 3) Issue detail ```bash -python3 "$SENTRY_API" \ - issue-detail \ - 1234567890 +sentry issue view {ABC-123} ``` ### 4) Issue events ```bash -python3 "$SENTRY_API" \ - issue-events \ - 1234567890 \ - --limit 20 +sentry issue events {ABC-123} --limit 20 --json ``` -### 5) Event detail (no stack traces by default) +### 5) Event detail ```bash -python3 "$SENTRY_API" \ - event-detail \ - --org {your-org} \ - --project {your-project} \ - abcdef1234567890 +sentry event view {your-org}/{your-project}/{event_id} --json ``` -## API requirements +### 6) AI-powered root cause analysis -Always use these endpoints (GET only): +```bash +sentry issue explain {ABC-123} +``` -- 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}/` +### 7) AI-powered fix plan + +```bash +sentry issue plan {ABC-123} +``` + +## Fallback: arbitrary API access + +For endpoints not covered by dedicated CLI commands, use `sentry api`: +```bash +sentry api /api/0/organizations/{your-org}/ --method GET +``` + +Use `sentry schema` to discover available API endpoints: +```bash +sentry schema issues +``` ## 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. +- `org_slug`, `project_slug`: auto-detected by the CLI from DSNs, env vars, and directory names. Override with positional `{your-org}/{your-project}` if auto-detection fails. +- `time_range`: default `24h` (pass as `--period 24h`). +- `environment`: default `prod` (pass as part of `--query`, e.g., `environment:production`). +- `limit`: default 20 (pass as `--limit`). +- `search_query`: optional `--query` parameter, uses Sentry search syntax (e.g., `is:unresolved`, `assigned:me`). +- `issue_short_id`: use directly with `sentry issue view`. ## Output formatting rules @@ -119,5 +116,5 @@ Always use these endpoints (GET only): - Project: `{your-project}` - Issue short ID: `{ABC-123}` -Example prompt: “List the top 10 open issues for prod in the last 24h.” +Example prompt: "List the top 10 open issues for prod in the last 24h." Expected: ordered list with titles, short IDs, counts, last seen. diff --git a/skills/.curated/sentry/scripts/sentry_api.py b/skills/.curated/sentry/scripts/sentry_api.py deleted file mode 100755 index a9858b7..0000000 --- a/skills/.curated/sentry/scripts/sentry_api.py +++ /dev/null @@ -1,238 +0,0 @@ -#!/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)