_global/sentry-triage
Weekly Sentry triage across the cms, telecom and renovation repos — classify unresolved production errors, fix the ones that are safely code-fixable via a draft PR per repo, journal the rest. Ignores third-party and infrastructure noise.
Sentry triage — cms, telecom, renovation
You are in the _global workspace. The repos to triage are the ones listed in
the REPOS block of your prompt: each is a full git clone, with the branch to
work from and the Sentry project that reports on it.
Your prompt also carries a GATE block. A probe already ran
sentry worklist and produced the worklist for this run — every issue not yet
recorded in triage memory, every one whose event rate says it is still bleeding,
and every REPORT a human has left unacted for a month. Work that list. Do not run sentry list to build your own:
the gate is what decided this run should happen at all, and a run that widens its
own scope is doing work nobody admitted.
The sentry CLI (on PATH; fallback /app/tools/bin/sentry) is your only Sentry
access. Every command you use is read-only: never resolve, ignore, assign or
comment on Sentry issues, and never run sentry resolve. Closing an issue is
not a triage decision — it is settled after the fact by _global:sentry-verify,
which checks the fix is in the running build and that the errors then stopped.
Your job is to record the fix ref (step 5) so that check can happen at all. Pass --project <slug> to look at a specific repo's project; the slugs
are in the REPOS block.
Step 0 — Load triage memory
Read memory/review-feedback.md — binding rules from human review of past PRs,
and they apply to every repo, not just the one they were learned in. A fix
that violates one will be rejected, so classify such issues REPORT instead.
Then read memory/sentry-triage-<repo>.md for each repo you are triaging. The
gate already used these to build the worklist, so you do not need them to decide
what to look at — you need them for context on what past runs concluded, and
you will append to them in step 4.
Step 1 — Take the worklist
For each issue on the gate's list, get the detail you need:
sentry show <shortId> --project <slug>
sentry trace <shortId> --project <slug>
sentry urls <shortId> --project <slug>
urls shows which sites and pages are affected — that goes in the triage record
and the PR body. An issue the gate marked REGRESSED or UNFIXED was triaged
before: read its old record first, because "we already decided this was infra",
"we fixed this and it came back", and "nobody ever acted on this" need different
answers. UNFIXED means the row is still classed REPORT a month later and still
growing — so the question is not "what is this", it is "why has nothing happened,
and can this run do better than the last one did". A line carrying
register=OS-nnn is already in decisions.md: read that entry before writing a
new one, and if its diagnosis no longer matches what you can see, say so — a
wrong entry a human already answered is worse than no entry.
A line marked FIX-FAILED is the one to slow down on. It means a past run's fix
is running in production and the errors did not stop — that has been checked
against the deployed build, not assumed from a merge. So the previous diagnosis
was wrong, or incomplete, and the answer is almost never to apply the same shape
of fix again. Re-read the current sentry trace from scratch, read the old fix's
diff, and say plainly what the last run got wrong. If you cannot better it, it is
a REPORT — a second failed guess is worse than an honest escalation.
If the gate's output ends in a [TRUNCATED: ...] marker, some of its list never
reached you. Work what you have and say so in your final report — do not
present a partial list as the whole board.
Step 2 — Classify
Every candidate gets exactly one class:
- FIX — root cause is in repo code, the failing scenario is unambiguous from
the event data, and the fix is small and low-risk: missing null/empty checks,
bad array keys, type mismatches, missing route params, malformed entries in
data files. An unhandled third-party failure counts as FIX when the right fix
is graceful handling in our code (e.g. an upstream API times out and the page
500s instead of degrading gracefully) — but read
review-feedback.mdfirst, because hiding an upstream failure is explicitly not an acceptable fix. - REPORT — real bug, but not safe for an unattended fix: the correct behavior is ambiguous, it involves business logic or factual data, or it sits in heavily shared core code where the blast radius is unclear. List it in your final report so a human routes it to the right team.
- FIX (in a dependency) — the trace lands in a package that is ours and
that you have a clone of.
sentry tracewill show it as a vendor path (vendor/selectra/comparator-core/...), which looks like third-party noise and is not: check the path against theREPOSblock before writing it off. Fix it in that repo's clone, on its own branch, against its own base —comparator-coreisv3, notdevelop. Never edit avendor/path directly; it is a build artifact, and in these clones it is not even installed. - IGNORE: third-party — noise we can't and shouldn't fix: vendor-only stack
traces with no in-app frames in a package we do not own, browser-extension/injected-script errors,
crawler and probe junk (requests to
/wp-admin,.phpexploits, malformed URLs), CSP violations from external scripts. - IGNORE: infra — not code-fixable: DB/Redis connection failures or timeouts, DNS errors, upstream 502/503/504, out-of-memory, deploy-window blips. Exception: if the same infra error recurs heavily and code-level resilience would genuinely help, reclassify as FIX (or REPORT if too risky).
A vendor frame is where an error surfaced, not where it came from. Before
attributing a root cause to a package — and above all before opening a register
entry about the package — check whether our own repo explains it. Two cheap
checks, either of which would have saved 29,000 events on
TELECOM-COMPARATOR-NT: git log --follow the class or file in the trace, since
a rename months ago still breaks anything that persisted the old name; and grep
the repo for the failing symbol, because if nothing in app/ can produce it, the
source is data and not code. Class … not found raised from a vendor
recovery/queue path is almost always a stored row naming something we renamed,
not a bug in the package that read the row. State which check you ran.
The same trap has a data-shaped version: an error that grows without plateauing usually means a failure that never clears its own trigger — a retried row that is never marked failed, a cache never invalidated. Find the loop before you name a culprit; "it grows" is evidence about the mechanism, not about whose fault it is.
When genuinely unsure between FIX and anything else, do not fix — classify as REPORT and explain the doubt. A wrong "fix" is worse than a week's delay.
Blast radius differs by repo, and so does the bar. cms is a monorepo
serving dozens of sites in several languages from shared app/ and
departments/ code: there, "safe for the site that errored" is not the bar,
"safe for every site touching this code" is. comparator-core is a shared
library and sits on the same side of that line — every comparator that depends on
it inherits the change, and you can only see one of those consumers from here, so
the bar is higher than for the repo that reported the error, not lower.
telecom and renovation are single applications, so shared code there has one
caller set you can actually read. Apply the caution the repo warrants — neither
wave a shared-code change through, nor refuse a safe single-app fix because a
rule about shared code says shared code is scary.
Step 3 — Fix
Cap the run at 3 fixes per repo and 6 in total, highest events × users
first; journal the overflow as FIX (deferred) for the next run. Finish one repo
before starting the next, so an interrupted run leaves whole repos done rather
than all of them half done.
Three repos looks like something to parallelise. It is not: do every repo yourself, in this turn. Do not hand repos to background agents and wait for them — this run is headless, your turn ending is the run ending, and anything still in flight is killed. That is not hypothetical: it is how the first three-repo run spent 15 minutes and produced nothing at all. The caps above exist so one turn is enough.
For each repo you are fixing in:
- Read that repo's own
CLAUDE.mdbefore changing anything — its conventions, not the cms's, govern its code. - Map Sentry server paths to the repo (
/var/www/html/…→ repo root) and read the actual code. Verify the bug still exists on the repo's base branch — it may already be fixed but not yet deployed. If so, record asFIX (already fixed, awaiting deploy)and move on. - Branch
countryos/global/<YYYY-MM-DD>-sentry-fixesfromorigin/<base>, where<base>is the base branch shown for that repo in theREPOSblock. One commit per issue, message referencing the short id (e.g.fix: handle empty agency list on annuaire pages (CMS-42)). - Fix the root cause, not the symptom — but stay minimal: handle the exact failing scenario from the event data; no refactors, no drive-by cleanups.
- Re-read the full diff before pushing: balanced Blade directives, no debug
leftovers, behavior unchanged on the happy path, and no comment that restates
the line under it (
review-feedback.md— this is where that gets caught, and two of two PRs on 2026-08-31 shipped one). There is no PHP runtime in this container — the PR's CI is the executable check, your re-read is the local one. - Push and open one draft PR per repo, targeting that repo's base branch:
gh pr create --draft --base <base>, titledSentry fixes <YYYY-MM-DD>. Body: one section per issue with the Sentry link, affected URLs, root cause in plain words, the fix, and how to verify after deploy. If nothing ended up fixable in a repo, open no PR for it.
Never put changes to two repos on one branch or in one PR. They are separate remotes with separate reviewers.
Step 4 — Register what a human has to own
A REPORT that exists only in your final message has not been reported. The Chat
line scrolls away, and next week's gate — seeing the issue recorded in triage
memory — will not raise it again. That is how a real defect goes quiet, and it is
the exact failure decisions.md was built to fix one layer down.
So the worst of them go in the register: countries/_global/decisions.md,
following the format of the entries already there (decisions list --country _global prints it).
The bar, and it is deliberately high: recurring, growing, and unowned. All three. A one-off spike is not recurring; a flat 200 events/week nobody has ever been hurt by is not growing; a bug in code this workspace can fix is not unowned — that is a FIX or a deferred FIX, not a question. At most 2 entries per run. A register with everything in it gets read exactly as often as a report with everything in it, which is the failure being fixed, reintroduced one level up. Everything below the bar stays in the final report, where it belongs.
Pick the type by who answers, not by how serious it is:
| Type | Use when | Goes to |
|---|---|---|
decision |
A trade-off nobody but a human may settle — an abandoned dependency to fork/replace/pin, a fix whose cost needs weighing | Google Chat, answered by replying in the thread |
report |
A real defect that someone else owns and can commit to — infrastructure, another team's service, a repo outside this workspace | A standing GitHub issue. Set the Repo field, or it lands where nobody is looking |
Write for the person answering, not for yourself: what is failing, how much and since when, what the options are, and what you recommend. Give the event count and the trend — "28k events, up 4x since last week" is what makes it urgent, and it is the one thing only you have looked up.
Name every Sentry shortId in the entry, each with its own count. An aggregate ("7 issues, ~28,500 events") cannot be checked by the person answering, and it hides the case where one shortId is 99% of the total. That is not hypothetical: OS-001 asked whether to replace an abandoned dependency and justified the urgency with 28.5k events, of which 28,403 belonged to a single issue the dependency was not causing — and it was answered on that basis. One root cause per entry. If the shortIds do not share a cause, they are two entries, and bundling them buys a decision made about the wrong thing.
Never mark an entry resolved, and never reopen one. Any task may open an entry; only a human closes it. If a past entry is now moot, say so in your report and leave the entry alone.
Step 5 — Record
Update memory/sentry-triage-<repo>.md for each repo, one line per issue:
| shortId | date | class | events at triage | affected sites | note / PR | fix ref |
The shortId and the event count are what the gate parses next week, so get those
two columns right — a missing count means the issue can never be seen to have
regressed, and a missing row means it comes back as NEW forever. The count must
lead column 4; annotating after it is fine and useful (582 (was 241 on 2026-07-02)), but a count that does not come first is read wrong.
If the issue has a decisions.md entry, put its id in the class or note column
(REPORT (OS-003)). The gate prints it on the worklist line, which is what stops
a future run from re-diagnosing something a human is already holding — or, as
happened here, from leaving a wrong diagnosis standing because nothing connected
the row to the entry.
Every row you classify FIX must carry column 7, fix ref — exactly one of
PR #18283, PR <owner>/<repo>#25 (a fix that landed in another repo),
sha 4124f1ca5d, or none when nothing shipped that Sentry could observe. Write
the PR number you actually opened this run; do not guess one.
That column is the whole loop. sentry verify uses it to find the commit, check
whether it is in a build that is actually serving traffic, and then ask whether
the errors stopped — after which sentry resolve closes the issue in Sentry, or
next week's gate raises it as FIX-FAILED. A row without it is never resolved and
never verified: the fix stays "unresolved" in Sentry forever, which is exactly the
state this column was added to end. Prose in column 6 is not a substitute — it is
free text and nothing parses it.
Append the usual dated entry to memory/journal.md. Final message: counts per
class per repo, the PR URL for each repo that got one, any decisions.md
entries you opened (by id), and the REPORT items that stayed below the register
bar.