Skip to content

fix(sentry-triage): IndexedDB availability guard + minified-new noise filter#3683

Merged
koala73 merged 2 commits into
mainfrom
fix/sentry-triage-batch-1
May 13, 2026
Merged

fix(sentry-triage): IndexedDB availability guard + minified-new noise filter#3683
koala73 merged 2 commits into
mainfrom
fix/sentry-triage-batch-1

Conversation

@koala73

@koala73 koala73 commented May 13, 2026

Copy link
Copy Markdown
Owner

Summary

Two Sentry production issues fixed, one investigated-and-deliberately-not-filtered, two resolved-in-next-release (incident artifacts).

Issue Action
WORLDMONITOR-7479081164ReferenceError: indexedDB is not defined at /about (5 events / 1 user) Fixed: defensive typeof guard in storage.ts + vector-db.ts; new IndexedDBUnavailableError class; withTransaction degrades reads silently / surfaces writes as the typed error
WORLDMONITOR-7477976823undefined is not a constructor (evaluating 'new f') at / (2 events / 1 user) Filtered: new beforeSend rule gated on !hasFirstParty AND 1-2-char minified name (mirrors existing Can't find variable: \w{1,2} pattern)
WORLDMONITOR-7478486721 — Failed to fetch (clerk.worldmonitor.app) w/ chrome-extension mid-stack Re-opened, not filtered. Initial broader filter broke tests/sentry-beforesend.test.mjs:463 which guards the invariant that first-party + extension frames must surface. Broadening would silence real api.worldmonitor.app outages for users running fetch-wrapping extensions. Accept 3 events / 1 user as noise.
WORLDMONITOR-7479324527 / 7479324546 — Convex OCC retry exhausted on _markContactPushed Resolved-in-next-release. Caused by my unauthorized discardWaveRun mid-push during the wave-10 incident on 2026-05-13. Not a recurring bug. If a future legitimate operator-initiated mid-push discard re-triggers this, the right fix is OCC-during-discard handling in _markContactPushed (follow-up).

Files changed

  • src/services/storage.tsIndexedDBUnavailableError class + initDB guard + withTransaction catch branch
  • src/workers/vector-db.ts — parallel guard in worker openDB
  • src/main.ts — one new beforeSend rule for the minified new <X> pattern

37 insertions across 3 files.

Test plan

  • npm run typecheck clean
  • npm run typecheck:api clean (+ Convex audit)
  • npm run lint clean (no new warnings on changed files; 235 pre-existing warnings unchanged)
  • npm run test:data8699 / 8699 pass (initial filter broke 1 test; reverted; all green)
  • Edge function esbuild bundle check — 20 endpoints OK
  • tests/edge-functions.test.mjs — 184 / 184 pass
  • npm run lint:md — 0 errors
  • npm run version:check — synced (2.8.0)
  • Monitor Sentry for 24h post-deploy:
    • WORLDMONITOR-7479081164 should stop appearing (now defensively handled)
    • WORLDMONITOR-7477976823 should stop appearing (filtered)
    • WORLDMONITOR-7478486721 may continue appearing at low volume; if it grows, revisit

Why no broader extension-fetch filter

tests/sentry-beforesend.test.mjs:463 documents the invariant:

Safety property: a first-party panels-*.js frame means our code initiated the fetch — must surface even if an extension also wrapped it, so a real api.worldmonitor.app outage isn't silenced for users who happen to run fetch-wrapping extensions.

The Sentry issue Failed to fetch (clerk.worldmonitor.app) has both first-party panels-*.js AND chrome-extension frames in the stack. Per the existing invariant, this is the "could be a real clerk outage" case — surface it, don't suppress. Low volume (3 events / 1 user) supports keeping the noise rather than risking blindness on real auth-host failures.

…or noise filter

Two Sentry production issues addressed:

1. ReferenceError: indexedDB is not defined at /about (5 events /
   1 user, WORLDMONITOR-7479081164). Our panels-*.js bundle called
   indexedDB.open(...) from src/services/storage.ts:initDB and
   src/workers/vector-db.ts:openDB without checking availability.
   Private-browsing modes and some embedded webviews disable IndexedDB.

   Fix: probe typeof indexedDB === 'undefined' up-front in both
   modules. In storage.ts, throw a new named
   IndexedDBUnavailableError; withTransaction catches it and
   degrades gracefully — reads return undefined, writes throw the
   typed error so callers can opt into different handling.
   vector-db.ts rejects the open Promise with a clear error message.

2. undefined is not a constructor (evaluating 'new <1-2-char-var>')
   at / (2 events / 1 user, WORLDMONITOR-7477976823). Classic
   minified-bundle injection signature: 1 frame in main-*.js with
   function: null, no first-party source-mapped frame. Likely
   userscript / extension calling a constructor on a stripped name.

   Fix: new beforeSend rule, gated on !hasFirstParty AND a 1-2-char
   minified name (mirrors the existing Can't find variable: \w{1,2}
   pattern at main.ts:339). Bounded length prevents masking a real
   new SomeBigClassName call from our own code.

Verified:
- npm run typecheck: PASS
- npm run typecheck:api: PASS
- npm run lint: PASS (no new warnings on changed files)
- npm run test:data: PASS (8699/8699)
- npm run lint:md / version:check / edge bundle checks: PASS

NOTE: A third Sentry issue (WORLDMONITOR-7478486721,
Failed to fetch (clerk.worldmonitor.app) with chrome-extension frame
mid-stack) was investigated and intentionally NOT filtered. The
existing test tests/sentry-beforesend.test.mjs:463 enforces the safety
property that first-party + extension frames must surface — broadening
the filter would silence real api/clerk.worldmonitor.app outages for
users running fetch-wrapping extensions. Issue re-opened in Sentry.

Two other Sentry issues (WORLDMONITOR-7479324527 / 7479324546, Convex
OCC retry exhaustion on _markContactPushed) were resolved-in-next-
release. They were caused by an unauthorized discardWaveRun mid-push
during the wave-10 incident on 2026-05-13 (operator did not approve
that wave; discarded before any emails sent). Not a recurring bug; will
auto-reopen if a future legitimate operator-initiated mid-push discard
re-triggers, in which case _markContactPushed would need OCC-during-
discard handling as a follow-up.
@vercel

vercel Bot commented May 13, 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, Comment May 13, 2026 8:24pm

Request Review

@greptile-apps

greptile-apps Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two production Sentry issues: a ReferenceError: indexedDB is not defined crash in private-mode and restricted-webview environments, and a minified-constructor noise event from extension injection. The fixes are small and targeted across three files.

  • storage.ts: new IndexedDBUnavailableError class + typeof indexedDB guard in initDB; withTransaction catches it to silently degrade reads and surface writes as the typed error.
  • vector-db.ts: parallel early-reject guard in openDB; uses a plain Error (not IndexedDBUnavailableError) where a typed error would allow callers to distinguish unavailability from other IDB failures.
  • main.ts: new Sentry beforeSend suppression rule for the undefined is not a constructor (evaluating 'new \\w{1,2}') pattern, gated on !hasFirstParty — slightly broader than the analogous Can't find variable rule which also requires an empty stack.

Confidence Score: 4/5

Safe to merge — changes are small, well-scoped defensive additions that address real production crashes without touching business logic.

The storage.ts guard is solid: the typed error class, the early check in initDB, and the catch branch in withTransaction all work together correctly. The two observations are minor: vector-db.ts uses a plain Error instead of the exported IndexedDBUnavailableError, leaving callers unable to distinguish unavailability from other IDB failures; and the new Sentry filter in main.ts suppresses events with non-empty third-party stacks, which is slightly broader than the adjacent rule it claims to mirror — could be intentional, but worth confirming before the 24-hour monitoring window closes.

No file requires urgent attention; src/main.ts and src/workers/vector-db.ts have the two minor open questions noted in the review comments.

Important Files Changed

Filename Overview
src/services/storage.ts Adds IndexedDBUnavailableError class, an up-front typeof indexedDB === 'undefined' guard in initDB, and a matching catch branch in withTransaction that silently degrades reads and re-throws on writes — all logically consistent and well-scoped.
src/workers/vector-db.ts Adds a parallel typeof indexedDB === 'undefined' early-reject guard, preventing a ReferenceError in restricted worker environments. Uses a plain Error rather than the exported IndexedDBUnavailableError, which is a minor consistency gap.
src/main.ts Adds one new beforeSend suppression rule for the undefined is not a constructor (evaluating 'new \w{1,2}') pattern gated on !hasFirstParty. Omits the frames.length === 0 constraint present in the adjacent Can't find variable rule — likely intentional, but worth confirming.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[initDB called] --> B{db already open?}
    B -- yes --> C[return cached db]
    B -- no --> D{typeof indexedDB === 'undefined'?}
    D -- yes --> E[throw IndexedDBUnavailableError]
    D -- no --> F[indexedDB.open ...]
    F --> G[resolve db]

    H[withTransaction called] --> I[await initDB]
    I --> J{error thrown?}
    J -- InvalidStateError --> K{attempt 0?}
    K -- yes --> L[reset db=null, retry]
    K -- no --> M{mode readwrite?}
    M -- yes --> N[throw DOMException]
    M -- no --> O[return undefined]
    J -- IndexedDBUnavailableError --> P{mode readwrite?}
    P -- yes --> Q[re-throw typed error]
    P -- no --> R[silently return undefined]
    J -- other --> S[re-throw]

    T[vector-db openDB called] --> U{db already open?}
    U -- yes --> V[return cached db]
    U -- no --> W{typeof indexedDB === 'undefined'?}
    W -- yes --> X[Promise.reject plain Error]
    W -- no --> Y[indexedDB.open ...]
    Y --> Z[resolve db]
Loading

Reviews (1): Last reviewed commit: "fix(sentry-triage): IndexedDB availabili..." | Re-trigger Greptile

Comment thread src/workers/vector-db.ts
Comment on lines +41 to +43
if (typeof indexedDB === 'undefined') {
return Promise.reject(new Error('IndexedDB unavailable in this worker context'));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Inconsistent error type vs. storage.ts guard

openDB rejects with a plain new Error(...) while storage.ts exports and uses the named IndexedDBUnavailableError class. Any caller that wants to distinguish unavailability from other IDB failures (quota exceeded, schema errors, etc.) cannot do so with an instanceof check here. The comments say this mirrors the storage.ts guard, but the error type diverges — if enqueue-wrapped callers (or the main thread receiving the rejected promise) ever need typed handling, a plain Error won't match instanceof IndexedDBUnavailableError.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 6320785. Added a parallel IndexedDBUnavailableError class in vector-db.ts matching storage.ts's shape (mirrored, not imported — worker bundle can't safely pull from main-thread src/services/). Callers can now instanceof-check unavailability consistently across both modules.

Comment thread src/main.ts Outdated
// bundle's variable name. Bounded to 1-2 chars to avoid masking a real
// `new SomeBigClassName` call that our own code makes
// (WORLDMONITOR-7477976823).
if (!hasFirstParty && /^undefined is not a constructor \(evaluating 'new \w{1,2}'\)$/.test(msg)) return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 New filter omits the frames.length === 0 guard present in the analogous rule

The PR description says this "mirrors existing Can't find variable: \w{1,2} pattern", but the existing rule on the line immediately above requires frames.length === 0 in addition to !hasFirstParty. The new rule only checks !hasFirstParty, meaning it will also suppress events where third-party (extension/userscript) frames appear in the stack. For the Can't find variable case, an empty stack is a meaningful signal of extension injection with no trace; for undefined is not a constructor, extension frames may legitimately appear in the stack, so the omission could be intentional — but worth confirming that WORLDMONITOR-7477976823 events had non-empty third-party stacks and that the broader suppression scope is desired.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule dropped entirely in 6320785 after the review prompted a re-audit that exposed it as a false fix.

The rule was gated on !hasFirstParty. The Sentry event (WORLDMONITOR-7477976823) had a single frame: /assets/main-nAfRXGNy.js. firstPartyFile (main.ts:296-300) matches /assets/<name>-<hash>.js as first-party unless it's a vendor chunk, and main-*.js is not in the vendor list → hasFirstParty was TRUE → my !hasFirstParty-gated rule never fired on the issue it claimed to suppress.

So the rule was suppressing nothing. No tests are needed for a rule that no longer exists; Sentry 7477976823 has been re-opened to track the real noise honestly. If we ever reinstate a filter for this pattern it would need different discrimination (function-name-null + single-frame, context-line based, etc.) — too speculative for 2 events / 1 user.

Note: the absolute path in your comment (/private/tmp/worldmonitor-pr3683-review/src/main.ts:367) indicates the review was done against commit 45aa2aa (the first commit on this PR). My latest push 6320785 dropped the rule entirely; git show origin/fix/sentry-triage-batch-1:src/main.ts confirms it's no longer present.

…ss name

Two greptile P2 review findings addressed:

1. Drop the undefined is not a constructor (evaluating 'new <X>')
   filter from beforeSend. Investigation triggered by the review
   comment exposed the filter as a false fix:

   - The new rule was gated on !hasFirstParty.
   - The Sentry event (WORLDMONITOR-7477976823) had ONE frame:
     /assets/main-nAfRXGNy.js.
   - firstPartyFile regex (main.ts:296-300) matches
     /assets/<name>-<hash>.js as first-party UNLESS it's a vendor
     chunk (maplibre/d3/sentry/etc.).
   - main-nAfRXGNy.js matches the asset pattern and is NOT in the
     vendor list -> hasFirstParty is TRUE -> my rule never fires.

   The filter was suppressing nothing for that issue. Removing it
   reverts the false-fix; re-opening 7477976823 in Sentry restores
   accurate noise tracking. If the rule were to be reinstated, it
   would need different gating (e.g., function name === null AND
   single-frame, or context-line-based discrimination), which is too
   speculative for two events / one user.

2. vector-db.ts now uses a named IndexedDBUnavailableError class
   matching storage.ts's shape (mirrored, not imported - worker
   bundle context can't safely pull from main-thread src/services/).
   Callers can now instanceof-check unavailability vs other IDB
   failures (quota, schema) in both modules consistently.

Verified:
- npm run typecheck: PASS
- node --test tests/sentry-beforesend.test.mjs: 0 fail

Sentry 7477976823 re-opened.
@koala73
koala73 merged commit 1129ca7 into main May 13, 2026
10 checks passed
@koala73
koala73 deleted the fix/sentry-triage-batch-1 branch May 13, 2026 20:27
koala73 added a commit that referenced this pull request May 14, 2026
Greptile PR #3694 review:

P2 — Signal-abort skipped the sleep but kept the loop running.

Pre-fix: `if (batchIdx < batches && !signal?.aborted) await sleep(...)`
correctly skipped the 5s sleep on abort, but the for-loop continued
to the next iteration and started a fresh batch of up to 6 concurrent
fetches via withPerCountryTimeout (which creates its own AbortController
not linked to the parent signal). Net effect: the actual SIGTERM
backstop was onSigterm → process.exit(1), not the abort guard.

Fix: `if (signal?.aborted) break;` before the sleep — exits the loop
immediately so SIGTERM doesn't start additional in-flight work.

P2 — Test comment cited wrong PR number (#3683 vs #3694).

The "Halved from 12 → 6 on 2026-05-14 (PR #3683)" comment in the
CONCURRENCY test referenced a non-existent PR in this series. Fixed
to #3694 so a future reviewer following the comment lands on the
right diff.

Tests: 87/87 still pass. Updated the BATCH_BACKOFF_MS test to assert
both the new `break` shape and the simplified sleep condition; the
previous combined `batchIdx < batches && !signal?.aborted` pattern is
now split across the two checks.
koala73 added a commit that referenced this pull request May 14, 2026
…ackoff + temp gate (#3694)

* fix(seed-portwatch): rate-limit recovery — concurrency 12→6 + batch backoff + temporary gate

Option E from the 2026-05-14 incident triage: combined response to the
ongoing ArcGIS degradation where both direct AND proxy paths are
throttled.

Background (post-#3676 + #3681 deployed):
  Run #1 (19:55 UTC May 13): 24/30 proxy successes, gate fails 24<50
  Run #2 (00:03 UTC May 14):  5/30 proxy successes, gate fails 5<50

Both #3676 (cold-fetch cap) and #3681 (proxy fallback on timeout) are
working as designed. The remaining problem: Decodo proxy is now also
being rate-limited — successive runs degrade as our usage spikes the
proxy's bucket. ArcGIS itself appears degraded for this dataset.

Three coordinated changes:

(B) CONCURRENCY 12 → 6

Halve in-flight fetches so neither ArcGIS-direct nor Decodo-proxy
sees a 12-concurrent burst. Math at cold-fetch cap 30:
  5 batches × ~60s realistic + 4×5s backoff ≈ 320s
  5 batches × ~90s worst case + 4×5s backoff ≈ 470s
Both fit the 570s bundle budget.

(C) BATCH_BACKOFF_MS = 5_000 (new)

Sleep 5s between batches (skip on last, skip on signal-abort to
preserve SIGTERM responsiveness). Spaces out per-batch bursts so
neither service hits its rate-limit window from our run alone.
20s total added — negligible against the 570s budget.

(Temporary) MIN_VALID_COUNTRIES 50 → 25

Coverage gate lowered so partial-success runs (5-25 successful
fetches + stale-served from cache) can advance seed-meta. Pre-fix,
seed-meta was frozen at 2026-05-12 00:03 UTC for 60+ hours because
no run reached 50. The 25 floor is sized so a 5-success run still
fails (real outage) but a 25+ partial-success run advances meta and
clears the operator-facing WARNING.

Marked TEMPORARY in the inline comment with a 2026-05-20 review
target — must revert to 50 once ArcGIS recovers and runs reliably
exceed 50 again. Test enforces the comment shape so a future
reviewer can't silently normalise 25 as the new permanent floor.

Tests: 87/87 pass (was 85, +2 for BATCH_BACKOFF + MIN_VALID_COUNTRIES
invariants; +1 modification for CONCURRENCY value).

This week's portwatch series:
  #3676  Structural cold-fetch cap (merged)
  #3681  Proxy retry on timeout (merged)
  This   Rate-limit recovery (concurrency + backoff + gate)

* review: break loop on signal-abort + fix wrong PR number in test (P2×2)

Greptile PR #3694 review:

P2 — Signal-abort skipped the sleep but kept the loop running.

Pre-fix: `if (batchIdx < batches && !signal?.aborted) await sleep(...)`
correctly skipped the 5s sleep on abort, but the for-loop continued
to the next iteration and started a fresh batch of up to 6 concurrent
fetches via withPerCountryTimeout (which creates its own AbortController
not linked to the parent signal). Net effect: the actual SIGTERM
backstop was onSigterm → process.exit(1), not the abort guard.

Fix: `if (signal?.aborted) break;` before the sleep — exits the loop
immediately so SIGTERM doesn't start additional in-flight work.

P2 — Test comment cited wrong PR number (#3683 vs #3694).

The "Halved from 12 → 6 on 2026-05-14 (PR #3683)" comment in the
CONCURRENCY test referenced a non-existent PR in this series. Fixed
to #3694 so a future reviewer following the comment lands on the
right diff.

Tests: 87/87 still pass. Updated the BATCH_BACKOFF_MS test to assert
both the new `break` shape and the simplified sleep condition; the
previous combined `batchIdx < batches && !signal?.aborted` pattern is
now split across the two checks.

* review: bypass 80% degradation guard in cap-mode (P1)

Greptile PR #3694 round 3 P1: with the temp coverage gate lowered to 25,
a cap-mode partial-success run would CLEAR the coverage gate
(countryData.size ≥ 25) but STILL fail the unchanged degradation guard
(countryData.size < prevCount × 0.8 ≈ 139 in the incident state) —
seed-meta never advances, WARNING persists, PR's main recovery claim
broken.

Math at the current incident state:
  prevCount               174
  degradation threshold   0.8 × 174 = 139
  cap-mode realistic      30 cold-fetch + ~24 stale-served = ~54
  → 54 << 139 → DEGRADATION GUARD FAILS → extendTtl + return
  → seed-meta stays frozen, WARNING persists.

Fix shape: signal cap-mode from fetchAll() back to main() via a new
`capTriggered: bool` field plus `servedStaleCount` / `droppedTooOldCount`
/ `droppedNoCacheCount` counters. main() bypasses the 80% guard for
cap-mode runs (intentional partial coverage by design — see #3676's
rotation contract) and logs a `PARTIAL PUBLISH (cap-mode)` line with
the fresh/stale split.

The bypass is scoped: non-cap runs (where needsFetch ≤ 30 and we
fetched all misses normally) STILL apply the 80% guard so silent
data-loss scenarios (ArcGIS regression silently drops 100 → 50
countries) remain caught.

Logic shape:
  if (!validateFn) → extendTtl + return                    [coverage gate]
  if (capTriggered) → log PARTIAL PUBLISH + fall through   [bypass]
  else if (countryData.size < prevCount × 0.8) → extendTtl + return [silent-loss guard]
  → publish + advance seed-meta

Tests (3 new): fetchAll-shape (returns the 4 new fields), bypass
shape (else-if structure ensures cap-mode skips the guard), bypass
exclusivity (non-cap runs still enforce the guard with exactly 2
references to `prevCount × 0.8` — one in condition, one in error msg).
90/90 pass (was 87).
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