Sentry Triage
Gate-and-escalate triage: pull not-resolved Sentry issues for a project, apply a qualifying gate, cross-reference against GitHub, and (only with explicit approval) escalate survivors to GitHub issues. Reads only — this skill never mutates Sentry state.
Inputs to establish first
- Org + project slug(s) — from the caller or by listing projects. Don't hunt for phantom projects: platforms sharing a DSN don't appear separately.
- Gate policy — the caller's thresholds, or the defaults in
references/qualifying-gate.md. - Escalation target — repo (and optional board IDs) if filing is on the table; otherwise this is a read-only report.
Workflow
1. Pull (discovery only)
MCP-first: use the connected Sentry MCP server's project-listing and issue-search tools (resolve the project slug first, then search). Do not hardcode mcp__... tool IDs — resolve by capability. No MCP connected → references/api-fallback.md (REST with SENTRY_AUTH_TOKEN).
Query !is:resolved — one call covers unresolved + ignored (status is exclusive; boolean OR returns HTTP 400 — full syntax notes in references/query-syntax.md). Pull per issue: shortId, title, count, userCount, lastSeen, firstSeen, level, culprit, permalink, status.
Issue search is a DISCOVERY tool, never a SCORING tool. It tells you which fingerprints exist; its counts must never reach a gate or a plate. The MCP's search_issues renders Events and Users scoped to its period argument, labelled identically to lifetime totals and with nothing in the output marking them as windowed — so a long-running issue reads as a one-off. Measured on Sassy-Dog/velovate, 2026-08-20 and re-verified live 2026-08-21: VELOVATE-WORKERS-2 returned 1 event / 1 user against a true 30 occurrences / 3 users, a 30× undercount, and the 30d pull returned 1 event too — widening period is not a workaround (issue #218). firstSeen is rescaled the same way: search reported it as 2 days ago against a true first-seen of 2026-06-25, 57 days earlier, so a long-running fingerprint reads as a fresh regression from the latest deploy. On the true numbers that issue qualifies (production environment, 3 users ≥ 2); on the windowed pair it fails every clause of the severity rule. The gate outcome inverts, silently. The same names mean different things per transport, and REST is the one that is already lifetime; the table in references/api-fallback.md is the authority for both.
Then resolve environments before gating (gate rule 6). Issue search does not return an environment breakdown, and an issue can span several, so ask the events dataset once per project rather than per issue: an aggregate over the errors dataset with fields ["environment", "count()"] grouped by environment, scoped to the same window. An issue whose events are entirely in a non-production environment is skip-nonprod. Skipping this step is not a smaller version of the gate — it is the version that ranks CI noise as a customer-facing outage, because a CI environment manufactures userCount (see the rule 6 rationale).
2. Confirm counts (gate rule 0)
Apply the count-independent gate rules first — status, staleness, parked, environment (rules 1, 2, 5, 6) — then fetch true lifetime Occurrences and Users Impacted for every survivor, via the connected server's resource-fetch capability (get_sentry_resource with resourceType='issue' and the SHORT-ID; resolve by capability, never a literal mcp__... id). Only then apply the count rule.
Never use a windowed count to decide which issues are worth confirming. That is the defect, not a shortcut around it: the issues that most need confirming are precisely the ones a window under-reports, so any pre-filter computed from the search counts re-creates the bug one step earlier. The ordering above exists so the cheap rules — none of which reads a count — do the narrowing instead.
An issue whose counts could not be confirmed is skip-unconfirmed: it never qualifies, and it is reported, never dropped. Unconfirmed is not clean and it is not noise; it is a number this skill declined to trust.
3. Gate
Read references/qualifying-gate.md; apply each rule and tag non-qualifiers (skip-noise / skip-stale / skip-parked / skip-nonprod / skip-unconfirmed / already-linked) so the report shows why something didn't escalate, not just that it didn't.
Cross-reference against GitHub by marker: gh issue list --search '"sentry-source: <SHORT_ID>" in:body' --state all. That search is a hint, not a verdict — it matches a token subsequence, so it also hits sentry-source: <SHORT_ID>-<something>; a hit may annotate the preview but must never drop a candidate. file-or-link-issue.sh owns the already-linked decision (see rule 4 in references/qualifying-gate.md). If the caller also pulled TestFlight crash feedback, fuzzy-merge on culprit/stack signature before reporting — the same crash in two surfaces is one item, not two.
4. Report
One line per issue: SHORT_ID · title · events/users · lastSeen · level · gate verdict · GH peer (#N or —). Qualifiers first, then skips grouped by reason. No section for empty buckets.
Every counts figure renders its provenance — 30/3 (lifetime) or 1/1 (windowed, unconfirmed). A confirmed and an unconfirmed count are otherwise indistinguishable on the page, which is how a windowed number gets quoted onward into a plate as though it had been verified.
5. Escalate (only if asked, never silently)
Filing goes through sassy-dog:github-issues — its file-or-link-issue.sh with --marker "sentry-source: <SHORT_ID>", following that skill's preview-then-confirm contract and burst rail (> 5 candidates → stop and summarize). Never raw gh issue create.
Hard prohibitions
- Never resolve, ignore, assign, or otherwise mutate Sentry issues.
- Never escalate
ignoredissues — ignoring was a human decision. - Never file without a preview the user approved in this run.
- Never score, rank, escalate, or dismiss an issue on counts this skill has not confirmed as lifetime.