Skip to content

fix(sentry): group limiter degradation by failure mode, not by route (#6454) - #6458

Merged
koala73 merged 2 commits into
mainfrom
fix/sentry-upstash-abort-fingerprint-6454
Aug 11, 2026
Merged

koala73 merged 2 commits into
mainfrom
fix/sentry-upstash-abort-fingerprint-6454

Conversation

@koala73

@koala73 koala73 commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Closes #6454.

The bug

logRateLimitDegraded fingerprinted on the raw stage, and stage embeds the caller:

checkEndpointRateLimit:/api/market/v1/list-market-quotes
checkScopedRateLimit:/api/skills/fetch-agentskills

Sentry groups by fingerprint, so the token meant to consolidate grouping was fragmenting it — one issue per route for a single Redis slowdown. checkEndpointRateLimit covers 213 gateway routes, so one sustained incident can open up to 213 issues.

It is the exact trap api/_sentry-common.js's own fingerprint doc warns about:

use when the error message contains a high-cardinality token ... that would otherwise split one logical issue into many

Evidence

A triage sweep on 2026-08-11 found seven unresolved issues plus two ignored ones for this single handled condition. Three of them — WORLDMONITOR-YY, YX, YZ — were created within two minutes of each other, sharing release ff93f2137cf9... and differing only by which route was being limited.

That trio ruled out per-deploy line drift as the cause and pointed straight at the fingerprint.

The fix

Collapse the stage to its low-cardinality head, for grouping only:

stage fingerprint segment
checkEndpointRateLimit:/api/market/v1/list-market-quotes checkEndpointRateLimit
checkScopedRateLimit:/api/skills/fetch-agentskills checkScopedRateLimit
checkScopedRateLimit:/api/x:missing-config checkScopedRateLimit:missing-config
checkRateLimit:timeout checkRateLimit:timeout

Failure-mode suffixes (missing-config, timeout) are a closed set and keep their own issues — a slow Redis and an unconfigured Redis are different problems and must not merge.

Two deliberate non-changes:

  • The full stage stays a TAG, so per-route breakdown survives inside the single grouped issue.
  • The Sentry dedup key stays the full stage, so an incident still emits one event per affected route. Collapsing the dedup key too would have reduced volume but left you seeing only one route's tag — which is what makes the breakdown meaningful.

Extracted as a pure rateLimitFingerprintStage() in both mirrors rather than inlined, so the mapping is unit-testable instead of only observable through a live Sentry capture.

Testing

Both layers mutation-proved:

Mutation Result
restore fingerprint: [..., stage] source guard red
neutralise the collapse (return raw stage) 3 unit tests red, incl. mirror-agreement

The pre-existing source-regex guard now also asserts the raw stage is not used as a fingerprint (assert.doesNotMatch), so the regression cannot come back silently — the previous guard only pinned the old literal and would have gone green on any replacement.

typecheck:api             clean  (caught a noUncheckedIndexedAccess slip; typecheck alone does not cover api/)
lint:boundaries           clean
lint:rate-limit-policies  clean
biome                     clean
rate-limit                56/56
edge-functions + docs-mcp + mcp-proxy + rate-limit   384/384
api _rate-limit + wm-session + analytics-health      75/75

Not in scope

WORLDMONITOR-R2 looks like this cluster but is not: route=api/user-prefs, surface=api, error_shape=transport_timeout, convex_fn=userPreferences:getPreferences. It is a Convex transport timeout with its own deliberate fingerprint at api/user-prefs.ts:608 and is correctly separate. Left alone.

After merge

Grouping changes for new events only — the seven existing issues will not retroactively merge. Resolve them once this deploys and let the next incident open the single grouped issue.

Type of change

  • Bug fix
  • New feature
  • Other: observability / Sentry grouping

Affected areas

  • API endpoints (/api/*)
  • Other: server/_shared/rate-limit.ts and its api/_rate-limit.js mirror

Checklist

  • No API keys or secrets committed
  • TypeScript compiles without errors (npm run typecheck:api)
  • Tested — mutation proofs above; no UI surface to exercise in a browser

…6454)

`logRateLimitDegraded` fingerprinted on the raw `stage`, and `stage` embeds the
caller: `checkEndpointRateLimit:/api/market/v1/list-market-quotes`,
`checkScopedRateLimit:/api/skills/fetch-agentskills`. Sentry groups by
fingerprint, so the token meant to consolidate grouping was fragmenting it —
one issue per route for a single Redis slowdown. `checkEndpointRateLimit` covers
213 gateway routes, so one sustained incident can open up to 213 issues.

This is the exact trap `_sentry-common.js`'s own fingerprint doc warns about:
"use when the error message contains a high-cardinality token ... that would
otherwise split one logical issue into many."

Observed: a 2026-08-11 triage sweep found seven unresolved issues plus two
ignored ones for this single handled condition. Three of them
(WORLDMONITOR-YY/YX/YZ) were created within two minutes of each other, sharing
a release and differing only by which route was being limited.

Fix: collapse the stage to its low-cardinality head for grouping only.

  checkEndpointRateLimit:/api/market/v1/list-market-quotes -> checkEndpointRateLimit
  checkScopedRateLimit:/api/skills/fetch-agentskills       -> checkScopedRateLimit
  checkScopedRateLimit:/api/x:missing-config               -> checkScopedRateLimit:missing-config
  checkRateLimit:timeout                                   -> checkRateLimit:timeout

Failure-mode suffixes are a closed set and keep their own issues — a slow Redis
and an unconfigured Redis are different problems. The full stage stays a TAG, and
the Sentry dedup key stays the full stage too, so an incident still emits one
event per affected route; that is what keeps the per-route breakdown meaningful
inside the single grouped issue.

Extracted as a pure `rateLimitFingerprintStage()` in both mirrors rather than
inlined, so the mapping is unit-testable instead of only observable through a
live Sentry capture. The pre-existing source-regex guard now also asserts the raw
stage is NOT used as a fingerprint, so the regression cannot come back silently.

Not in scope: WORLDMONITOR-R2 looks like this cluster but is not — it is a Convex
transport timeout from api/user-prefs with its own deliberate fingerprint. Left
alone.

Grouping changes for NEW events only; the existing issues will not retroactively
merge. Resolve them after deploy and let the next incident open the single issue.

Verified: typecheck:api, lint:boundaries, lint:rate-limit-policies, biome,
rate-limit (56), edge-functions/docs-mcp/mcp-proxy (384 total), and api
_rate-limit/wm-session/analytics-health (75) all pass. Mutation-proved both
layers: restoring the raw-stage fingerprint turns the source guard red, and
neutralising the collapse turns three unit tests red including the mirror-
agreement check.

Closes #6454

Claude-Session: https://claude.ai/code/session_016vZfnBuYuBiwAZE1DADrqz
@vercel

vercel Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview Aug 11, 2026 6:57am

Request Review

@koala73
koala73 merged commit ae0a0fe into main Aug 11, 2026
32 checks passed
@koala73
koala73 deleted the fix/sentry-upstash-abort-fingerprint-6454 branch August 11, 2026 07:14
koala73 added a commit that referenced this pull request Sep 11, 2026
## Summary

A live Pro-MCP failure condition currently reads as drained when it is
not. On 2026-09-11, `Pro MCP token validation temporarily unavailable`
was firing in two Sentry groups at the same time: WORLDMONITOR-ZR,
resolved 2026-09-04 and quiet since, and WORLDMONITOR-T8, which took
three events that same day. Anyone looking at ZR concludes the condition
stopped. It has not.

The cause is that `api/mcp/error-fingerprint.ts` was only ever wired
into `dispatch.ts`. The two captures in `validateProMcpAuthorization`
fall back to Sentry's default stack grouping, and on the minified edge
bundle every frame is an anonymous `(vc/edge/function` with no source
map — so they collapse into the T8 catch-all, exactly the pathology that
helper's own docstring was written to close.

This also means PR #7601 does not do what its commit message says. That
PR downgraded the transient arm to `warning` so a sustained Convex
outage would "still escalate by volume." Volume split across two groups,
one of them a permanently noisy catch-all, escalates nothing. A 25-event
T8 sample held seven distinct messages across three steps and two
routes.

Both arms now carry an explicit fingerprint and stay separable from each
other: `threw` for the defect arm, `transient` for the fail-soft arm.
That preserves a distinction the surrounding comment already protects —
a thrown validator is an unexpected defect, not the fail-soft path.
Using distinct discriminators rather than leaning on the error name
means the two arms cannot merge even when the thrown error is a plain
`Error`.

## What a reviewer should expect after deploy

Sentry applies grouping at ingest, so the existing T8 and ZR groups are
untouched. New transient-verdict events will land in a **new** group
rather than in T8. That new issue appearing is the fix working, not a
regression. ZR stays resolved and should remain so, since its own
fingerprint no longer receives traffic.

Tags and levels are unchanged on both paths, so existing
`step:pro-token-validate` queries keep working.

## Validation

Written test-first against the real code path, not a source-text pin.
The test activates a throwaway DSN before importing the module, stubs
`fetch`, and asserts on the delivered Sentry envelope.

- Failing first: both arms delivered no `fingerprint` at all (`actual:
undefined`). The message and `level: warning` assertions passed in the
same run, so the harness was provably exercising the real capture rather
than silently no-opping.
- After the fix: 3/3 green.
- Neighbouring suites: 137 pass across `mcp-pro-token-fingerprint`,
`mcp-auth-entitlement-and-limits`, `mcp-error-fingerprint` and
`pro-mcp-token`; a further 206 across `mcp`, `mcp-user-key-auth` and
`mcp-billing-denial`. The pre-existing WORLDMONITOR-ZR level test still
passes.
- Mutation-tested per guard: dropping either fingerprint reds two tests;
giving both arms the same discriminator reds one.
- Lint clean on both changed files.

That mutation pass found a real weakness in my own test. The
separability assertion used `notDeepEqual` alone, which is satisfied by
`undefined !== [...]`, so it passed vacuously when a fingerprint was
missing entirely. It now asserts both fingerprints are present before
comparing them, and only then does the mutation red it.

`npm run typecheck` reports one error in `src/services/webmcp.ts`. It is
pre-existing and unrelated — I confirmed it reproduces identically on a
clean tree with this change reverted.

## Related

- Sentry: WORLDMONITOR-ZR, WORLDMONITOR-T8
- Prior art for this pattern: #6409,
#6458, #7228
- Level downgrade this restores the intent of: #7601

---

[![Compound
Engineering](https://img.shields.io/badge/Built_with-Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
![Claude
Code](https://img.shields.io/badge/Opus_5_%281M%29-D97757?logo=claude&logoColor=white)

This branch was successfully deployed

1 active deployment
Preview — dbe77c46 Deployed Aug 11, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ops(sentry): Upstash abort timeouts fragment into a new issue per edge function per incident — 7 unresolved + 2 ignored for one handled condition

1 participant