diff --git a/plugins/sentry/.codex-plugin/plugin.json b/plugins/sentry/.codex-plugin/plugin.json index a578ee46..74760066 100644 --- a/plugins/sentry/.codex-plugin/plugin.json +++ b/plugins/sentry/.codex-plugin/plugin.json @@ -1,11 +1,10 @@ { "name": "sentry", - "version": "0.1.0", - "description": "Inspect recent issues and events in Sentry from Codex.", + "version": "0.2.0", + "description": "Investigate Sentry with the official CLI and use a separate setup skill for instrumentation and SDK features.", "author": { - "name": "OpenAI", - "email": "support@openai.com", - "url": "https://openai.com/" + "name": "Sentry", + "url": "https://sentry.io/" }, "homepage": "https://sentry.io/", "repository": "https://github.com/openai/plugins", @@ -18,9 +17,9 @@ "skills": "./skills/", "interface": { "displayName": "Sentry", - "shortDescription": "Inspect recent Sentry issues and events", - "longDescription": "Use Sentry skills to inspect recent issues, review events, and summarize production errors with a read-only workflow.", - "developerName": "OpenAI", + "shortDescription": "Investigate with Sentry CLI and separate setup guidance", + "longDescription": "Use Sentry skills to query issues, events, traces, spans, logs, dashboards, organizations, and projects through the official Sentry CLI, and route installation, instrumentation, and feature work to a dedicated setup skill backed by current guidance from skills.sentry.dev.", + "developerName": "Sentry", "category": "Productivity", "capabilities": [ "Interactive", @@ -30,7 +29,8 @@ "privacyPolicyURL": "https://sentry.io/privacy/", "termsOfServiceURL": "https://sentry.io/terms/", "defaultPrompt": [ - "Inspect recent Sentry issues and summarize the top production errors" + "Inspect recent Sentry issues, traces, or logs and summarize likely root cause and next steps", + "Help install or configure the right Sentry SDK for this project" ], "composerIcon": "./assets/sentry-small.svg", "logo": "./assets/sentry.png", diff --git a/plugins/sentry/skills/sentry-setup/SKILL.md b/plugins/sentry/skills/sentry-setup/SKILL.md new file mode 100644 index 00000000..62f82133 --- /dev/null +++ b/plugins/sentry/skills/sentry-setup/SKILL.md @@ -0,0 +1,172 @@ +--- +name: "sentry-setup" +description: "Use when the user wants to install Sentry, update instrumentation, enable or tune SDK features, or fetch framework-specific setup guidance from skills.sentry.dev. This skill covers platform detection, routing to the right upstream Sentry skill, and applying or updating instrumentation safely." +--- + +# Sentry Setup + +Use this skill for installation and source-code changes: adding Sentry to a project, updating instrumentation, upgrading SDK setup, or enabling features like Replay, Logs, Profiling, AI monitoring, Crons, alerts, and OpenTelemetry-related workflows. + +If the user instead wants to inspect live Sentry data, route to `../sentry/SKILL.md`. + +## Responsibilities + +Use this skill when the user asks to: + +- add Sentry to a project +- install `@sentry/...` or another Sentry SDK +- configure Sentry in Next.js, React, Node.js, Python, Go, Cloudflare, or another framework +- update `instrumentation.ts`, `global-error.tsx`, init files, DSNs, or SDK wiring +- enable or adjust Replay, Logs, Profiling, AI monitoring, Crons, alerts, metrics, or tracing +- upgrade an SDK or resolve deprecated Sentry APIs + +Do not use this skill for Sentry issue, event, trace, or log investigation. That belongs in `../sentry/SKILL.md`. + +## Required flow + +Do not skip the routing steps. + +1. Detect the project platform from repo files. +2. Tell the user what platform or framework was detected. +3. Recommend the matching upstream Sentry skill or router page. +4. Fetch the specific upstream skill with `curl -sL`. +5. Follow the fetched skill carefully instead of improvising from memory. +6. Only make code changes after the target framework and feature path are clear. + +## Fetch rules + +Use `curl -sL` because these skill files are long and structured: + +```bash +curl -sL https://skills.sentry.dev/sdks +curl -sL https://skills.sentry.dev/workflows +curl -sL https://skills.sentry.dev/features +curl -sL https://skills.sentry.dev//SKILL.md +``` + +Primary entry points: + +- SDK routing: `https://skills.sentry.dev/sdks` +- Workflow routing: `https://skills.sentry.dev/workflows` +- Feature routing: `https://skills.sentry.dev/features` +- Full index: `https://skills.sentry.dev/` + +## Platform detection + +Detect the platform from project files before fetching a framework-specific skill: + +- `package.json` with `next` -> Next.js +- `package.json` with `@nestjs/core` -> NestJS +- `package.json` with `react-native` -> React Native +- `package.json` with `react` and no framework -> React +- `package.json` alone -> Node.js / Bun / Deno +- `build.gradle` with Android plugin -> Android +- `Podfile` or `*.xcodeproj` -> Apple platforms +- `pubspec.yaml` -> Flutter +- `requirements.txt` or `pyproject.toml` -> Python +- `go.mod` -> Go +- `Gemfile` -> Ruby +- `composer.json` -> PHP +- `mix.exs` -> Elixir +- `*.csproj` or `*.sln` -> .NET +- `wrangler.toml` or `wrangler.jsonc` -> Cloudflare +- `svelte.config.js` -> Svelte + +Priority rules: + +- Prefer Next.js over React or generic Node.js. +- Prefer NestJS over generic Node.js. +- Prefer Cloudflare over generic Node.js. +- Prefer React Native over React. +- Prefer framework-specific skills over language-only skills. + +State the recommendation explicitly before proceeding, for example: + +```text +I found a Next.js app, so the right upstream Sentry setup skill is sentry-nextjs-sdk. +I’m fetching that now and will follow its guidance for the instrumentation changes. +``` + +## SDK skills + +Fetch the framework-specific skill that matches the detected platform: + +- Android: `https://skills.sentry.dev/sentry-android-sdk/SKILL.md` +- Browser JavaScript: `https://skills.sentry.dev/sentry-browser-sdk/SKILL.md` +- Cloudflare Workers/Pages: `https://skills.sentry.dev/sentry-cloudflare-sdk/SKILL.md` +- Apple platforms: `https://skills.sentry.dev/sentry-cocoa-sdk/SKILL.md` +- .NET: `https://skills.sentry.dev/sentry-dotnet-sdk/SKILL.md` +- Elixir: `https://skills.sentry.dev/sentry-elixir-sdk/SKILL.md` +- Flutter / Dart: `https://skills.sentry.dev/sentry-flutter-sdk/SKILL.md` +- Go: `https://skills.sentry.dev/sentry-go-sdk/SKILL.md` +- NestJS: `https://skills.sentry.dev/sentry-nestjs-sdk/SKILL.md` +- Next.js: `https://skills.sentry.dev/sentry-nextjs-sdk/SKILL.md` +- Node.js / Bun / Deno: `https://skills.sentry.dev/sentry-node-sdk/SKILL.md` +- PHP: `https://skills.sentry.dev/sentry-php-sdk/SKILL.md` +- Python: `https://skills.sentry.dev/sentry-python-sdk/SKILL.md` +- React Native / Expo: `https://skills.sentry.dev/sentry-react-native-sdk/SKILL.md` +- React: `https://skills.sentry.dev/sentry-react-sdk/SKILL.md` +- Ruby: `https://skills.sentry.dev/sentry-ruby-sdk/SKILL.md` +- Svelte / SvelteKit: `https://skills.sentry.dev/sentry-svelte-sdk/SKILL.md` + +## Feature and update routing + +Use the framework SDK skill for initial installation, then route to feature or upgrade skills when the user’s ask is narrower: + +- SDK upgrades or deprecated APIs: `https://skills.sentry.dev/sentry-sdk-upgrade/SKILL.md` +- AI monitoring: `https://skills.sentry.dev/sentry-setup-ai-monitoring/SKILL.md` +- Alerts: `https://skills.sentry.dev/sentry-create-alert/SKILL.md` +- OpenTelemetry Collector setup: `https://skills.sentry.dev/sentry-otel-exporter-setup/SKILL.md` + +If the user is asking about fixing production issues or Sentry PR comments rather than instrumentation, use the workflows router: + +- `https://skills.sentry.dev/workflows` + +## Feature guidance + +When the upstream skill leaves implementation choices open, use these defaults: + +- Error monitoring: always include it +- Tracing: default on for server and client frameworks where the upstream skill recommends it +- Replay: recommend for user-facing browser apps +- Logs: recommend when the app already uses structured logging or needs log-to-trace correlation +- Profiling: recommend for performance-sensitive apps only when platform support is clear +- AI monitoring: recommend only when the app actually makes LLM calls +- Crons: recommend only when there are scheduled jobs or cron-like workflows in the codebase + +Do not enable optional features blindly. Tie them to the actual app and user request. + +## Next.js path + +When the detected platform is Next.js: + +1. Fetch `https://skills.sentry.dev/sentry-nextjs-sdk/SKILL.md`. +2. Follow its detection steps before editing files. +3. Prefer the official wizard when the user is open to it: + +```bash +npx @sentry/wizard@latest -i nextjs +``` + +4. If the user wants manual changes, follow the current upstream guidance for: + - `instrumentation-client.ts` + - `sentry.server.config.ts` + - `sentry.edge.config.ts` + - `instrumentation.ts` + - `app/global-error.tsx` or `pages/_error.tsx` +5. Merge with existing instrumentation files instead of overwriting them. + +## Safety rules + +- Do not guess SDK APIs when the upstream skill can be fetched. +- Do not overwrite an existing instrumentation file without reading and merging it. +- Do not invent DSNs, orgs, or project names. +- If multiple runtimes exist, make the runtime split explicit. +- Keep changes aligned with the project’s existing package manager and file naming conventions. + +## Output expectations + +- State the detected platform and the upstream skill you fetched. +- Summarize the exact instrumentation or feature changes you made or recommend. +- Call out any interactive step the user must run themselves, such as browser-driven wizards. +- If the request turns into live issue investigation instead of setup, switch to `../sentry/SKILL.md`. diff --git a/plugins/sentry/skills/sentry-setup/agents/openai.yaml b/plugins/sentry/skills/sentry-setup/agents/openai.yaml new file mode 100644 index 00000000..070b29b9 --- /dev/null +++ b/plugins/sentry/skills/sentry-setup/agents/openai.yaml @@ -0,0 +1,6 @@ +interface: + display_name: "Sentry Setup" + short_description: "Install and update Sentry instrumentation" + icon_small: "../../../assets/sentry-small.svg" + icon_large: "../../../assets/sentry.png" + default_prompt: "Set up or update Sentry instrumentation for this project using the current upstream guidance." diff --git a/plugins/sentry/skills/sentry/SKILL.md b/plugins/sentry/skills/sentry/SKILL.md index 5f726d49..75d1ee2a 100644 --- a/plugins/sentry/skills/sentry/SKILL.md +++ b/plugins/sentry/skills/sentry/SKILL.md @@ -1,128 +1,198 @@ --- 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 wants to inspect Sentry data from Codex with the official CLI: issues, events, traces, spans, logs, dashboards, orgs, projects, schema, or authenticated API access. This skill is for investigation and querying, not SDK installation or instrumentation changes." --- +# Sentry CLI -# Sentry (Read-only Observability) +Use this skill for read and investigate workflows inside Sentry. If the user wants to install Sentry, update instrumentation, turn on new SDK features, or fetch guidance from `skills.sentry.dev`, route to `../sentry-setup/SKILL.md` immediately. -## Quick start +## Querying Sentry Data -- 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). +Use the Sentry CLI as the primary interface. In Codex, the zero-install path is: -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. +```bash +npx -y sentry@latest ... +``` + +### Core rules + +- Prefer CLI commands over raw API calls. +- Use `--json` for structured output when you need to parse results. +- Use `--fields` to keep JSON payloads small. +- Use `--limit` aggressively. +- Prefer `--period` for time filters. +- Prefer direct lookup by ID over list-then-filter when the identifier is already known. +- Let the CLI auto-detect org/project context when possible; add explicit `org/project` scoping when detection fails or resolves incorrectly. +- Never print or store auth tokens. +- Do not dump large raw JSON into the conversation context. +- Prefer noun-first commands exactly as the CLI exposes them: `issue list`, `trace view`, `event list`, `schema`, `api`. + +### Use this skill for + +- Investigating recent errors or unresolved issues +- Reading issue details, events, traces, spans, logs, dashboards, orgs, or projects +- Asking Sentry for Seer explanations or plans on an issue +- Exploring the Sentry API surface with `schema` +- Using authenticated raw API access when the dedicated command family is not enough + +### Do not use this skill for + +- Installing Sentry into an app +- Adding or updating `instrumentation.ts`, `global-error.tsx`, SDK init files, or DSNs +- Turning on features like Replay, Profiling, Logs, AI monitoring, or Crons in source code +- SDK upgrades or instrumentation migrations -## Core tasks (use bundled script) +For those tasks, switch to `../sentry-setup/SKILL.md`. -Use `scripts/sentry_api.py` for deterministic API calls. It handles pagination and retries once on transient errors. +### Authentication -## Bundled script path +- Prefer CLI-native auth with: ```bash -export SENTRY_API="plugins/sentry/skills/sentry/scripts/sentry_api.py" +npx -y sentry@latest auth login ``` -If you are running from an installed plugin copy instead of this repo checkout, use the same -`skills/sentry/scripts/sentry_api.py` path inside the installed plugin directory. +- To verify auth status: -### 1) List issues (ordered by most recent) +```bash +npx -y sentry@latest auth status --json +``` + +- Token-based auth is allowed when interactive login is not appropriate, but do not ask the user to paste tokens into chat. +- If the user is not authenticated, tell them to run `npx -y sentry@latest auth login` locally and confirm when ready. + +### Fast validation + +Before relying on a command family for the first time in a session, it is reasonable to confirm the CLI surface: ```bash -python3 "$SENTRY_API" \ - --org {your-org} \ - --project {your-project} \ - list-issues \ - --environment prod \ - --time-range 24h \ - --limit 20 \ - --query "is:unresolved" +npx -y sentry@latest --help +npx -y sentry@latest auth --help +npx -y sentry@latest issue --help +npx -y sentry@latest trace --help ``` -### 2) Resolve an issue short ID to issue ID +### Common commands ```bash -python3 "$SENTRY_API" \ - --org {your-org} \ - --project {your-project} \ - list-issues \ - --query "ABC-123" \ - --limit 1 +npx -y sentry@latest issue list --limit 5 +npx -y sentry@latest issue view PROJ-123 +npx -y sentry@latest issue explain PROJ-123 +npx -y sentry@latest issue plan PROJ-123 +npx -y sentry@latest event list PROJ-123 --limit 10 +npx -y sentry@latest event view my-org/my-project/ +npx -y sentry@latest trace list --limit 5 +npx -y sentry@latest trace view my-org/my-project/ +npx -y sentry@latest trace logs my-org/ +npx -y sentry@latest span list my-org/my-project/ +npx -y sentry@latest log list --limit 20 +npx -y sentry@latest dashboard list +npx -y sentry@latest org list +npx -y sentry@latest project list +npx -y sentry@latest schema issues +npx -y sentry@latest api /api/0/organizations/my-org/ ``` -Use the returned `id` for issue detail or events. +### When to use `schema` vs `api` + +- Use `schema` first when you need to discover the shape of an endpoint or confirm the supported path. +- Use `api` only after checking whether a dedicated command family already exists. +- Prefer `issue`, `event`, `trace`, `span`, `log`, `dashboard`, `org`, and `project` over `api`. -### 3) Issue detail +Examples: ```bash -python3 "$SENTRY_API" \ - --org {your-org} \ - issue-detail \ - 1234567890 +npx -y sentry@latest schema "GET /api/0/organizations/{organization_id_or_slug}/issues/" +npx -y sentry@latest api /api/0/organizations/my-org/projects/ --json ``` -### 4) Issue events +### Large JSON handling + +Sentry JSON can be very large. Do not paste raw `--json` output into context. + +Use a temp file, then inspect it with `jq`: + +```bash +npx -y sentry@latest trace view my-org/my-project/ --json > /tmp/sentry-trace.json +jq '.spans[] | {op, description, duration}' /tmp/sentry-trace.json +``` ```bash -python3 "$SENTRY_API" \ - --org {your-org} \ - issue-events \ - 1234567890 \ - --environment prod \ - --time-range 24h \ - --limit 20 +npx -y sentry@latest issue list --json --fields shortId,title,status,count --limit 10 > /tmp/sentry-issues.json +jq '.[] | {shortId, title, status, count}' /tmp/sentry-issues.json ``` -### 5) Event detail (no stack traces by default) +Guidance: +- Default to temp files for `--json` output. +- Extract only the fields needed for the current question. +- Prefer `--fields` plus `jq` over wide tables or full payloads. +- Use `--period` for time filters instead of inventing custom date logic. +- If a single trace or issue dump is large, summarize the subset you actually inspected instead of presenting the whole file. + +### Workflow patterns + +#### Investigate an issue ```bash -python3 "$SENTRY_API" \ - --org {your-org} \ - --project {your-project} \ - event-detail \ - abcdef1234567890 +npx -y sentry@latest issue list --query "is:unresolved" --limit 5 +npx -y sentry@latest issue view @latest +npx -y sentry@latest issue explain @latest +npx -y sentry@latest issue plan @latest ``` -## API requirements +If the user already gave an issue short ID, skip the list step and go straight to `issue view`, `issue events`, `issue explain`, or `issue plan`. -Always use these endpoints (GET only): +#### Explore traces and performance -- List issues: `/api/0/projects/{org_slug}/{project_slug}/issues/` -- Issue detail: `/api/0/organizations/{org_slug}/issues/{issue_id}/` -- Events for issue: `/api/0/organizations/{org_slug}/issues/{issue_id}/events/` -- Event detail: `/api/0/projects/{org_slug}/{project_slug}/events/{event_id}/` +```bash +npx -y sentry@latest trace list --limit 5 +npx -y sentry@latest trace view my-org/my-project/ +npx -y sentry@latest span list my-org/my-project/ +npx -y sentry@latest trace logs my-org/ +``` -## Inputs and defaults +#### Inspect logs -- `org_slug`: default to `{your-org}` (required for issue detail, issue events, and event detail). -- `project_slug`: default to `{your-project}` (required for list issues and event detail). -- `time_range`: default `24h` (pass as `statsPeriod` for list issues and issue events). -- `environment`: default `prod` (used by list issues and issue events). -- `limit`: default 20, max 50 (paginate until limit reached). -- `search_query`: optional `query` parameter. -- `issue_short_id`: resolve via list-issues query first. +```bash +npx -y sentry@latest log list --limit 20 +npx -y sentry@latest log list --query "severity:error" --limit 20 +``` -## Output formatting rules +#### Inspect events -- 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. +```bash +npx -y sentry@latest event list PROJ-123 --limit 20 +npx -y sentry@latest event view my-org/my-project/ +``` -## Golden test inputs +#### Explore schema or raw API -- Org: `{your-org}` -- Project: `{your-project}` -- Issue short ID: `{ABC-123}` +```bash +npx -y sentry@latest schema +npx -y sentry@latest schema issues +npx -y sentry@latest schema "GET /api/0/organizations/{organization_id_or_slug}/issues/" +npx -y sentry@latest api /api/0/organizations/my-org/projects/ +``` -Example prompt: “List the top 10 open issues for prod in the last 24h.” -Expected: ordered list with titles, short IDs, counts, last seen. +### Command families to prefer + +- `auth`: login, status, identity +- `issue`: list, events, view, explain, plan +- `event`: list, view +- `trace`: list, view, logs +- `span`: list, view +- `log`: list, view +- `dashboard`: list, view +- `org`: list, view +- `project`: list, view +- `schema`: explore supported API surfaces +- `api`: authenticated fallback for endpoints not covered by higher-level commands + +## Output expectations + +- Summarize findings instead of dumping raw payloads. +- Call out when no results are returned. +- Mention the concrete commands used when that helps the user reproduce or refine the search. +- Redact obvious secrets or PII if they appear in command output. +- If the user actually wants installation or instrumentation work, say that you are switching to `../sentry-setup/SKILL.md`. diff --git a/plugins/sentry/skills/sentry/agents/openai.yaml b/plugins/sentry/skills/sentry/agents/openai.yaml index 834c3181..35f17b63 100644 --- a/plugins/sentry/skills/sentry/agents/openai.yaml +++ b/plugins/sentry/skills/sentry/agents/openai.yaml @@ -1,6 +1,6 @@ interface: - display_name: "Sentry (Read-only Observability)" - short_description: "Read-only Sentry observability" + display_name: "Sentry CLI" + short_description: "Investigate Sentry with the official CLI" icon_small: "./assets/sentry-small.svg" icon_large: "./assets/sentry.png" - default_prompt: "Investigate this issue in read-only Sentry data and report likely root cause, impact, and next steps." + default_prompt: "Investigate this issue with the Sentry CLI and report likely root cause, impact, and next steps." diff --git a/plugins/sentry/skills/sentry/scripts/sentry_api.py b/plugins/sentry/skills/sentry/scripts/sentry_api.py deleted file mode 100755 index 3fa77216..00000000 --- a/plugins/sentry/skills/sentry/scripts/sentry_api.py +++ /dev/null @@ -1,255 +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 require_org(org): - if org == DEFAULT_ORG: - raise RuntimeError("Missing org. Set SENTRY_ORG or pass --org.") - - -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): - require_org(args.org) - path = f"/api/0/organizations/{args.org}/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): - require_org(args.org) - 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/organizations/{args.org}/issues/{args.issue_id}/events/" - events = paged_get(base_url, path, params, 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("--time-range", default="24h") - issue_events.add_argument("--environment", default="prod") - issue_events.add_argument("--query", default="") - 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)