Skip to content

fix(sentry): gate non-actionable client noise before it leaves the device (AGE-105) - #169

Merged
dzianisv merged 1 commit into
mainfrom
age-105-sentry-noise-gate
Aug 14, 2026
Merged

fix(sentry): gate non-actionable client noise before it leaves the device (AGE-105)#169
dzianisv merged 1 commit into
mainfrom
age-105-sentry-noise-gate

Conversation

@dzianisv

Copy link
Copy Markdown
Owner

Why

opencode-mobile is now the #1 source of Sentry error volume in the org — ~4,500 events/month against a 3,500/month org quota. Box-bot was silenced by the AGE-55 shim (50.7 -> 0.00 events/h); mobile is what's left.

~1,100 of those events are three non-defects:

462 events / 104 users   Error: connect timeout
498 events /   1 user    Error: API Error: 401 - ...
157 events /  33 users   Error: connect server-unreachable

The connect ... ones are captureDiagnostic() reports of the user's own LAN/VPN/self-hosted box being down. The 401 storm is one device's token-refresh retry loop: 498 copies of one problem, not 498 problems.

What

New pure module src/lib/sentry-noise.ts, wired into beforeSend. Three layers, cheapest first:

Layer Rule Rationale
Always-send allowlist OOM / ANR / native / IllegalStateException / NullPointerException / fatal level / unhandled mechanism Quota is worthless if it silences real crashes. These bypass everything below.
Transport drop-list connect timeout/server-unreachable/no-internet/malformed-url, Network request failed, Request timed out after, ECONN*/ETIMEDOUT... Hard drop, not sampled — the gate runs per-install, so even "1 per device per day" multiplies by the install base back into thousands/month.
Dedup + rate cap 6h per-fingerprint cooldown, <=6 new fingerprints/h, <=10 events/h Mirrors the openclaw-box-bot shim (AGE-55). Turns a retry loop into one report and caps the blast radius of any future regression.

connect health-failed and connect tls-error are deliberately not dropped — a box that answers but is unhealthy, or a broken cert, is actionable.

We don't lose the signal

Connection failures are already (a) shown to the user as connection UI and (b) counted, PII-free, in PostHog as connection_failed{error_class} via analytics-classify.ts. Sentry was charging us per event for a graph we already have. captureDiagnostic() now short-circuits for those classifications so the event isn't even built.

Drops stay auditable: the count since the last delivered event rides along as a noise.dropped_since_last tag on the next event that does go out.

Verification

  • 18 new unit tests in src/lib/sentry-noise.test.ts (pure, deterministic clock — no sleeps).
  • Includes a replay of the exact observed hour: 1,126 raw events -> 5 delivered (1 auth report + 4 real OOMs).
  • Explicit tests that OOM/ANR/fatal survive after every budget is exhausted.
  • npm test: 257 passing. npm run typecheck: clean.

Follow-up (not in this PR)

  • Ship in a release build, then re-measure over a clean 7d post-deploy window; target < ~1,500 events/month with genuine crash classes still arriving.
  • Investigate why one user produced 498 x 401 — the dedup hides the symptom; there may be a real token-refresh bug underneath.

Tracked in AGE-105.

…vice

opencode-mobile is now the org's #1 Sentry volume source (~4,500 events/mo
against a 3,500/mo org quota, AGE-105). ~1,100 of those events are three
non-defects: `connect timeout` (462), `connect server-unreachable` (157), and
one device's `API Error: 401` token-refresh loop firing 498 times.

Adds a pure, unit-tested noise gate (src/lib/sentry-noise.ts) wired into
`beforeSend`, applying three layers cheapest-first:

  1. Always-send allowlist — OOM/ANR/native/fatal crash classes bypass every
     limit. Quota is worthless if it silences real crashes.
  2. Transport drop-list — hard drop for client-side network conditions. Hard,
     not sampled: the gate runs per-install, so "1 per device per day" would
     multiply by the install base straight back into thousands per month.
  3. Dedup + rate cap — 6h per-fingerprint cooldown, ≤6 new fingerprints/h,
     ≤10 events/h, mirroring the openclaw-box-bot shim (AGE-55).

Nothing is lost by the transport drop: those failures are already user-visible
as connection UI and already trended, PII-free, as the PostHog
`connection_failed{error_class}` event. captureDiagnostic() also short-circuits
for those classifications so the event is never even built. Drops are auditable
— the count since the last delivered event rides along as a
`noise.dropped_since_last` tag.

Replaying the observed 1,126-event hour through the gate yields 5 delivered
events (1 auth report + 4 real OOMs).

Tests: 18 new, 257 total passing; tsc --noEmit clean.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@dzianisv
dzianisv merged commit 7c8bc7d into main Aug 14, 2026
8 checks passed
@dzianisv
dzianisv deleted the age-105-sentry-noise-gate branch August 14, 2026 13:35
dzianisv pushed a commit that referenced this pull request Aug 14, 2026
…users

Ships 7c8bc7d (#169). Until this release, the filter exists only in main: every
installed build still uploads `connect timeout` / `connect server-unreachable`
and un-deduped retry loops, which is what makes opencode-mobile the org's #1
Sentry volume source (~4,500 events/month against a 3,500/month org quota).

User-visible change is deliberately small — quieter crash reporting, real
crashes unaffected — so the Play changelog says exactly that.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
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.

1 participant