Skip to content

test: add regression tests proving concurrent trace() calls corrupt Sentry async-context state - #45249

Open
MajorLift wants to merge 3 commits into
mainfrom
jongsun/test/sentry-concurrency-repro-7523
Open

test: add regression tests proving concurrent trace() calls corrupt Sentry async-context state#45249
MajorLift wants to merge 3 commits into
mainfrom
jongsun/test/sentry-concurrency-repro-7523

Conversation

@MajorLift

@MajorLift MajorLift commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds regression tests that prove — via forced deterministic interleaving against the real @sentry/browser SDK, not mocks — the concurrency defect tracked in MetaMask-planning#7523: concurrent trace() calls in the service worker can corrupt each other's Sentry async-context state.

No fix is included. This PR's purpose is to make the confirmed-but-unfixed bug visible and provable in the codebase (a red assertion documented and gated, not a silent gap), per MetaMask-planning#7523's acceptance criteria:

  • AC1 — Reproduce (or rule out) isolation-scope cross-contamination between two concurrently-instrumented trace() calls. Reproduced.
  • AC2 — Reproduce (or rule out) getCurrentTraceId() correlating a consensys-request-id with the wrong concurrently-running operation's trace id. Reproduced, for the realistic concurrent-RPC scenario.

The claim being tested

shared/lib/trace.ts's startSpan() wraps every span in sentryWithIsolationScope(). @sentry/browser registers no alternative async-context strategy for browser/service-worker environments (no AsyncLocalStorage/Zone equivalent — only @sentry/server-utils and @sentry/opentelemetry register one), so it falls back to a stack-based implementation: AsyncContextStack (node_modules/@sentry/core/.../asyncContext/stackStrategy.js), a single, shared, mutable array for the whole JS realm's lifetime.

When two trace() calls overlap — ordinary event-loop behavior for concurrent RPC handlers, wrapMessengerWithTracing-wrapped calls, or websocket notification handling in the service worker — a later call's layer can still be on top of that shared stack when an earlier, still-pending call's own continuation resumes and reads "the current active span." The read is misattributed to the wrong logical operation. This isn't a rare edge case; it's what the shared stack does on every overlap by construction.

Mechanism correction from the original ticket

The ticket's original write-up described this as isolation-scope cross-contamination ("B's layer pushes on top of A's isolation scope"). Reading the actual SDK source (@sentry/core v10.38.0, this repo's installed version) turned up a more specific mechanism:

  • The isolation scope itself is never forked or pushed per call — it's a single unforked singleton (AsyncContextStack._isolationScope) for the whole realm's lifetime. The SDK's own docstring confirms this: "If no async context strategy is set, the isolation scope and the current scope will not be forked (this is currently the case, for example, in the browser)."
  • What actually gets pushed/popped per call is the current-scope stack (_stack). Sentry.startSpan() pushes a second layer onto that same shared array, and shared/lib/trace.ts:538-544's active-span-inheritance code (sentryGetActiveSpan(), used when no explicit parentContext is given) reads whatever's on top of it at call time.

Mutation-check implication for any future fix

Disabling just trace.ts's active-span-inheritance shortcut (lines 538-544) does not fix the defect — confirmed empirically in this PR's own harness. Sentry.startSpan()'s own current-scope cloning independently inherits the ambient trace id from whatever is on top of the shared stack, regardless of that one shortcut. The same holds for getCurrentTraceId()'s getActiveSpan() branch: disabling it doesn't flip the correlation defect, because the fallback path (getCurrentScope().getPropagationContext()) reads from the exact same corrupted shared stack-top and independently carries the same wrong trace id.

Any real fix needs to address the shared, unforked current-scope stack itself — not just patch one of the two call sites that happen to read it. Candidate directions are listed in MetaMask-planning#7523; this PR takes no position on which one.

