MetaMask / MetaMask/metamask-extension
[P2] Cold-start liveness: validate the SW-termination hypothesis + telemetry-flush fix
- Dominant language
- TypeScript
- Stars
- 13.2k
- Forks
- 5.6k
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 451
Description
**Related:** #43410 (the telemetry-flush half benefits that epic — cross-reference, not a parent)
**Files:** `app/service-worker.ts`, `app/scripts/lib/sentry-make-transport.ts`
**Size:** S | **Hours:** 2–4
---
## Problem
A startup-stability issue: the MV3 service worker can idle-terminate during cold-start initialization, before the keep-alive is established, causing startup errors that prevent the user from opening the extension. Two teardown races, verified against current `main`:
1. **Cold-start liveness (primary, premise unconfirmed).** `app/service-worker.ts` runs `await import('./scripts/background.js')` to load the heavy init. The `saveTimestamp` keep-alive heartbeat (`browser.storage.session.set`, which resets Chrome's ~30s idle timer) starts only **inside** the imported `background.js`, *after* the import resolves, leaving the import window without that heartbeat. **Whether the worker actually idle-terminates in that window is not confirmed** — for the UI-triggered path an open `runtime.Port` (`CriticalStartupErrorHandler.#port`) likely already holds it alive (see *Validate before building* below). The failures surface as startup phase-timeout errors; the `uiStartup.receivedAppInitPing:true` subset means the port was working yet `background.js` startup did not finish — which points at init not completing as much as at termination.
`event.waitUntil(runImportScripts())` alone is insufficient: it extends lifetime only on the `install` event, which does not fire on the recurring restart/wake cold-start path. (The module-scope `if (state === 'activated') runImportScripts()` already re-*invokes* init on restart; the gap is keeping the worker *alive through* the import, not calling it.)
2. **Telemetry flush (secondary).** `app/scripts/lib/sentry-make-transport.ts` sends envelopes via plain `fetch(...args)` with no `keepalive: true`, so an in-flight envelope POST dies on worker teardown (the SW has no `pagehide` / `visibilitychange` flush). The transport also `await`s `getAnalyticsState()` before each send, widening the race.
---
## Reported symptoms
User-facing startup failures consistent with this race, raised from the same startup-error handler (`ui/helpers/utils/critical-startup-error-handler.ts`) that carries the `receivedAppInitPing` tag:
- **#35572 — [Sentry] Background connection unresponsive** (`critical-startup-error-handler.ts:61`; Sentry `METAMASK-XQ6N`, ~199K occurrences): the UI's connection to the background times out.
- **#40473 — [Sentry] UI initialization timeout** (`critical-startup-error-handler.ts:112`): UI init times out waiting for the background to become ready.
- Possibly related: **#31519** (infinite loading screen on startup after update, no console errors, resolved by disable/re-enable).
This symptom is large and long-standing: "Background Connection Unresponsive" persisted at ~4,900 events/week through the prior cross-team epic **#39102** (Background Connection Reliability Improvements — `team-wallet-platform`, now closed; PRs #35308 / #36729 / #38322), which explicitly targets the *remaining* failure modes. Cold-start liveness is a candidate one.
These errors have more than one cause. This fix targets the subset where the worker terminates before `background.js` finishes init (tagged `receivedAppInitPing:true` — ping received, background startup unfinished); it will not eliminate cases where the port connection never establishes (`receivedAppInitPing:false`) or non-liveness failures. Diagnostic context: #39103.
---
## Validate before building the liveness fix
The liveness premise — that the worker idle-terminates during the import window — is **not yet confirmed and the architecture argues against it for the UI-triggered path.** `CriticalStartupErrorHandler` holds a long-lived `browser.Runtime.Port`, and an open `runtime.Port` keeps an MV3 worker alive (Chrome behaviour, independent of the 30s idle timer). Since #35572 (liveness timeout) and #40473 (init timeout) are the UI waiting on that open port, the worker is likely already held alive by the port — so the `receivedAppInitPing:true` subset (port worked at `onConnect`) is more consistent with **init not completing** (slow / hung / errored) than with idle-termination. A heartbeat started earlier does not make a hung init finish.
So validate first: instrument or check whether the worker actually terminates during these failures (vs. staying alive on the port while init hangs). If it does not terminate, the heartbeat-at-top is redundant and the real fix is in the init path, not the keep-alive timing.
## Solution
- **Telemetry flush (independently valid):** add `keepalive: true` to the transport `fetch` (envelopes <64KB, within the Fetch `keepalive` body cap) so the finalized pageload/transaction envelope survives teardown. Optionally cache the `getAnalyticsState()` opt-in check to narrow the race. This does not depend on the liveness question.
- **Liveness (conditional on the validation above):** if the worker *does* terminate during init (e.g. on non-UI/no-port cold starts where no port holds it up), start a heartbeat at the **top of `app/service-worker.ts`, before the import** — a *new* native `chrome.storage.session.set` call (the existing `saveTimestamp` uses the `browser` polyfill and lives in `background.js`, not yet loaded), plus `event.waitUntil(runImportScripts())` on `install`. Note this relies on Chrome's contested "extension-API call resets the idle timer" behaviour.
**Codebase Context:**
- `app/service-worker.ts` — `await import('./scripts/background.js')`; `onConnect` listener sends the early app-init liveness ping.
- `app/scripts/background.js` — `saveTimestamp()` and its `setInterval` (the heartbeat) live here, only reached after the import.
- `app/scripts/lib/sentry-make-transport.ts` — plain `fetch(...args)`, no `keepalive`.
- `ui/helpers/utils/critical-startup-error-handler.ts` — sets the `uiStartup.receivedAppInitPing` Sentry tag (PR #40189 / #40306).
---
## Acceptance Criteria
- [ ] **Validated:** determined whether the worker actually idle-terminates during init, or whether the open UI port holds it alive while init hangs/errors. This decides whether the heartbeat fix applies at all.
- [ ] `keepalive: true` on the Sentry transport `fetch`; finalized pageload/transaction envelopes survive worker teardown. (Independent of the validation above.)
- [ ] *If termination is confirmed:* heartbeat established at SW entry, before `await import('./scripts/background.js')`, on every cold start (install and restart), via native `chrome.storage.session`; the `receivedAppInitPing:true` phase-timeout errors measurably drop; no regression in idle-termination when genuinely idle.
---
## Labels
`team-extension-platform`
---
## References
- Reported symptoms this targets (Sentry error groups from the startup-error handler): #35572 (Background connection unresponsive), #40473 (UI initialization timeout); possibly #31519 (infinite load after update). Related background-connection debugging: #39103 / epic #39102.
- Related startup-stability hypotheses: #43773 (keep the SW alive through cold-start init), #43774 (ensure async storage writes / envelope POSTs finish before teardown).
- Liveness instrumentation this is measured by: #40189, #40306 (`uiStartup.receivedAppInitPing`).
- Telemetry-flush benefit to the SW Sentry-volume work: #43410. Distinct from #43136 (could-not-reproduce, assets-controller rate-limit).
Contributor guide
Research direction
Read app/service-worker.ts and app/scripts/background.js to trace cold-start import and heartbeat timing, then use the existing uiStartup.receivedAppInitPing signal and referenced startup failures to validate whether termination or a hung init is responsible. Separately inspect app/scripts/lib/sentry-make-transport.ts for the envelope fetch. Done means the liveness hypothesis is measured, the independent telemetry-flush behavior is fixed, and any conditional heartbeat change is justified by that evidence.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, typescript
- Domain
- tooling, web-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100