MetaMask / MetaMask/metamask-extension

[P2] Cold-start liveness: validate the SW-termination hypothesis + telemetry-flush fix

Open
#44,037 0 comments 0 reactions 0 assignees View on GitHub
INVALID-ISSUE-TEMPLATE team-extension-platform
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.