fix(sentry): stamp a header-only stack on the createTimeoutSignal fallback reason so extension leaks stay suppressed
Problem
When a runtime lacks AbortSignal.timeout, createTimeoutSignal falls back to aborting with a JS-built new DOMException('signal timed out', 'TimeoutError') (src/services/timeout-signal.ts:40-44). That reason carries none of the native reason's header-only stack.
- Chromium-family engines leave a JS-built DOMException stackless (
stack === undefined). - Sentry's fetch instrumentation (
@sentry/core10.46instrumentFetch,build/esm/instrument/fetch.js:100-106) fills anundefinedstack on a rejected error with the fetch call site. It writes that onto the shared error object.
So if a browser extension that hooks window.fetch leaks an unhandled copy of a fallback timeout, the event carries first-party frames (the createTimeoutSignal caller's fetch). It then escapes the dashboard's !hasFirstParty /signal timed out/ suppression (src/bootstrap/sentry-init.ts) and reads as our own bug. On engines that do record a DOMException stack, the reason carries the timer callback's first-party frames instead, so the same misattribution happens.
This is the same failure mode fixed for insights-loader in #8296 (WORLDMONITOR-125/12Z/11N), documented in docs/solutions/logic-errors/sentry-stack-backfill-makes-a-stackless-abort-reason-look-first-party.md (#8297). Native AbortSignal.timeout reasons already have stack === "TimeoutError: signal timed out", parse to zero frames, and are suppressed correctly. Only the fallback path diverges.
Scope / impact: only runtimes without AbortSignal.timeout. No production event has been attributed to this path yet; this is consistency hardening, not an active incident.
Proposed fix
Stamp the native header-only stack on the fallback reason, as #8296 did in src/services/insights-loader.ts abortInFlightRequest:
let reason: DOMException | undefined;
if (typeof DOMException === 'function') {
reason = new DOMException('signal timed out', 'TimeoutError');
Object.defineProperty(reason, 'stack', {
value: 'TimeoutError: signal timed out',
configurable: true,
writable: true,
});
}
controller.abort(reason);
Keep name === 'TimeoutError'. analytics-collector-transport.ts branches on it to tell timeouts from cancellations.
Constraints
- Two bundles, byte-identical.
pro-test/src/services/timeout-signal.tsmust change identically.tests/marketing-mirror-parity.test.mtsenforces the match. - Marketing surface keeps timeouts visible.
pro-test/src/sentry-filter-policy.tsdeliberately has nosignal timed outsuppression. After the stamp, a fallback timeout there still reports; it just loses its frames, which matches native engines. Update that file's comment, which sayscreateTimeoutSignal"mints one on the pre-Baseline-2024 fallback path", to reflect the stamped shape. - Trade-off (dashboard). After the stamp, a first-party caller that leaks a fallback timeout unhandled and untagged becomes invisible, exactly as it already is on native engines. Callers that must surface a timeout have to catch it or report with a
kindtag. The checkout transport already reports with one.
Callers affected on the fallback path:
- Dashboard:
checkout-transport,market,supply-chain,scorecard,telegram-intel,passkey-offer-reservation,NqCatalystsPanel,NqPulsePanel. - Marketing:
checkout,checkout-transport,teasers,PricingSection,App.
Acceptance criteria
- Both
timeout-signal.tscopies stampstack === 'TimeoutError: signal timed out'on the fallback reason; the name and message are unchanged. - The regression test forces the fallback (stub out
AbortSignal.timeout) and swaps in a stacklessDOMExceptionsubclass. That recreates Chromium; Node's DOMException already has a stack, so without the swap the test passes vacuously. The pattern istests/insights-loader.test.mjs"aborts with a frameless stack". The test is red before the change and green after. -
tests/marketing-mirror-parity.test.mts,tests/pro-timeout-signal.test.mts,tests/checkout-transport.test.mtsandtests/sentry-beforesend.test.mjsall pass. - The stale comment in
pro-test/src/sentry-filter-policy.tsis updated. -
docs/solutions/logic-errors/sentry-stack-backfill-makes-a-stackless-abort-reason-look-first-party.mdand theCONCEPTS.md"Stack Backfill" entry drop the "fallback not yet stamped" caveat.
Related: #8296, #8297, #8299
Source: koala73/worldmonitor