What's in this PR

  • shared/lib/trace.test.ts — new describe('concurrent trace() calls (MetaMask-planning#7523)', ...) block. Exercises the real @sentry/browser SDK (a real BrowserClient, globalThis.sentry wired to the real SDK) instead of this file's default mock, because the defect lives inside the SDK's real AsyncContextStack.
  • app/scripts/lib/sentry-trace-propagation.concurrency.test.ts — new file, not a new block in the existing sentry-trace-propagation.test.ts. That file does a file-wide jest.mock('@sentry/browser') / jest.mock('@sentry/core'), which is incompatible with exercising the real SDK end-to-end (the module under test binds its getActiveSpan/getCurrentScope/getIsolationScope imports at its own first load, so partially un-mocking within that file wouldn't change what the module itself sees).

Both files force the interleaving deterministically with hand-controlled deferred Promises and microtask stepping — no timing-dependent flakiness, no real timers.

Why the bug-revealing tests are it.skip, not left red

There is no fix yet, so the assertions that state the correct (post-fix) behavior currently fail. Rather than delete them or leave the suite red with no explanation, three tests are it.skip, each with a comment stating: what's currently wrong, exactly what removing .skip shows today, and a pointer to MetaMask-planning#7523.

it.failing (Jest 29's built-in "expected failure" mechanism, which would let these run and self-document without a human needing to remove .skip) was tried first and reverted: it type-checks fine in isolation, but this repo also has @types/mocha installed, which declares a conflicting global it with no .failing member — yarn lint:tsc resolves the wrong merged type project-wide and fails with Property 'failing' does not exist on type 'TestFunction'. it.skip is what this repo's actual toolchain supports for this. shared/lib/trace.test.ts's two skips carry an explicit eslint-disable-next-line jest/no-disabled-tests (that rule is error-level for shared/**/*.test.ts); sentry-trace-propagation.concurrency.test.ts's skip needs no such comment because .eslintrc.js's jest-rules file list doesn't currently cover app/scripts/lib/**/*.test.ts at all (a separate, pre-existing gap called out in that file's own TODO, not something this PR relies on or should be read as endorsing).

Each skipped test is paired with a sequential/discriminating control (a structurally identical test with the interleaving removed) that currently passes — proving the harness discriminates real overlap from no-overlap, and isn't just failing for an unrelated reason. All three skipped tests were verified, by temporarily removing .skip, to fail for the intended reason (not a vacuous or incidental failure) before being re-skipped for this PR.

Related

  • MetaMask-planning#7523 (this PR closes neither AC3 nor AC4 — no fix decision has been made yet)
  • Parent epic MetaMask-planning#7354 (a separate, already-tracked SDK behavior — root spans with no explicit parent share one ambient/baseline trace id for the life of the JS realm — came up while designing these tests and is called out in code comments to avoid conflating it with the concurrency-specific defect this PR proves)

Test plan

  • yarn jest shared/lib/trace.test.ts app/scripts/lib/sentry-trace-propagation.concurrency.test.ts app/scripts/lib/sentry-trace-propagation.test.ts — all green (3 skipped, 43 passed)
  • Each skipped test verified to hit the real, intended failure (not a vacuous pass) by temporarily removing .skip and inspecting the actual thrown error, then re-skipped
  • yarn lint:eslint clean on both changed files
  • yarn lint:tsc run project-wide: zero errors in either of this PR's two files. (The full run exits non-zero on pre-existing, unrelated errors elsewhere in the tree — e.g. ui/hooks/ramps/utils/mapRampsOrderSafely.ts, app/scripts/messenger-client-init/** — present on main before this PR; confirmed by file path, not introduced here.)
  • yarn lint:format (oxfmt) clean on both changed files

CHANGELOG entry: null


Note

Low Risk
Changes are limited to new and extended test files with no production or runtime behavior changes.

Overview
Adds test-only regression coverage for MetaMask-planning#7523: overlapping trace() calls against the real @sentry/browser SDK can corrupt shared async-context state in the service worker (no fix in this PR).

shared/lib/trace.test.ts gains a concurrent trace() calls suite that uses a real BrowserClient instead of the file’s mocked globalThis.sentry. It asserts span parenting stays independent when operations overlap; two it.skip cases document the current wrong behavior (child spans parented under a still-pending unrelated trace, including a third concurrent call). A sequential case passes as a baseline.

app/scripts/lib/sentry-trace-propagation.concurrency.test.ts is a new file (separate from the fully mocked sentry-trace-propagation.test.ts) that wires consensysTracePropagationIntegration to real fetch instrumentation and checks consensys-request-id / baggage correlation under forced interleaving with distinct distributed trace ids. One skipped test encodes the bug (A’s fetch correlated with B’s trace); a control test passes when B finishes before A resumes.

Skipped tests are intentional so CI stays green until a fix lands; each skip is paired with a passing control that isolates overlap as the failure driver.

Reviewed by Cursor Bugbot for commit a11a6b8. Bugbot is set up for automated code reviews on this repo. Configure here.

`shared/lib/trace.ts`'s `startSpan()` wraps every span in
`sentryWithIsolationScope()`, which relies on `@sentry/browser`'s
stack-based async-context strategy -- a single, shared, mutable stack per
JS realm, since no `AsyncLocalStorage`/Zone equivalent exists in a
browser/service-worker environment. These tests exercise the real SDK
end-to-end (a real `BrowserClient`, not the file's default mocked
`globalThis.sentry`) to force two `trace()` calls to interleave
deterministically, and show that a concurrently-pending, logically
unrelated call gets silently parented under -- and adopts the trace id of
-- whichever operation's layer is currently on top of the shared stack.

`sentry-trace-propagation.concurrency.test.ts` covers the same root cause's
second symptom: `getCurrentTraceId()` correlating an outbound
`consensys-request-id` with the wrong concurrently-running operation's
trace id. It's a separate file rather than a new block in
`sentry-trace-propagation.test.ts` because that file's file-wide
`jest.mock('@sentry/browser')` / `jest.mock('@sentry/core')` is
incompatible with exercising the real SDK the defect actually lives in.

No fix exists yet, so the corruption-revealing assertions are marked
`it.failing` -- CI stays green, and flipping them back to `it` once a fix
lands will fail loudly if the fix is incomplete. See
MetaMask-planning#7523.
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@metamask-ci metamask-ci Bot added the team-extension-platform Extension Platform team label Aug 5, 2026
@metamask-ci

metamask-ci Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
Builds ready [62dfa62] [reused from 4a98074]
⚡ Performance Benchmarks (Total: 🟢 10 pass · 🟡 7 warn · 🔴 4 fail)

Baseline (latest main): 171ed20 | Date: 7/28/2026 | Pipeline: 31034059228 | Baseline logs

Metricschrome-webpackfirefox-webpack
onboardingImportWallet
[Sentry log · main/release]
🔴 srpButtonToSrpForm(p95) [CI log]🔴 [CI log]
onboardingNewWallet
[Sentry log · main/release]
🔴 longTaskTotalDuration(p95) [CI log]🔴 [CI log]

Regressions (🔴 4 failures)

Interaction Benchmarks · Samples: 5
Benchmarkchrome-webpackfirefox-webpack
loadNewAccount
[Sentry log · main/release]
🟢 [CI log]🟡 [CI log]
🟡 load_new_account
confirmTx
[Sentry log · main/release]
🟢 [CI log]🟡 [CI log]
bridgeUserActions
[Sentry log · main/release]
🟢 [CI log]🟢 [CI log]

📈 Results compared to the previous 5 runs on main

  • loadNewAccount/fcp: -12%
  • confirmTx/longTaskTotalDuration: -23%
  • confirmTx/longTaskMaxDuration: -24%
  • confirmTx/tbt: -41%
  • confirmTx/fcp: -16%
  • confirmTx/lcp: +743%
  • bridgeUserActions/bridge_load_page: -11%
  • bridgeUserActions/bridge_load_asset_picker: -26%
  • bridgeUserActions/longTaskCount: -44%
  • bridgeUserActions/longTaskTotalDuration: -48%
  • bridgeUserActions/longTaskMaxDuration: -20%
  • bridgeUserActions/tbt: -55%
  • bridgeUserActions/total: -12%
  • bridgeUserActions/inp: -15%
  • bridgeUserActions/fcp: -16%
  • bridgeUserActions/lcp: -13%
  • loadNewAccount/load_new_account: +21%
  • loadNewAccount/total: +21%
  • loadNewAccount/inp: +165%
  • loadNewAccount/lcp: +1208%
  • confirmTx/confirm_tx: +16%
  • confirmTx/longTaskCount: -100%
  • confirmTx/longTaskTotalDuration: -100%
  • confirmTx/longTaskMaxDuration: -100%
  • confirmTx/tbt: -100%
  • confirmTx/total: +16%
  • confirmTx/inp: -24%
  • confirmTx/lcp: +1174%
  • bridgeUserActions/bridge_load_page: +340%
  • bridgeUserActions/bridge_load_asset_picker: +53%
  • bridgeUserActions/longTaskCount: -100%
  • bridgeUserActions/longTaskTotalDuration: -100%
  • bridgeUserActions/longTaskMaxDuration: -100%
  • bridgeUserActions/tbt: -100%
  • bridgeUserActions/total: +37%
  • bridgeUserActions/inp: -23%
  • bridgeUserActions/fcp: -46%
  • bridgeUserActions/lcp: +1206%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 loadNewAccount/INP: p75 280ms
  • 🟡 loadNewAccount/FCP: p75 1.9s
  • 🟡 confirmTx/FCP: p75 1.8s
Startup Benchmarks · Samples: 100
Benchmarkchrome-webpackfirefox-webpack
startupStandardHome
[Sentry log · main/release]
🟢 [CI log]🟢 [CI log]
startupPowerUserHome
[Sentry log · main/release]
🟡 [CI log]

📈 Results compared to the previous 5 runs on main

  • startupStandardHome/numNetworkReqs: -14%
  • startupStandardHome/domInteractive: -24%
  • startupStandardHome/numNetworkReqs: -13%
  • startupStandardHome/fcp: -19%
  • startupPowerUserHome/uiStartup: +23%
  • startupPowerUserHome/load: +24%
  • startupPowerUserHome/domContentLoaded: +24%
  • startupPowerUserHome/domInteractive: +17%
  • startupPowerUserHome/backgroundConnect: +65%
  • startupPowerUserHome/firstReactRender: +20%
  • startupPowerUserHome/initialActions: +11%
  • startupPowerUserHome/loadScripts: +23%
  • startupPowerUserHome/setupStore: +295%
  • startupPowerUserHome/inp: +10%
  • startupPowerUserHome/fcp: +15%
  • startupPowerUserHome/lcp: +19%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 startupPowerUserHome/INP: p75 208ms
  • 🟡 startupPowerUserHome/LCP: p75 3.4s
User Journey Benchmarks · Samples: 5 · mock API 🔴 4

⚠️ Missing data: chrome/webpack/userJourneyTransactions

Benchmarkchrome-webpackfirefox-webpack
onboardingImportWallet
[Sentry log · main/release]
🔴 [CI log]
🔴 doneButtonToHomeScreen
🔴 total
🔴 [CI log]
🔴 total
onboardingNewWallet
[Sentry log · main/release]
🔴 [CI log]
🔴 total
🔴 [CI log]
🔴 total
assetDetails
[Sentry log · main/release]
🟢 [CI log]🟢 [CI log]
solanaAssetDetails
[Sentry log · main/release]
🟡 [CI log]🟡 [CI log]
importSrpHome
[Sentry log · main/release]
🟡 [CI log]🟢 [CI log]
sendTransactions
[Sentry log · main/release]
🟡 [CI log]
swap
[Sentry log · main/release]
🟢 [CI log]

📈 Results compared to the previous 5 runs on main

  • onboardingImportWallet/doneButtonToHomeScreen: -36%
  • onboardingImportWallet/openAccountMenuToAccountListLoaded: +139%
  • onboardingImportWallet/longTaskCount: +57%
  • onboardingImportWallet/longTaskTotalDuration: +20%
  • onboardingNewWallet/skipBackupToMetricsScreen: -18%
  • onboardingNewWallet/agreeButtonToOnboardingSuccess: -11%
  • onboardingNewWallet/doneButtonToAssetList: -11%
  • onboardingNewWallet/tbt: +34%
  • onboardingNewWallet/total: -11%
  • solanaAssetDetails/assetClickToPriceChart: +28%
  • solanaAssetDetails/longTaskCount: -100%
  • solanaAssetDetails/longTaskTotalDuration: -100%
  • solanaAssetDetails/longTaskMaxDuration: -100%
  • solanaAssetDetails/tbt: -100%
  • solanaAssetDetails/total: +28%
  • solanaAssetDetails/inp: +11%
  • solanaAssetDetails/fcp: +18%
  • solanaAssetDetails/lcp: +14%
  • importSrpHome/loginToHomeScreen: +14%
  • importSrpHome/homeAfterImportWithNewWallet: +16%
  • importSrpHome/longTaskTotalDuration: +12%
  • importSrpHome/longTaskMaxDuration: +18%
  • importSrpHome/tbt: +16%
  • importSrpHome/total: +15%
  • importSrpHome/inp: +17%
  • importSrpHome/fcp: +13%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 solanaAssetDetails/FCP: p75 1.8s
  • 🟡 importSrpHome/INP: p75 360ms
  • 🟡 importSrpHome/FCP: p75 1.9s
  • 🟡 solanaAssetDetails/FCP: p75 2.7s
  • 🟡 solanaAssetDetails/LCP: p75 3.6s
  • 🟡 sendTransactions/FCP: p75 2.0s
  • 🟡 sendTransactions/LCP: p75 2.8s
Dapp Page Load Benchmarks · Samples: 100

⚠️ Missing data: chrome/webpack/pageLoadBenchmark

✅ No regressions detected

Bundle size diffs
  • background: 0 Bytes (0%)
  • ui: 0 Bytes (0%)
  • common: 0 Bytes (0%)
  • other: 0 Bytes (0%)
  • contentScripts: 0 Bytes (0%)
  • zip: 0 Bytes (0%)

@github-actions github-actions Bot added the size-M label Aug 5, 2026
Comment thread app/scripts/lib/sentry-trace-propagation.concurrency.test.ts Outdated
Comment thread shared/lib/trace.test.ts Outdated
`it.failing` type-checks fine in isolation, but this repo also has
`@types/mocha` installed, which declares a conflicting global `it` with no
`.failing` member -- `yarn lint:tsc` picked the wrong merged type and
failed with `Property 'failing' does not exist on type 'TestFunction'`.
Switched both new-bug-revealing tests to `it.skip` instead, which is what
this repo's own tooling can actually support; `jest/no-disabled-tests` is
`error`-level for `shared/**/*.test.ts` (not configured at all for
`app/scripts/lib/**/*.test.ts`, a separate pre-existing gap in
`.eslintrc.js`'s file globs), so `shared/lib/trace.test.ts`'s two skips get
an explicit, comment-justified `eslint-disable-next-line`.

Also fixes:
- `Sentry.Client` isn't exported from `@sentry/browser`'s type namespace;
  import `Client` from `@sentry/core` instead, matching the existing
  `sentry-trace-propagation.ts`.
- `trace()`'s public `TraceCallback<T>` type is `(context?: TraceContext) =>
  T`, not `(span: Sentry.Span | null) => T` -- reading the callback's own
  parameter for the real Sentry span (as the original version of these
  tests did) doesn't type-check. Read `Sentry.getActiveSpan()` from inside
  the callback instead, which also more faithfully mirrors how production
  code (`getSerializedTraceContext()`) actually reads it.
- Generic type parameter `T` was too short for
  `@typescript-eslint/naming-convention`'s minimum-length rule; renamed to
  `Value`.
- `_traceId`/`_spanId`/`trace_id` object-literal keys (required by
  `hasDistributedTraceIds`'s and Sentry's own shapes) now carry the
  `eslint-disable-next-line @typescript-eslint/naming-convention` comment
  already used elsewhere in this codebase for the same shapes.

Verified after each fix: `yarn jest` green (3 skipped, 43 passed), the
three skipped tests still fail for the intended reason when temporarily
un-skipped, `yarn lint:eslint` clean on both files, `yarn lint:tsc` clean
project-wide.
@metamask-ci

metamask-ci Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
Builds ready [3cd1d07] [reused from 4a98074]
⚡ Performance Benchmarks (Total: 🟢 10 pass · 🟡 7 warn · 🔴 4 fail)

Baseline (latest main): 171ed20 | Date: 7/28/2026 | Pipeline: 31035147109 | Baseline logs

Metricschrome-webpackfirefox-webpack
onboardingImportWallet
[Sentry log · main/release]
🔴 srpButtonToSrpForm(p95) [CI log]🔴 [CI log]
onboardingNewWallet
[Sentry log · main/release]
🔴 longTaskTotalDuration(p95) [CI log]🔴 [CI log]

Regressions (🔴 4 failures)

Interaction Benchmarks · Samples: 5
Benchmarkchrome-webpackfirefox-webpack
loadNewAccount
[Sentry log · main/release]
🟢 [CI log]🟡 [CI log]
🟡 load_new_account
confirmTx
[Sentry log · main/release]
🟢 [CI log]🟡 [CI log]
bridgeUserActions
[Sentry log · main/release]
🟢 [CI log]🟢 [CI log]

📈 Results compared to the previous 5 runs on main

  • loadNewAccount/fcp: -12%
  • confirmTx/longTaskTotalDuration: -23%
  • confirmTx/longTaskMaxDuration: -24%
  • confirmTx/tbt: -41%
  • confirmTx/fcp: -16%
  • confirmTx/lcp: +743%
  • bridgeUserActions/bridge_load_page: -11%
  • bridgeUserActions/bridge_load_asset_picker: -26%
  • bridgeUserActions/longTaskCount: -44%
  • bridgeUserActions/longTaskTotalDuration: -48%
  • bridgeUserActions/longTaskMaxDuration: -20%
  • bridgeUserActions/tbt: -55%
  • bridgeUserActions/total: -12%
  • bridgeUserActions/inp: -15%
  • bridgeUserActions/fcp: -16%
  • bridgeUserActions/lcp: -13%
  • loadNewAccount/load_new_account: +21%
  • loadNewAccount/total: +21%
  • loadNewAccount/inp: +165%
  • loadNewAccount/lcp: +1208%
  • confirmTx/confirm_tx: +16%
  • confirmTx/longTaskCount: -100%
  • confirmTx/longTaskTotalDuration: -100%
  • confirmTx/longTaskMaxDuration: -100%
  • confirmTx/tbt: -100%
  • confirmTx/total: +16%
  • confirmTx/inp: -24%
  • confirmTx/lcp: +1174%
  • bridgeUserActions/bridge_load_page: +340%
  • bridgeUserActions/bridge_load_asset_picker: +53%
  • bridgeUserActions/longTaskCount: -100%
  • bridgeUserActions/longTaskTotalDuration: -100%
  • bridgeUserActions/longTaskMaxDuration: -100%
  • bridgeUserActions/tbt: -100%
  • bridgeUserActions/total: +37%
  • bridgeUserActions/inp: -23%
  • bridgeUserActions/fcp: -46%
  • bridgeUserActions/lcp: +1206%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 loadNewAccount/INP: p75 280ms
  • 🟡 loadNewAccount/FCP: p75 1.9s
  • 🟡 confirmTx/FCP: p75 1.8s
Startup Benchmarks · Samples: 100
Benchmarkchrome-webpackfirefox-webpack
startupStandardHome
[Sentry log · main/release]
🟢 [CI log]🟢 [CI log]
startupPowerUserHome
[Sentry log · main/release]
🟡 [CI log]

📈 Results compared to the previous 5 runs on main

  • startupStandardHome/numNetworkReqs: -14%
  • startupStandardHome/domInteractive: -24%
  • startupStandardHome/numNetworkReqs: -13%
  • startupStandardHome/fcp: -19%
  • startupPowerUserHome/uiStartup: +23%
  • startupPowerUserHome/load: +24%
  • startupPowerUserHome/domContentLoaded: +24%
  • startupPowerUserHome/domInteractive: +17%
  • startupPowerUserHome/backgroundConnect: +65%
  • startupPowerUserHome/firstReactRender: +20%
  • startupPowerUserHome/initialActions: +11%
  • startupPowerUserHome/loadScripts: +23%
  • startupPowerUserHome/setupStore: +295%
  • startupPowerUserHome/inp: +10%
  • startupPowerUserHome/fcp: +15%
  • startupPowerUserHome/lcp: +19%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 startupPowerUserHome/INP: p75 208ms
  • 🟡 startupPowerUserHome/LCP: p75 3.4s
User Journey Benchmarks · Samples: 5 · mock API 🔴 4

⚠️ Missing data: chrome/webpack/userJourneyTransactions

Benchmarkchrome-webpackfirefox-webpack
onboardingImportWallet
[Sentry log · main/release]
🔴 [CI log]
🔴 doneButtonToHomeScreen
🔴 total
🔴 [CI log]
🔴 total
onboardingNewWallet
[Sentry log · main/release]
🔴 [CI log]
🔴 total
🔴 [CI log]
🔴 total
assetDetails
[Sentry log · main/release]
🟢 [CI log]🟢 [CI log]
solanaAssetDetails
[Sentry log · main/release]
🟡 [CI log]🟡 [CI log]
importSrpHome
[Sentry log · main/release]
🟡 [CI log]🟢 [CI log]
sendTransactions
[Sentry log · main/release]
🟡 [CI log]
swap
[Sentry log · main/release]
🟢 [CI log]

📈 Results compared to the previous 5 runs on main

  • onboardingImportWallet/doneButtonToHomeScreen: -36%
  • onboardingImportWallet/openAccountMenuToAccountListLoaded: +139%
  • onboardingImportWallet/longTaskCount: +57%
  • onboardingImportWallet/longTaskTotalDuration: +20%
  • onboardingNewWallet/skipBackupToMetricsScreen: -18%
  • onboardingNewWallet/agreeButtonToOnboardingSuccess: -11%
  • onboardingNewWallet/doneButtonToAssetList: -11%
  • onboardingNewWallet/tbt: +34%
  • onboardingNewWallet/total: -11%
  • solanaAssetDetails/assetClickToPriceChart: +28%
  • solanaAssetDetails/longTaskCount: -100%
  • solanaAssetDetails/longTaskTotalDuration: -100%
  • solanaAssetDetails/longTaskMaxDuration: -100%
  • solanaAssetDetails/tbt: -100%
  • solanaAssetDetails/total: +28%
  • solanaAssetDetails/inp: +11%
  • solanaAssetDetails/fcp: +18%
  • solanaAssetDetails/lcp: +14%
  • importSrpHome/loginToHomeScreen: +14%
  • importSrpHome/homeAfterImportWithNewWallet: +16%
  • importSrpHome/longTaskTotalDuration: +12%
  • importSrpHome/longTaskMaxDuration: +18%
  • importSrpHome/tbt: +16%
  • importSrpHome/total: +15%
  • importSrpHome/inp: +17%
  • importSrpHome/fcp: +13%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 solanaAssetDetails/FCP: p75 1.8s
  • 🟡 importSrpHome/INP: p75 360ms
  • 🟡 importSrpHome/FCP: p75 1.9s
  • 🟡 solanaAssetDetails/FCP: p75 2.7s
  • 🟡 solanaAssetDetails/LCP: p75 3.6s
  • 🟡 sendTransactions/FCP: p75 2.0s
  • 🟡 sendTransactions/LCP: p75 2.8s
Dapp Page Load Benchmarks · Samples: 100

⚠️ Missing data: chrome/webpack/pageLoadBenchmark

✅ No regressions detected

Bundle size diffs
  • background: 0 Bytes (0%)
  • ui: 0 Bytes (0%)
  • common: 0 Bytes (0%)
  • other: 0 Bytes (0%)
  • contentScripts: 0 Bytes (0%)
  • zip: 0 Bytes (0%)

@cursor cursor Bot left a comment

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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3cd1d07. Configure here.

Comment thread shared/lib/trace.test.ts
// (candidate directions are in the ticket), remove `.skip` -- if it's
// still red at that point, the fix is incomplete.
// eslint-disable-next-line jest/no-disabled-tests
it.skip('does not parent the second span under the still-pending first one', async () => {

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.

Skipped tests lose fix detection

Medium Severity

These bug-reproducing cases were switched from it.failing to it.skip, so they never run. That drops the PR’s stated guard: today the defect stays invisible in CI, and a later fix won’t surface as an unexpected pass. The new comments also imply the alternative was a red suite, which it.failing already avoids.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3cd1d07. Configure here.

@MajorLift MajorLift changed the title Add regression tests proving concurrent trace() calls corrupt Sentry async-context state test: add regression tests proving concurrent trace() calls corrupt Sentry async-context state Aug 5, 2026
@metamask-ci metamask-ci Bot added the INVALID-PR-TEMPLATE PR's body doesn't match template label Aug 5, 2026
CI's `Test lint` job runs `yarn lint`, which includes `yarn lint:format`
(`oxfmt -c oxfmt.config.mts --check`) ahead of ESLint and tsc -- a check
this repo's own coding-guidelines skill calls out separately from ESLint
("Do not run Prettier directly on code files; Prettier remains for JSON
formatting"), and one this PR's earlier commits never ran. No behavioral
change; `yarn jest` and `yarn lint:eslint` both still pass unchanged.
@metamask-ci

metamask-ci Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
Builds ready [a11a6b8] [reused from 4a98074]
⚡ Performance Benchmarks (Total: 🟢 10 pass · 🟡 7 warn · 🔴 4 fail)

Baseline (latest main): 171ed20 | Date: 7/28/2026 | Pipeline: 31036153698 | Baseline logs

Metricschrome-webpackfirefox-webpack
onboardingImportWallet
[Sentry log · main/release]
🔴 srpButtonToSrpForm(p95) [CI log]🔴 [CI log]
onboardingNewWallet
[Sentry log · main/release]
🔴 longTaskTotalDuration(p95) [CI log]🔴 [CI log]

Regressions (🔴 4 failures)

Interaction Benchmarks · Samples: 5
Benchmarkchrome-webpackfirefox-webpack
loadNewAccount
[Sentry log · main/release]
🟢 [CI log]🟡 [CI log]
🟡 load_new_account
confirmTx
[Sentry log · main/release]
🟢 [CI log]🟡 [CI log]
bridgeUserActions
[Sentry log · main/release]
🟢 [CI log]🟢 [CI log]

📈 Results compared to the previous 5 runs on main

  • loadNewAccount/fcp: -12%
  • confirmTx/longTaskTotalDuration: -23%
  • confirmTx/longTaskMaxDuration: -24%
  • confirmTx/tbt: -41%
  • confirmTx/fcp: -16%
  • confirmTx/lcp: +743%
  • bridgeUserActions/bridge_load_page: -11%
  • bridgeUserActions/bridge_load_asset_picker: -26%
  • bridgeUserActions/longTaskCount: -44%
  • bridgeUserActions/longTaskTotalDuration: -48%
  • bridgeUserActions/longTaskMaxDuration: -20%
  • bridgeUserActions/tbt: -55%
  • bridgeUserActions/total: -12%
  • bridgeUserActions/inp: -15%
  • bridgeUserActions/fcp: -16%
  • bridgeUserActions/lcp: -13%
  • loadNewAccount/load_new_account: +21%
  • loadNewAccount/total: +21%
  • loadNewAccount/inp: +165%
  • loadNewAccount/lcp: +1208%
  • confirmTx/confirm_tx: +16%
  • confirmTx/longTaskCount: -100%
  • confirmTx/longTaskTotalDuration: -100%
  • confirmTx/longTaskMaxDuration: -100%
  • confirmTx/tbt: -100%
  • confirmTx/total: +16%
  • confirmTx/inp: -24%
  • confirmTx/lcp: +1174%
  • bridgeUserActions/bridge_load_page: +340%
  • bridgeUserActions/bridge_load_asset_picker: +53%
  • bridgeUserActions/longTaskCount: -100%
  • bridgeUserActions/longTaskTotalDuration: -100%
  • bridgeUserActions/longTaskMaxDuration: -100%
  • bridgeUserActions/tbt: -100%
  • bridgeUserActions/total: +37%
  • bridgeUserActions/inp: -23%
  • bridgeUserActions/fcp: -46%
  • bridgeUserActions/lcp: +1206%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 loadNewAccount/INP: p75 280ms
  • 🟡 loadNewAccount/FCP: p75 1.9s
  • 🟡 confirmTx/FCP: p75 1.8s
Startup Benchmarks · Samples: 100
Benchmarkchrome-webpackfirefox-webpack
startupStandardHome
[Sentry log · main/release]
🟢 [CI log]🟢 [CI log]
startupPowerUserHome
[Sentry log · main/release]
🟡 [CI log]

📈 Results compared to the previous 5 runs on main

  • startupStandardHome/numNetworkReqs: -14%
  • startupStandardHome/domInteractive: -24%
  • startupStandardHome/numNetworkReqs: -13%
  • startupStandardHome/fcp: -19%
  • startupPowerUserHome/uiStartup: +23%
  • startupPowerUserHome/load: +24%
  • startupPowerUserHome/domContentLoaded: +24%
  • startupPowerUserHome/domInteractive: +17%
  • startupPowerUserHome/backgroundConnect: +65%
  • startupPowerUserHome/firstReactRender: +20%
  • startupPowerUserHome/initialActions: +11%
  • startupPowerUserHome/loadScripts: +23%
  • startupPowerUserHome/setupStore: +295%
  • startupPowerUserHome/inp: +10%
  • startupPowerUserHome/fcp: +15%
  • startupPowerUserHome/lcp: +19%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 startupPowerUserHome/INP: p75 208ms
  • 🟡 startupPowerUserHome/LCP: p75 3.4s
User Journey Benchmarks · Samples: 5 · mock API 🔴 4

⚠️ Missing data: chrome/webpack/userJourneyTransactions

Benchmarkchrome-webpackfirefox-webpack
onboardingImportWallet
[Sentry log · main/release]
🔴 [CI log]
🔴 doneButtonToHomeScreen
🔴 total
🔴 [CI log]
🔴 total
onboardingNewWallet
[Sentry log · main/release]
🔴 [CI log]
🔴 total
🔴 [CI log]
🔴 total
assetDetails
[Sentry log · main/release]
🟢 [CI log]🟢 [CI log]
solanaAssetDetails
[Sentry log · main/release]
🟡 [CI log]🟡 [CI log]
importSrpHome
[Sentry log · main/release]
🟡 [CI log]🟢 [CI log]
sendTransactions
[Sentry log · main/release]
🟡 [CI log]
swap
[Sentry log · main/release]
🟢 [CI log]

📈 Results compared to the previous 5 runs on main

  • onboardingImportWallet/doneButtonToHomeScreen: -36%
  • onboardingImportWallet/openAccountMenuToAccountListLoaded: +139%
  • onboardingImportWallet/longTaskCount: +57%
  • onboardingImportWallet/longTaskTotalDuration: +20%
  • onboardingNewWallet/skipBackupToMetricsScreen: -18%
  • onboardingNewWallet/agreeButtonToOnboardingSuccess: -11%
  • onboardingNewWallet/doneButtonToAssetList: -11%
  • onboardingNewWallet/tbt: +34%
  • onboardingNewWallet/total: -11%
  • solanaAssetDetails/assetClickToPriceChart: +28%
  • solanaAssetDetails/longTaskCount: -100%
  • solanaAssetDetails/longTaskTotalDuration: -100%
  • solanaAssetDetails/longTaskMaxDuration: -100%
  • solanaAssetDetails/tbt: -100%
  • solanaAssetDetails/total: +28%
  • solanaAssetDetails/inp: +11%
  • solanaAssetDetails/fcp: +18%
  • solanaAssetDetails/lcp: +14%
  • importSrpHome/loginToHomeScreen: +14%
  • importSrpHome/homeAfterImportWithNewWallet: +16%
  • importSrpHome/longTaskTotalDuration: +12%
  • importSrpHome/longTaskMaxDuration: +18%
  • importSrpHome/tbt: +16%
  • importSrpHome/total: +15%
  • importSrpHome/inp: +17%
  • importSrpHome/fcp: +13%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 solanaAssetDetails/FCP: p75 1.8s
  • 🟡 importSrpHome/INP: p75 360ms
  • 🟡 importSrpHome/FCP: p75 1.9s
  • 🟡 solanaAssetDetails/FCP: p75 2.7s
  • 🟡 solanaAssetDetails/LCP: p75 3.6s
  • 🟡 sendTransactions/FCP: p75 2.0s
  • 🟡 sendTransactions/LCP: p75 2.8s
Dapp Page Load Benchmarks · Samples: 100

⚠️ Missing data: chrome/webpack/pageLoadBenchmark

✅ No regressions detected

Bundle size diffs
  • background: 0 Bytes (0%)
  • ui: 0 Bytes (0%)
  • common: 0 Bytes (0%)
  • other: 0 Bytes (0%)
  • contentScripts: 0 Bytes (0%)
  • zip: 0 Bytes (0%)

@sonarqubecloud

sonarqubecloud Bot commented Aug 5, 2026

Copy link
Copy Markdown

@MajorLift

MajorLift commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

🧪 Validation Run

This is a trial run of two experimental Claude Code skills — the evidence orchestrator (MetaMask/skills#84) and the race-condition-repro lane it routed to (MetaMask/skills#97). Corrections and feedback are welcome — open an issue or comment on either PR; no action is needed on this PR because of this comment.

Verdict: ✅ proven — Claim: concurrent startSpan() calls in the service worker corrupt each other's Sentry async-context state — both parentSpanId/traceId misattribution and consensys-request-id correlation — via the SDK's shared, unforked current-scope stack (not the isolation scope MetaMask-planning#7523 originally named). This is a Sentry-side limitation Sentry has tracked publicly and not fixed — getsentry/sentry-javascript#3751 ("Better scope management for async code", filed against @sentry/browser specifically) and getsentry/sentry-javascript#4071 — so a MetaMask-side mitigation is needed rather than an SDK upgrade. Regression coverage: #45249
head a11a6b844f0 · 2026-08-05 · deterministic interleaving

Forced-interleaving harness against the real, installed @sentry/browser/@sentry/core SDK (v10.38.0) — a real BrowserClient, hand-controlled deferred Promises, and microtask stepping. No Sentry internals mocked.

Guarantee table

guarantee forced how assertion result test
sequential startSpan() calls do not cross-parent fully await A, then start B spanToJSON(B).parent_span_id undefined pass trace.test.ts:612
an unrelated startSpan() call B started while A is unresolved gets parented under A and adopts A's traceId A awaits an unresolved deferred; B's startSpan() runs before A resolves B.traceId === A.traceId, B.parent_span_id === A.spanId today's behavior, it.skip'd trace.test.ts:635
a third concurrent op C inherits the corrupted lineage from B, not from A A and B both unresolved; C started while both are on the stack C.traceId === B.traceId, C.parent_span_id === B.spanId, ≠ A.spanId today's behavior, it.skip'd trace.test.ts:684
an operation's own outbound fetch correlates with a concurrently-unresolved, distinct-traceId operation's request id instead of its own two operations each carry an explicit, distinct distributed traceId; A resumes and fetches while B has not yet resolved correlatedRequestId(TRACE_ID_A) === requestId today's behavior, it.skip'd concurrency.test.ts:174
the same fetch correlates correctly once the concurrent operation has already resolved (discriminating control) B resolved and popped off the stack before A resumes and fetches correlatedRequestId(TRACE_ID_A) === requestId pass concurrency.test.ts:236

The three it.skip'd rows are the bug: with no fix in this PR, their assertions fail today, so they're wrapped it.skip (each with a comment stating exactly what fails and why) instead of left red — yarn jest reports them as skipped, not passed or failed.

Live-telemetry corroboration — rows 2–3 (parent-span misattribution). Both specimens below meet the same bar: a parent-child edge that the parent's own implementation, read at a pinned commit, has no code path to produce. parent_span_id is set entirely by Sentry.startSpan()'s parent-resolution logic, so such an edge has no explanation other than the stack corruption the tests force deterministically. Captured in the dev project and in production, each with its Sentry query, project selector and time window in frame.

Specimen — test-metamask. Trace 1e62a032e2c74fe79a69a8f4035de9aa, 13 spans, 17.7ms end‑to‑end, crosses from rpc.handler into messenger.call. Open this exact query in Sentry:

   0.0ms  rpc.handler     subscriptionsStopPolling                                               root
   3.0ms  rpc.handler     lookupSelectedNetworks                                                 root
   3.0ms  messenger.call  ↳ LegacyBackgroundApiService:lookupSelectedNetworks                    ↳ lookupSelectedNetworks (real parent)
   3.3ms  http.client     ↳↳ (its own fetch)                                                     ↳↳ the messenger.call above
   3.9ms  rpc.handler     subscriptionsStartPolling                                              root
   6.0ms  rpc.handler     trackMetaMetricsPage                                                   root
   8.1ms  rpc.handler     markNotificationPopupAsAutomaticallyClosed                             root
   8.1ms  messenger.call  ↳ LegacyBackgroundApiService:markNotificationPopupAsAutomaticallyClosed ↳ line above (real parent)
  12.5ms  rpc.handler     removePollingTokenFromAppState                                         root
  15.2ms  rpc.handler     addPollingTokenToAppState                                              root
  17.3ms  messenger.call  ClientController:setUiOpen                                             ⚠⚠ MISPARENTED — lookupSelectedNetworks's messenger.call node
  17.5ms  messenger.call  SnapController:setClientActive                                         ⚠⚠ MISPARENTED — lookupSelectedNetworks's messenger.call node
  17.7ms  messenger.call  BackendWebSocketService:disconnect                                     ⚠⚠ MISPARENTED — lookupSelectedNetworks's messenger.call node
Sentry UI — dev-project specimen, live query Sentry Traces view, query trace:1e62a032e2c74fe79a69a8f4035de9aa, test-metamask, 30D, parent_span column showing three messenger.call rows sharing parent 9231e0eb72ab57aa

Open full-size image

ClientController:setUiOpen, SnapController:setClientActive, and BackendWebSocketService:disconnect all show parent_span pointing at the lookupSelectedNetworks messenger-call node — but that function's real implementation (legacy-background-api-service.ts#L1011-L1030, pinned) only calls NetworkEnablementController:getState, NetworkController:getState, and NetworkController:lookupNetwork. It never calls any of the three.

Specimen — production (metamask, not test-metamask). One production trace (c95a7aa1aba945728ceb6bb00efaf94c) contains 349 Messenger Call: PermissionController:executeRestrictedMethod spans. Open this exact query in Sentry (the query behind the screenshot below):

span.op            count()
messenger.call      363
http.client          61
custom               17
rpc.handler          12
                total 453

That trace's full op breakdown sums to 453 spans over 18.7 seconds — small and short next to the hours-long mega-trace pattern noted further down.

parent_span (of the "executeRestrictedMethod" transaction)   count()
SnapRegistryController:requestPeriodicUpdate's span            327
a different, legitimate parent                                  22
                                                          total 349

327 of the 349 executeRestrictedMethod spans in that trace are parented under Messenger Call: SnapRegistryController:requestPeriodicUpdate. SnapRegistryController.ts's real implementation, pinned (the actual external package, not a mock) — requestPeriodicUpdate(), requestUpdate(), and the private #update() it calls — contains zero references to executeRestrictedMethod anywhere in the file; it only calls its own private methods and this.messenger.publish(...). This parent-child edge cannot come from real code.

Sentry UI — production specimen, live query Sentry Traces view, metamask project, 7D, filtered to SnapRegistryController requestPeriodicUpdate and PermissionController executeRestrictedMethod, executeRestrictedMethod rows sharing parent_span 8a012753fbd662f8

Open full-size image

A third cluster matched the shape but is not a defect. This trace shows ClientController:setUiOpen, SnapController:setClientActive, SnapRegistryController:requestPeriodicUpdate, and BackendWebSocketService:connect/disconnect sharing one parent — the same surface pattern as the two specimens above.

metamask-controller.js's set isClientOpen(open) (pinned) calls all four, in that order, from one real code path — a legitimate sibling group. Name mismatch alone is not evidence; only an edge the parent's implementation cannot produce is.

Implementation — forced-interleaving unit test suite (regression coverage)

Interleaving-forcing check. Every forced-interleaving test in shared/lib/trace.test.ts and sentry-trace-propagation.concurrency.test.ts asserts the pending operation's own getActiveSpan() result was truthy before the concurrent call runs. The sequential/discriminating-control rows above are the same assertions with only the overlap removed, and they pass — showing the harness discriminates real overlap from no overlap rather than failing for an unrelated reason.

Mutation check (shared/lib/trace.test.ts, sentry-trace-propagation.concurrency.test.ts). Disabling shared/lib/trace.ts's own active-span-inheritance shortcut below does not fix the parenting defect: Sentry.startSpan()'s own current-scope cloning independently inherits the ambient traceId from whatever is on top of the shared stack regardless of that shortcut. The same holds for getCurrentTraceId()'s getActiveSpan() branch — disabling it does not change the correlation outcome either, because the fallback path (getCurrentScope().getPropagationContext()) reads from the same corrupted shared stack-top. A fix confined to either shortcut would leave the underlying defect open.

shared/lib/trace.ts, startSpan():
  let forceTransaction: boolean | undefined;
  if (!parentSpan && !parentContext) {
    const activeSpan = sentryGetActiveSpan();
    if (activeSpan) {
      parentSpan = activeSpan;
      forceTransaction = true;
    }
  }

Exhibit — jest run (this PR's own two new test files, plus the sibling file they extend — --no-coverage, head a11a6b844f0):

$ yarn jest shared/lib/trace.test.ts app/scripts/lib/sentry-trace-propagation.concurrency.test.ts app/scripts/lib/sentry-trace-propagation.test.ts --no-coverage
PASS app/scripts/lib/sentry-trace-propagation.concurrency.test.ts
PASS app/scripts/lib/sentry-trace-propagation.test.ts
PASS shared/lib/trace.test.ts

Test Suites: 3 passed, 3 total
Tests:       3 skipped, 43 passed, 46 total
Snapshots:   0 total
Time:        7.335 s

Same result, from GitHub's own CI (not a local run) — Unit tests (3) and Unit tests (5), the two shards that drew these files, head a11a6b844f0:

2026-08-05T18:47:01.0323376Z PASS shared/lib/trace.test.ts
2026-08-05T18:48:28.6213632Z PASS app/scripts/lib/sentry-trace-propagation.concurrency.test.ts

Gist is a reproducibility backstop for the exact jest invocation above — a re-run path, not a substitute for the block already shown.

Follows from the above

  • The concurrency defect is real for both call sites named above, in the realistic RPC-overlap shape (not just a theoretical SDK-source reading).
  • The span-tree misattribution (rows 2–3) is independently corroborated in both the dev project and production, crossing from rpc.handler into messenger.call — ruling out an rpc.handler-specific explanation.
  • The root cause is the shared, unforked current-scope stack (AsyncContextStack._stack in @sentry/core), not the isolation scope — a fix aimed only at the isolation scope, or only at one of the two call sites, would leave the other exposed (mutation check above).
  • That stack's behavior sits upstream in the SDK, and Sentry's own tracker documents it as a known gap rather than a regression to report. getsentry/sentry-javascript#3751, filed against @sentry/browser specifically, states the mechanism directly: "the @sentry/browser SDK has no mechanism to automatically clone the Hub... any kind of async code may end up accidentally mixing up state unless hubs are manually cloned per unit of concurrency." Neither that issue nor getsentry/sentry-javascript#4071 was closed by a merged SDK fix — both were closed by a stale-issue bot — and @sentry/core@10.38.0 (this repo's installed version) still ships the docstring describing the same gap: "If no async context strategy is set, the isolation scope and the current scope will not be forked (this is currently the case, for example, in the browser)." Waiting on an SDK release is therefore not a fix path.
  • Three regression tests now exist (trace.test.ts:635, trace.test.ts:684, concurrency.test.ts:174) that will fail loudly the moment someone flips .skip back to it without an accompanying fix, or once a real fix lands and those flips are made deliberately.

Fix direction — decided in MetaMask-planning#7523, not implemented in this PR. Explicit context propagation, which is what Sentry's own engineers recommend for this defect class on getsentry/sentry-javascript#4071: "explicit propagation of hubs and scopes will always work — the issues arise when you try to implicitly propagate it. In the case of implicit propagation, the auto-instrumentation has to 'pick' the correct hub/scope to call methods on, which breaks down in async environments." Concretely: pass parentContext explicitly at the call sites that currently rely on implicit active-span resolution (closes rows 2–3), plus startNewTrace() at top-level request entry points and re-keying consensys-request-id per-span rather than per-trace (row 4 needs both, since ambient trace-id sharing is a separate contributor). Serializing/queueing overlapping trace calls was considered and dropped — blunt, costly on hot paths, and it does not address the ambient-sharing contributor at all. Scope, sequencing, and acceptance criteria live in MetaMask-planning#7523.

Open for review — one branch is not covered here. getCurrentTraceId()'s narrowest fallback ("isolation scope only, no active span anywhere") is not separable by observation from a distinct, non-buggy SDK behavior: root spans with no explicit parent share one ambient traceId for the JS realm's lifetime, which MetaMask-planning#7354 tracks separately. Whether that branch carries its own concurrency defect on top of that baseline sharing is an open question, not a resolved one — worth a decision on whether to track it under MetaMask-planning#7523, fold it into MetaMask-planning#7354, or accept the residual risk.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

INVALID-PR-TEMPLATE PR's body doesn't match template risk:low size-M team-extension-platform Extension Platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant