Skip to content

feat: improve handling of UI↔Background initialization timeout errors#40306

Open
gauthierpetetin wants to merge 65 commits intomainfrom
fix/critical-error-phase1-catch-scope
Open

feat: improve handling of UI↔Background initialization timeout errors#40306
gauthierpetetin wants to merge 65 commits intomainfrom
fix/critical-error-phase1-catch-scope

Conversation

@gauthierpetetin
Copy link
Copy Markdown
Contributor

@gauthierpetetin gauthierpetetin commented Feb 20, 2026

Description

This change improves handling of UI↔Background initialization timeout errors by distinguishing:

  • Background connection unresponsive (timeout during background liveness check)
  • Background initialization timeout
  • Background state sync timeout

All these errors bring the user critical error screen where UI can now show a "Restore accounts" option when a vault backup exists, that user can click to trigger the existing vault recovery flow.

Open in GitHub Codespaces

UI↔Background initialization flow

Sequence diagram (old flow)

The UI used a 2-phase startup check: first liveness (ALIVE), then a single wait for state sync (START_UI_SYNC). Because there was no separate signal for “background initialization done,” any timeout after liveness was reported as a generic “UI initialization timeout”, so we could not tell whether the background was stuck initializing or stuck sending state.

sequenceDiagram
    participant UI as UI (CriticalStartupErrorHandler)
    participant Port as Runtime Port
    participant BG as Background
    participant MC as MetaMaskController

    Note over UI,MC: Phase 1 – Liveness (timeout 15s)
    UI->>Port: connect
    Port->>BG: onConnect
    BG->>Port: postMessage(ALIVE)
    Port->>UI: ALIVE
    UI->>UI: phase 1 OK → start single “sync” phase

    Note over UI,MC: Single phase – wait for state sync (timeout 16s)
    Note over BG,MC: Background does init + state sync with no separate signal to UI
    BG->>BG: await isInitialized (controllers)
    BG->>MC: connectWindowPostMessage(port)
    MC->>Port: START_UI_SYNC + initialState
    Port->>UI: START_UI_SYNC
    UI->>UI: OK → normal app load

    Note over UI,MC: On timeout
    Note over UI: Phase 1 timeout → "Background connection unresponsive"
    Note over UI: Phase 2 timeout → "UI initialization timeout" (same for init or state-sync hang)
    UI->>UI: displayCriticalErrorMessage(...)
    UI->>UI: Show "MetaMask had trouble starting" (no Restore link)
Loading

Timeouts (old behavior)

Phase What UI waited for Timeout Error if timeout
1 ALIVE 15 s Background connection unresponsive
2 START_UI_SYNC 16 s UI initialization timeout (same whether background was stuck initializing or stuck sending state)

Sequence diagram (new flow)

The UI now uses a 3-phase startup check. Each phase waits for a message from the background.
This allows to know what step caused the timeout issue when one occurs.

sequenceDiagram
    participant UI as UI (CriticalStartupErrorHandler)
    participant Port as Runtime Port
    participant BG as Background
    participant MC as MetaMaskController

    Note over UI,MC: Phase 1 – Liveness (timeout 15s)
    UI->>Port: connect
    Port->>BG: onConnect
    BG->>Port: postMessage(ALIVE)
    Port->>UI: ALIVE
    UI->>UI: phase 1 OK → start Phase 2

    Note over UI,MC: Phase 2 – Initialization (timeout 16s)
    BG->>BG: await isInitialized (controllers)
    BG->>Port: postMessage(BACKGROUND_INITIALIZED)
    Port->>UI: BACKGROUND_INITIALIZED
    UI->>UI: phase 2 OK → start Phase 3

    Note over UI,MC: Phase 3 – State sync (timeout 16s)
    BG->>MC: connectWindowPostMessage(port)
    MC->>Port: START_UI_SYNC + initialState
    Port->>UI: START_UI_SYNC
    UI->>UI: phase 3 OK → normal app load

    Note over UI,MC: On timeout in any phase
    UI->>UI: displayCriticalErrorMessage(...)
    UI->>UI: Show "MetaMask had trouble starting" + Restore link if backup exists
Loading

Timeouts (new behavior)

Phase Message Timeout Error shown if timeout
1 ALIVE 15 s Background connection unresponsive
2 BACKGROUND_INITIALIZED 16 s Background initialization timeout
3 START_UI_SYNC 16 s Background state sync timeout

Note for reviewers: the heart of the logic is located in these 4 files:

File What changed Why
app/scripts/background.js Sends ALIVE then BACKGROUND_INITIALIZED, and attaches early repair and CRITICAL_ERROR_SCREEN_VIEWED listeners. UI must know "alive" vs "initialized" vs "state synced". "Restore accounts" must work even when init/state-sync never complete.
shared/lib/error-utils.js Restore link is shown when hasBackup is true. Let user trigger vault recovery flow when possible.
ui/helpers/utils/critical-startup-error-handler.ts Three sequential timeouts (ALIVE → BACKGROUND_INITIALIZED → START_UI_SYNC) with distinct error types. UI must show the right timeout type.
ui/helpers/utils/display-critical-error.ts Uses checkVaultBackupExists() (IndexedDB) to show "Restore accounts" and notifies background when the user clicks restore. PersistenceManager may be unavailable on timeout, so we read backup from IndexedDB. Background must react to restore action.

Most other files are there for adding tests or metrics.

Changelog

CHANGELOG entry: Adds "Restore accounts" on critical error screen when a backup exists.

Related issues

Fixes: #15629

Manual testing steps

  1. Build the extension: yarn build:test (or use manifest flag to simulate init/state-sync hang).
  2. Complete onboarding, then trigger a background init hang (e.g. via test flag simulateBackgroundInitializationHang).
  3. Confirm the critical error screen shows "Restore accounts" when a backup exists.
  4. Click "Restore accounts", confirm the dialog, and verify the wallet restores, thanks to the existing vault recovery flow.
  5. Repeat for state-sync hang (simulateBackgroundStateSyncHang) and confirm the same restore flow.

PS: To set remote feature flags, you can add the following in .manifest-overrides.json

{
  "_flags": {
    "testing": {
      "simulateBackgroundInitializationHang": true,
      "simulateBackgroundStateSyncHang": true
    }
  }
}

and the following line in .metamaskrc:


MANIFEST_OVERRIDES=.manifest-overrides.json

Screenshots/Recordings

Before

We didn't know what was causing the timeout, and there was no way to trigger the vault recovery flow (i.e. "Restore accounts"):

Screenshot 2026-02-23 at 06 18 37

After

We now differentiate whether timeout occurred during background initialization or while background sends the state to UI (state sync).
Also, we've added "Restore accounts" amongst options user can trigger.

Timeout during background initialization:
Screenshot 2026-02-19 at 15 26 38

Timeout while background sends the state to UI:
Screenshot 2026-02-20 at 06 41 34

Pre-merge author checklist

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Medium Risk
Touches UI↔background initialization, error handling, and recovery/repair flows; mistakes could block startup or trigger unintended reload/restore behavior, though changes are guarded and well-tested.

Overview
Improves critical startup timeout handling by splitting UI startup checks into three phases (background liveness, background initialized, and state sync) with distinct timeout errors, powered by a new BACKGROUND_INITIALIZED_METHOD signal from the background.

Adds a restore-from-backup path on the critical error screen: the UI now detects whether an IndexedDB vault backup exists, shows a "Restore accounts" link when it does, and can trigger a new timeout-specific repair message (METHOD_REPAIR_DATABASE_TIMEOUT) back to the background.

Refactors repair/backup utilities into shared helpers (shared/lib/backup, app/scripts/lib/repair) and wires a new CriticalErrorHandler in the background to coordinate repair + reload across all UI ports, including early Segment metrics (CriticalErrorScreenViewed, CriticalErrorRestoreWalletButtonPressed). Includes new unit tests and E2E coverage for init/state-sync timeout restore flows, plus i18n support for a reorderable critical error footer.

Written by Cursor Bugbot for commit ca022d3. This will update automatically on new commits. Configure here.

- Add restore accounts option on critical error screen when vault backup exists
- Background: reload all UI ports on timeout restore (align with vault recovery)
- Consolidate critical error analytics to two events: Critical Error Screen
  Viewed (with restore_accounts_enabled) and Critical Error Restore Wallet
  Button Pressed
- Add track-critical-error.ts for early Segment tracking
- Critical startup handler: three-phase check (liveness, init, state sync)
  with BACKGROUND_INITIALIZED and distinct timeout messages
- E2E: onboardThenTriggerTimeOutFlow, CriticalErrorPage restore link and
  waitForPageAfterExtensionReload; use waitUntil instead of driver.delay
  after restore confirm; home page send button supports EVM and non-EVM
- Unit tests: display-critical-error (restore link), critical-startup-error-handler
- i18n and manifest flags for init/state-sync hang simulation

Co-authored-by: Cursor <cursoragent@cursor.com>
@gauthierpetetin gauthierpetetin self-assigned this Feb 20, 2026
@gauthierpetetin gauthierpetetin added the team-extension-platform Extension Platform team label Feb 20, 2026
@github-actions
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.

@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 bot commented Feb 20, 2026

✨ Files requiring CODEOWNER review ✨

🧪 @MetaMask/qa (2 files, +94 -15)
  • 📁 test/
    • 📁 e2e/
      • 📁 page-objects/
        • 📁 flows/
          • 📄 vault-corruption.flow.ts +86 -6
        • 📁 pages/
          • 📄 account-list-page.ts +8 -9

@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 bot commented Feb 20, 2026

Builds ready [56aeb76]
⚡ Performance Benchmarks (1410 ± 113 ms)
👆 Interaction Benchmarks
ActionMetricMean (ms)Std Dev (ms)P75 (ms)P95 (ms)
Load New Accountload_new_account32043322384
total32043322384
Confirm Txconfirm_tx60391860486069
total60391860486069
Bridge User Actionsbridge_load_page2125216216
bridge_load_asset_picker22165281289
bridge_search_token72024743752
total11586412021238
🔌 Startup Benchmarks
BuildMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P 75 (ms)P 95 (ms)
Chrome Browserify Startup Standard HomeuiStartup14101157175411314611633
load1189968148410012361371
domContentLoaded118096214379512191353
domInteractive281798192580
firstPaint183671336175211402
backgroundConnect20918926915211248
firstReactRender20125262133
initialActions107113
loadScripts98978012389510281166
setupStore1464561628
numNetworkReqs312290192582
Chrome Browserify Startup Power User HomeuiStartup16971300217114517751952
load1125999172313911211492
domContentLoaded1108993164513011061442
domInteractive35191672633108
firstPaint198721429178239368
backgroundConnect28824645336290355
firstReactRender23145472639
initialActions104113
loadScripts90779414381299011246
setupStore1573851627
numNetworkReqs59381562555128
Chrome Webpack Startup Standard HomeuiStartup86468412381039331044
load73461196890795886
domContentLoaded72860696290787882
domInteractive2815125212483
firstPaint1116133955130201
backgroundConnect281969103242
firstReactRender18125572031
initialActions105113
loadScripts72560495589784877
setupStore1163751218
numNetworkReqs312293202588
Chrome Webpack Startup Power User HomeuiStartup1201891169815712871496
load7036181089104692998
domContentLoaded6936131078104682990
domInteractive35181553032112
firstPaint1266541866141268
backgroundConnect17412642763166318
firstReactRender22173132428
initialActions102112
loadScripts6916111065102680981
setupStore1145151316
numNetworkReqs1083426953138252
Firefox Browserify Startup Standard HomeuiStartup16031384237917516182015
load13341150205913313681572
domContentLoaded13331145205913313681572
domInteractive70302294384141
firstPaint------
backgroundConnect6236287356792
firstReactRender13111711415
initialActions102112
loadScripts13151136203512813491512
setupStore187178241552
numNetworkReqs311997212591
Firefox Browserify Startup Power User HomeuiStartup27232109386038527913550
load15541232230428116292223
domContentLoaded15531232230228016282223
domInteractive12634653124114414
firstPaint------
backgroundConnect3151121264259304901
firstReactRender191471101823
initialActions112022
loadScripts15261218228727116052177
setupStore85778914377377
numNetworkReqs58281533577137
Firefox Webpack Startup Standard HomeuiStartup16601425223114217031957
load1393120817079914431564
domContentLoaded1392120317079914431564
domInteractive852821949127185
firstPaint------
backgroundConnect56233213865113
firstReactRender15123331522
initialActions103112
loadScripts1375119816919214291532
setupStore198140241457
numNetworkReqs311994182779
Firefox Webpack Startup Power User HomeuiStartup26351984373535227623337
load15491236231924416902068
domContentLoaded15491236231824416842067
domInteractive12332776135106469
firstPaint------
backgroundConnect273114877210242827
firstReactRender20153142127
initialActions204122
loadScripts15151226230623616391985
setupStore1469734205161647
numNetworkReqs57261463480126
🧭 User Journey Benchmarks
BenchmarkMetricMean (ms)Std Dev (ms)P75 (ms)P95 (ms)
Onboarding Import WalletimportWalletToSocialScreen2171218218
srpButtonToSrpForm8808989
confirmSrpToPwForm2002020
pwFormToMetricsScreen1401414
metricsToWalletReadyScreen1501515
doneButtonToHomeScreen8122929841309
openAccountMenuToAccountListLoaded705467276368018
total822245985648955
Onboarding New WalletcreateWalletToSocialScreen2180218218
srpButtonToPwForm1032105106
createPwToRecoveryScreen8088
skipBackupToMetricsScreen3413535
agreeButtonToOnboardingSuccess1501515
doneButtonToAssetList684199890950
total106419712611332
Asset DetailsassetClickToPriceChart4104141
total4104141
Solana Asset DetailsassetClickToPriceChart4915050
total4915050
Import Srp HomeloginToHomeScreen18684218541940
openAccountMenuAfterLogin4054446
homeAfterImportWithNewWallet249113326082628
total461635947905225
Send TransactionsopenSendPageFromHome27133748
selectTokenToSendFormLoaded1912020
reviewTransactionToConfirmationPage8531854854
total89210891909
SwapopenSwapPageFromHome10516110132
fetchAndDisplaySwapQuotes4613746214621
total47052347104735
🌐 Dapp Page Load Benchmarks

Current Commit: 56aeb76 | Date: 2/20/2026

📄 Localhost MetaMask Test Dapp

Samples: 100

Summary

  • pageLoadTime-> current mean value: 1.04s (±43ms) 🟡 | historical mean value: 1.04s ⬇️ (historical data)
  • domContentLoaded-> current mean value: 723ms (±39ms) 🟢 | historical mean value: 726ms ⬇️ (historical data)
  • firstContentfulPaint-> current mean value: 77ms (±11ms) 🟢 | historical mean value: 80ms ⬇️ (historical data)

📈 Detailed Results

Metric Mean Std Dev Min Max P95 P99
pageLoadTime 1.04s 43ms 1.02s 1.35s 1.05s 1.35s
domContentLoaded 723ms 39ms 700ms 989ms 738ms 989ms
firstPaint 77ms 11ms 60ms 164ms 88ms 164ms
firstContentfulPaint 77ms 11ms 60ms 164ms 88ms 164ms
largestContentfulPaint 0ms 0ms 0ms 0ms 0ms 0ms
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 31.69 KiB (0.74%)
  • ui: 135.85 KiB (1.65%)
  • common: 239.67 KiB (2.19%)

@gauthierpetetin gauthierpetetin changed the title Critical error: restore accounts flow, analytics, and E2E feat: add restore accounts flow on critical error screen Feb 20, 2026
gauthierpetetin and others added 2 commits February 20, 2026 22:22
Remove backgroundInitializationTimeout and backgroundStateSyncTimeout from
en and en_GB; they were never referenced in code (error messages are
hardcoded in critical-startup-error-handler).

Co-authored-by: Cursor <cursoragent@cursor.com>
…tical-error flow

Co-authored-by: Cursor <cursoragent@cursor.com>
@gauthierpetetin gauthierpetetin marked this pull request as ready for review February 20, 2026 21:50
@gauthierpetetin gauthierpetetin requested a review from a team as a code owner February 20, 2026 21:50
@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 bot commented Feb 20, 2026

Builds ready [1a0a013]
⚡ Performance Benchmarks (1353 ± 105 ms)
👆 Interaction Benchmarks
ActionMetricMean (ms)Std Dev (ms)P75 (ms)P95 (ms)
Load New Accountload_new_account28624310318
total28624310318
Confirm Txconfirm_tx60371060396055
total60371060396055
Bridge User Actionsbridge_load_page24340271289
bridge_load_asset_picker22441252274
bridge_search_token72917743748
total11976312431271
🔌 Startup Benchmarks
BuildMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P 75 (ms)P 95 (ms)
Chrome Browserify Startup Standard HomeuiStartup13531144183210513851550
load114395515859411761310
domContentLoaded113795215779311701302
domInteractive2715135212372
firstPaint160651075127206341
backgroundConnect19818227313200221
firstReactRender19128072027
initialActions106114
loadScripts9547801379929861126
setupStore1465771623
numNetworkReqs312294202288
Chrome Browserify Startup Power User HomeuiStartup17141377221313717611999
load11221010161811911151478
domContentLoaded11081000154211411011451
domInteractive3319142193361
firstPaint189731497151246292
backgroundConnect28825444930293329
firstReactRender23156572437
initialActions109112
loadScripts90580813361109001238
setupStore1664771728
numNetworkReqs57361442157112
Chrome Webpack Startup Standard HomeuiStartup850697113389905986
load72961196988783899
domContentLoaded72360696588777893
domInteractive2816131232481
firstPaint1116128156137248
backgroundConnect28195473143
firstReactRender18123452027
initialActions107112
loadScripts72160496387775886
setupStore1263751224
numNetworkReqs312297212589
Chrome Webpack Startup Power User HomeuiStartup1234973209817113131536
load72462712561247231041
domContentLoaded71462112381237101033
domInteractive39181563437139
firstPaint1356243880164294
backgroundConnect17213136353168295
firstReactRender23173942431
initialActions103112
loadScripts71161812271207081023
setupStore1454681436
numNetworkReqs1083926250136230
Firefox Browserify Startup Standard HomeuiStartup16371397242219716532047
load13701157214816114031649
domContentLoaded13681157214816114021649
domInteractive84338409395138
firstPaint------
backgroundConnect63361782471102
firstReactRender13102011415
initialActions103112
loadScripts13501141212815413891599
setupStore197183281640
numNetworkReqs312195202589
Firefox Browserify Startup Power User HomeuiStartup27432003769460927923462
load15761232539244816252108
domContentLoaded15761232539244816252108
domInteractive11834736119113377
firstPaint------
backgroundConnect3401091216281433919
firstReactRender201469111961
initialActions213122
loadScripts15471218537244615922067
setupStore1517768211146647
numNetworkReqs59271483385129
Firefox Webpack Startup Standard HomeuiStartup17111461300923517472044
load14281220270320414581635
domContentLoaded14271219270320414581635
domInteractive982923249132175
firstPaint------
backgroundConnect64222253973135
firstReactRender16122931624
initialActions103122
loadScripts14091210267420114371591
setupStore2571813716142
numNetworkReqs301995162767
Firefox Webpack Startup Power User HomeuiStartup27341816413447728563774
load15731274242025616812155
domContentLoaded15731274241925616812154
domInteractive13133833156103534
firstPaint------
backgroundConnect2771101266229262903
firstReactRender23157792532
initialActions203123
loadScripts15411246237724616232054
setupStore17871199233244686
numNetworkReqs58271433384120
🧭 User Journey Benchmarks
BenchmarkMetricMean (ms)Std Dev (ms)P75 (ms)P95 (ms)
Onboarding Import WalletimportWalletToSocialScreen2216226229
srpButtonToSrpForm8919091
confirmSrpToPwForm2002121
pwFormToMetricsScreen1401515
metricsToWalletReadyScreen1501516
doneButtonToHomeScreen97728212131217
openAccountMenuToAccountListLoaded709670578237875
total843547687628940
Onboarding New WalletcreateWalletToSocialScreen2181218218
srpButtonToPwForm1085114114
createPwToRecoveryScreen8088
skipBackupToMetricsScreen3513637
agreeButtonToOnboardingSuccess1501616
doneButtonToAssetList50949483594
total89554873988
Asset DetailsassetClickToPriceChart3823939
total3823939
Solana Asset DetailsassetClickToPriceChart4514646
total4514646
Import Srp HomeloginToHomeScreen19544719902004
openAccountMenuAfterLogin4614646
homeAfterImportWithNewWallet24773124832524
total44727044984574
Send TransactionsopenSendPageFromHome1811919
selectTokenToSendFormLoaded28104040
reviewTransactionToConfirmationPage8588864873
total90413911923
SwapopenSwapPageFromHome12418138148
fetchAndDisplaySwapQuotes46668347284768
total47878748714876
🌐 Dapp Page Load Benchmarks

Current Commit: 1a0a013 | Date: 2/20/2026

📄 Localhost MetaMask Test Dapp

Samples: 100

Summary

  • pageLoadTime-> current mean value: 1.05s (±106ms) 🟡 | historical mean value: 1.04s ⬆️ (historical data)
  • domContentLoaded-> current mean value: 734ms (±127ms) 🟢 | historical mean value: 726ms ⬆️ (historical data)
  • firstContentfulPaint-> current mean value: 96ms (±196ms) 🟢 | historical mean value: 80ms ⬆️ (historical data)

📈 Detailed Results

Metric Mean Std Dev Min Max P95 P99
pageLoadTime 1.05s 106ms 1.02s 2.07s 1.06s 2.07s
domContentLoaded 734ms 127ms 704ms 1.98s 738ms 1.98s
firstPaint 96ms 196ms 60ms 2.04s 88ms 2.04s
firstContentfulPaint 96ms 196ms 60ms 2.04s 88ms 2.04s
largestContentfulPaint 0ms 0ms 0ms 0ms 0ms 0ms
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 2.2 KiB (0.05%)
  • ui: 2.41 KiB (0.03%)
  • common: 850 Bytes (0.01%)

gauthierpetetin and others added 2 commits February 23, 2026 06:03
…atch

- Add onupgradeneeded handler when opening metamask-backup so the store
  object store is created if the DB is created here first (avoids corrupting
  backup for PersistenceManager).
- Close DB connection in catch path to avoid leaking the connection when
  transaction or store access fails.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use fixed 3s delay then single switchToWindowWithTitle (like
VaultRecoveryPage.clickRecoveryButton) instead of polling. Polling
was flaky in CI and could contribute to ECONNREFUSED.

Co-authored-by: Cursor <cursoragent@cursor.com>
@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 bot commented Feb 23, 2026

Builds ready [d491397]
⚡ Performance Benchmarks (1370 ± 113 ms)
👆 Interaction Benchmarks
ActionMetricMean (ms)Std Dev (ms)P75 (ms)P95 (ms)
Load New Accountload_new_account3091310310
total3091310310
Confirm Txconfirm_tx6050460516056
total6050460516056
Bridge User Actionsbridge_load_page2443245247
bridge_load_asset_picker22556258311
bridge_search_token72016734739
total11866712251276
🔌 Startup Benchmarks
BuildMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P 75 (ms)P 95 (ms)
Chrome Browserify Startup Standard HomeuiStartup13701143174611314281558
load116697115149612111322
domContentLoaded115996614899412071311
domInteractive2716108202476
firstPaint1316641068179240
backgroundConnect20518149531207229
firstReactRender19126672024
initialActions105113
loadScripts97478712979310221119
setupStore1262541419
numNetworkReqs312297202284
Chrome Browserify Startup Power User HomeuiStartup17011358225013917561928
load1123979170613911101534
domContentLoaded1109966163013410971494
domInteractive36181822835103
firstPaint178691622165209317
backgroundConnect28425344124291324
firstReactRender23154872539
initialActions1010112
loadScripts90677114091308941283
setupStore17672101933
numNetworkReqs58351482457124
Chrome Webpack Startup Standard HomeuiStartup85468911361029081082
load724605103190784878
domContentLoaded718602102289779865
domInteractive2815112212381
firstPaint1216238660149213
backgroundConnect27195183045
firstReactRender18125062129
initialActions106113
loadScripts715600102088777858
setupStore1275061321
numNetworkReqs312293202587
Chrome Webpack Startup Power User HomeuiStartup1233935189114713301467
load7266311184105718990
domContentLoaded7176261174106708981
domInteractive3418152223592
firstPaint1326950076161266
backgroundConnect18513138466174366
firstReactRender23174142531
initialActions102112
loadScripts7146241164104705973
setupStore1455981430
numNetworkReqs1013724546134183
Firefox Browserify Startup Standard HomeuiStartup16281393318127316432065
load13671161294024614051751
domContentLoaded13661160293524614041751
domInteractive80339169796182
firstPaint------
backgroundConnect62222603670128
firstReactRender13112421415
initialActions103112
loadScripts13491143292223913911649
setupStore177155201549
numNetworkReqs311995192587
Firefox Browserify Startup Power User HomeuiStartup28402168416238230063651
load16121282240725717092244
domContentLoaded16111281240725717042244
domInteractive12036790134109419
firstPaint------
backgroundConnect3011251038235295899
firstReactRender20148391923
initialActions103122
loadScripts15691264237424916382191
setupStore131881921199681
numNetworkReqs59281633775141
Firefox Webpack Startup Standard HomeuiStartup17471452333434617552028
load14751198297232414641624
domContentLoaded14741197297232414641624
domInteractive117301549206130166
firstPaint------
backgroundConnect61251913468133
firstReactRender15122531619
initialActions103122
loadScripts14541189295232314471592
setupStore197132231558
numNetworkReqs311988182781
Firefox Webpack Startup Power User HomeuiStartup28042033399949329363847
load16271308303435117532444
domContentLoaded16271308303435117532440
domInteractive135321195182105554
firstPaint------
backgroundConnect2831151381222294703
firstReactRender231680122334
initialActions203122
loadScripts15791291261930617182165
setupStore1477715190198633
numNetworkReqs58271944073139
🧭 User Journey Benchmarks
BenchmarkMetricMean (ms)Std Dev (ms)P75 (ms)P95 (ms)
Onboarding Import WalletimportWalletToSocialScreen2181218218
srpButtonToSrpForm9029192
confirmSrpToPwForm2102122
pwFormToMetricsScreen1501515
metricsToWalletReadyScreen1611616
doneButtonToHomeScreen62890584783
openAccountMenuToAccountListLoaded751635876658043
total883316889858995
Onboarding New WalletcreateWalletToSocialScreen2181218218
srpButtonToPwForm1032105106
createPwToRecoveryScreen8088
skipBackupToMetricsScreen3513636
agreeButtonToOnboardingSuccess1511516
doneButtonToAssetList61220629629
total9892310091009
Asset DetailsassetClickToPriceChart3453741
total3453741
Solana Asset DetailsassetClickToPriceChart4824851
total4824851
Import Srp HomeloginToHomeScreen18458618531967
openAccountMenuAfterLogin4024243
homeAfterImportWithNewWallet258013726432815
total454723446994927
Send TransactionsopenSendPageFromHome26113940
selectTokenToSendFormLoaded2683536
reviewTransactionToConfirmationPage85715872879
total9026904911
SwapopenSwapPageFromHome10945150161
fetchAndDisplaySwapQuotes46698247494753
total47927448104899
🌐 Dapp Page Load Benchmarks

Current Commit: d491397 | Date: 2/23/2026

📄 Localhost MetaMask Test Dapp

Samples: 100

Summary

  • pageLoadTime-> current mean value: 1.03s (±36ms) 🟡 | historical mean value: 1.04s ⬇️ (historical data)
  • domContentLoaded-> current mean value: 716ms (±34ms) 🟢 | historical mean value: 726ms ⬇️ (historical data)
  • firstContentfulPaint-> current mean value: 75ms (±10ms) 🟢 | historical mean value: 80ms ⬇️ (historical data)

📈 Detailed Results

Metric Mean Std Dev Min Max P95 P99
pageLoadTime 1.03s 36ms 1.01s 1.30s 1.05s 1.30s
domContentLoaded 716ms 34ms 696ms 974ms 731ms 974ms
firstPaint 75ms 10ms 56ms 160ms 84ms 160ms
firstContentfulPaint 75ms 10ms 56ms 160ms 84ms 160ms
largestContentfulPaint 0ms 0ms 0ms 0ms 0ms 0ms
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 2.2 KiB (0.05%)
  • ui: 2.55 KiB (0.03%)
  • common: 850 Bytes (0.01%)

gauthierpetetin and others added 2 commits February 23, 2026 06:47
Co-authored-by: Cursor <cursoragent@cursor.com>
…rs restore tests

Co-authored-by: Cursor <cursoragent@cursor.com>
@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 bot commented Mar 16, 2026

Builds ready [5b10d08]
⚡ Performance Benchmarks
👆 Interaction Benchmarks
BenchmarkMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P75 (ms)P95 (ms)
Load New Accountload_new_account29026231819299318
total29026231819299318
Confirm Txconfirm_tx6089603162267561146226
total6089603162267561146226
Bridge User Actionsbridge_load_page21918527332231273
bridge_load_asset_picker26424527613276276
bridge_search_token7527507552755755
total1261124712681012681268
🔌 Startup Benchmarks
BenchmarkMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P75 (ms)P95 (ms)
Standard HomeuiStartup14531217197511914891641
load11991007164410612361377
domContentLoaded1192999161710412311351
domInteractive3017102202583
firstPaint164721191160206262
backgroundConnect21619840623218248
firstReactRender19123442027
initialActions106124
loadScripts996802141410110331154
setupStore1364661721
numNetworkReqs393184153879
Power User HomeuiStartup4859223914947214262138817
load13301171202814913461631
domContentLoaded13121162199914513281621
domInteractive44194265235133
firstPaint227871367149285391
backgroundConnect209432812319185533454675
firstReactRender24175162539
initialActions105113
loadScripts1087944170613511001368
setupStore1666481928
numNetworkReqs21711539760231334
🧭 User Journey Benchmarks
BenchmarkMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P75 (ms)P95 (ms)
Onboarding Import WalletimportWalletToSocialScreen2182182191218219
srpButtonToSrpForm95939719597
confirmSrpToPwForm23222312323
pwFormToMetricsScreen15151501515
metricsToWalletReadyScreen16161601616
doneButtonToHomeScreen60758765829594658
openAccountMenuToAccountListLoaded290228962908429032908
total3876385539192538653919
Onboarding New WalletcreateWalletToSocialScreen2232222230223223
srpButtonToPwForm1101071132111113
createPwToRecoveryScreen999099
skipBackupToMetricsScreen40394004040
agreeButtonToOnboardingSuccess16161701717
doneButtonToAssetList51747058039542580
total91686698944941989
Asset DetailsassetClickToPriceChart89521543795154
total89521543795154
Solana Asset DetailsassetClickToPriceChart1083717346127173
total1083717346127173
Import Srp HomeloginToHomeScreen2251218523526222912352
openAccountMenuAfterLogin52515315353
homeAfterImportWithNewWallet1679543248089023872480
total39842948475982846234759
Send TransactionsopenSendPageFromHome24242402424
selectTokenToSendFormLoaded35304033640
reviewTransactionToConfirmationPage1119844134320013251343
total1184900140220113801402
SwapopenSwapPageFromHome12010813512128135
fetchAndDisplaySwapQuotes2703268327372327242737
total2801279128211127982821
🌐 Dapp Page Load Benchmarks

Current Commit: 5b10d08 | Date: 3/16/2026

📄 Localhost MetaMask Test Dapp

Samples: 100

Summary

  • pageLoadTime-> current mean value: 1.05s (±46ms) 🟡 | historical mean value: 1.04s ⬆️ (historical data)
  • domContentLoaded-> current mean value: 739ms (±67ms) 🟢 | historical mean value: 737ms ⬆️ (historical data)
  • firstContentfulPaint-> current mean value: 84ms (±42ms) 🟢 | historical mean value: 88ms ⬇️ (historical data)

📈 Detailed Results

Metric Mean Std Dev Min Max P95 P99
pageLoadTime 1.05s 46ms 1.02s 1.37s 1.08s 1.37s
domContentLoaded 739ms 67ms 714ms 1.33s 759ms 1.33s
firstPaint 84ms 42ms 64ms 496ms 88ms 496ms
firstContentfulPaint 84ms 42ms 64ms 496ms 88ms 496ms
largestContentfulPaint 0ms 0ms 0ms 0ms 0ms 0ms
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 4.78 KiB (0.09%)
  • ui: 3.14 KiB (0.04%)
  • common: 2.61 KiB (0.02%)

@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 bot commented Mar 17, 2026

Builds ready [8cc0f2e]
⚡ Performance Benchmarks
👆 Interaction Benchmarks
BenchmarkMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P75 (ms)P95 (ms)
Load New Accountload_new_account29928232716302327
total29928232716302327
Confirm Txconfirm_tx603760266049860416049
total603760266049860416049
Bridge User Actionsbridge_load_page25820431241299312
bridge_load_asset_picker22515129860274298
bridge_search_token73469975727756757
total1217109413078412991307
🔌 Startup Benchmarks
BenchmarkMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P75 (ms)P95 (ms)
Standard HomeuiStartup15111292176810015411691
load1242103814428612791404
domContentLoaded1235103314338312701381
domInteractive3118100192785
firstPaint1557440380199316
backgroundConnect22520441224225258
firstReactRender21135262233
initialActions108124
loadScripts103081812288310661175
setupStore167168171830
numNetworkReqs393186153875
Power User HomeuiStartup4846232812372203061298038
load13471105192014814061638
domContentLoaded13281095187914113891583
domInteractive44213174734151
firstPaint20589601103270402
backgroundConnect191631010002183728905003
firstReactRender25174552637
initialActions105113
loadScripts1094885160613311291371
setupStore1656091836
numNetworkReqs25110738765303332
🧭 User Journey Benchmarks
BenchmarkMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P75 (ms)P95 (ms)
Onboarding Import WalletimportWalletToSocialScreen2202192211221221
srpButtonToSrpForm96939939999
confirmSrpToPwForm23212512425
pwFormToMetricsScreen16151711717
metricsToWalletReadyScreen17171801718
doneButtonToHomeScreen64160167930673679
openAccountMenuToAccountListLoaded2922290529391129282939
total3938390139722539603972
Onboarding New WalletcreateWalletToSocialScreen2192182201219220
srpButtonToPwForm1111101131112113
createPwToRecoveryScreen989099
skipBackupToMetricsScreen39374013940
agreeButtonToOnboardingSuccess16161711717
doneButtonToAssetList63750171777681717
total1067100511164010731116
Asset DetailsassetClickToPriceChart49425654956
total49425654956
Solana Asset DetailsassetClickToPriceChart1131101173114117
total1131101173114117
Import Srp HomeloginToHomeScreen2359230624294523812429
openAccountMenuAfterLogin513467125667
homeAfterImportWithNewWallet65063866713667667
total3052292831648330593164
Send TransactionsopenSendPageFromHome28243342733
selectTokenToSendFormLoaded29292902929
reviewTransactionToConfirmationPage1099680152231313971522
total1165734157830914521578
SwapopenSwapPageFromHome13812415913139159
fetchAndDisplaySwapQuotes268726842691226882691
total2823280928461428302846
🌐 Dapp Page Load Benchmarks

Current Commit: 8cc0f2e | Date: 3/17/2026

📄 Localhost MetaMask Test Dapp

Samples: 100

Summary

  • pageLoadTime-> current mean value: 1.05s (±113ms) 🟡 | historical mean value: 1.03s ⬆️ (historical data)
  • domContentLoaded-> current mean value: 743ms (±136ms) 🟢 | historical mean value: 727ms ⬆️ (historical data)
  • firstContentfulPaint-> current mean value: 100ms (±206ms) 🟢 | historical mean value: 90ms ⬆️ (historical data)

📈 Detailed Results

Metric Mean Std Dev Min Max P95 P99
pageLoadTime 1.05s 113ms 1.02s 2.15s 1.06s 2.15s
domContentLoaded 743ms 136ms 710ms 2.08s 751ms 2.08s
firstPaint 100ms 206ms 64ms 2.15s 92ms 2.15s
firstContentfulPaint 100ms 206ms 64ms 2.15s 92ms 2.15s
largestContentfulPaint 0ms 0ms 0ms 0ms 0ms 0ms
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 4.78 KiB (0.09%)
  • ui: 3.14 KiB (0.04%)
  • common: 2.57 KiB (0.02%)

Copy link
Copy Markdown

@cursor cursor bot left a comment

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.

Copy link
Copy Markdown

@cursor cursor bot left a comment

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 2 potential issues.

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.

@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 bot commented Mar 17, 2026

Builds ready [4f0d8e9]
⚡ Performance Benchmarks
👆 Interaction Benchmarks
BenchmarkMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P75 (ms)P95 (ms)
Load New Accountload_new_account29627133322304333
total29627133322304333
Confirm Txconfirm_tx6041601560611860536061
total6041601560611860536061
Bridge User Actionsbridge_load_page25221029329271293
bridge_load_asset_picker24120627126269271
bridge_search_token76474077916778779
total1249120812822812681282
🔌 Startup Benchmarks
BenchmarkMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P75 (ms)P95 (ms)
Standard HomeuiStartup14721255172911015311690
load1218101515059912591420
domContentLoaded1211101014429612551406
domInteractive291898202689
firstPaint154711444151201320
backgroundConnect21919926512223239
firstReactRender20134052131
initialActions107115
loadScripts100981412319510541200
setupStore1473461626
numNetworkReqs393182154376
Power User HomeuiStartup55682341169903094619414617
load13171153177014013361667
domContentLoaded12971138174713513141648
domInteractive45203305133139
firstPaint246801270177304408
backgroundConnect2853308143063063389911271
firstReactRender24183542632
initialActions104113
loadScripts1069933148212310841380
setupStore1576381626
numNetworkReqs20610438760237325
🧭 User Journey Benchmarks
BenchmarkMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P75 (ms)P95 (ms)
Onboarding Import WalletimportWalletToSocialScreen2182172191218219
srpButtonToSrpForm95949619696
confirmSrpToPwForm23222622426
pwFormToMetricsScreen16151711617
metricsToWalletReadyScreen17161701717
doneButtonToHomeScreen64258676071635760
openAccountMenuToAccountListLoaded2951293329671429672967
total3995392740967040634096
Onboarding New WalletcreateWalletToSocialScreen2192182191219219
srpButtonToPwForm1081071091109109
createPwToRecoveryScreen989099
skipBackupToMetricsScreen39364124041
agreeButtonToOnboardingSuccess16151601616
doneButtonToAssetList52947861755535617
total9198691008569271008
Asset DetailsassetClickToPriceChart91781101293110
total91781101293110
Solana Asset DetailsassetClickToPriceChart985313229115132
total985313229115132
Import Srp HomeloginToHomeScreen236823642373423732373
openAccountMenuAfterLogin724197208197
homeAfterImportWithNewWallet1294543231882822962318
total37362992483582046374835
Send TransactionsopenSendPageFromHome462164196364
selectTokenToSendFormLoaded38284784347
reviewTransactionToConfirmationPage1161967143616112001436
total12441020153417713051534
SwapopenSwapPageFromHome1147714625134146
fetchAndDisplaySwapQuotes270026992700027002700
total2816276328733728342873
🌐 Dapp Page Load Benchmarks

Current Commit: 4f0d8e9 | Date: 3/17/2026

📄 Localhost MetaMask Test Dapp

Samples: 100

Summary

  • pageLoadTime-> current mean value: 1.05s (±113ms) 🟡 | historical mean value: 1.04s ⬆️ (historical data)
  • domContentLoaded-> current mean value: 741ms (±110ms) 🟢 | historical mean value: 734ms ⬆️ (historical data)
  • firstContentfulPaint-> current mean value: 89ms (±89ms) 🟢 | historical mean value: 86ms ⬆️ (historical data)

📈 Detailed Results

Metric Mean Std Dev Min Max P95 P99
pageLoadTime 1.05s 113ms 1.02s 2.14s 1.07s 2.14s
domContentLoaded 741ms 110ms 709ms 1.81s 757ms 1.81s
firstPaint 89ms 89ms 64ms 968ms 92ms 968ms
firstContentfulPaint 89ms 89ms 64ms 968ms 92ms 968ms
largestContentfulPaint 0ms 0ms 0ms 0ms 0ms 0ms
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 4.7 KiB (0.09%)
  • ui: 3.39 KiB (0.04%)
  • common: 2.57 KiB (0.02%)

});
}
if (hasVault(backup)) {
await initBackground(backup);
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.

Hm, repairAndReinitialize now calls initBackground(...) again without destroying the existing controller / global listeners first.

That didn't happen in the old corruption flow because recovery only ever happened before background init completed, but this PR now reaches this path from the state-sync timeout after BACKGROUND_INITIALIZED has already been sent. At that point we already have a live controller and process-wide side effects (store subscriptions, webRequest listener, DeepLinkRouter, tab listeners, intervals, etc.), so reinitializing here duplicates a lot of background state machines and listeners after recovery.

We might need a larger refactor of the initialization process in order to make this work, but even then, we might not be able to properly tear down everything we need torn down, since anything could have set up misc globally listeners and streams.

I had an LLM write a test to test for this, here: https://github.com/MetaMask/metamask-extension/pull/41023/files#diff-6d83f689934173d72a111dcf3beb2c1b045a65cdd2f9ebea6a15b85e44850b24R190
It tests that the DeeplinkRouter is doubly subscribed after the restore call, and thus emits too many events.

Copy link
Copy Markdown
Contributor Author

@gauthierpetetin gauthierpetetin Mar 19, 2026

Choose a reason for hiding this comment

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

You're right, well done for the catch. It's not an easy one to fix :/

I looked at what we'd need to implement/refactor to properly teardown everything that needs to be and the amount of changes looks important. Such a global teardown function would also be hard to test and maintain.

I'm a bit disappointed by this conclusion, but I tend to think we should remove the "Restore accounts" option on the critical error screen for cases where the background is already initialized, i.e. after UI receives BACKGROUND_INITIALIZED message. The only option left in that case would be "Restart MetaMask" or "Contact support".
Does that sound reasonable to you, or do you have any other option in mind?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The solution you recommended on Slack is now implemented in this secondary PR and merged.

@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 bot commented Mar 23, 2026

Builds ready [a6135e0]
⚡ Performance Benchmarks
👆 Interaction Benchmarks
BenchmarkMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P75 (ms)P95 (ms)
Load New Accountload_new_account28226629311292293
total28226629311292293
Confirm Txconfirm_tx606960646078560676078
total606960646078560676078
Bridge User Actionsbridge_load_page22221024012226240
bridge_load_asset_picker2582492739263273
bridge_search_token7497407577755757
total1253121113083312631308
🔌 Startup Benchmarks
BenchmarkMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P75 (ms)P95 (ms)
Standard HomeuiStartup14971265175911115571702
load12421032146710213121422
domContentLoaded12361027145710113061414
domInteractive311791172875
firstPaint1597338473216270
backgroundConnect22320227714226253
firstReactRender20114562133
initialActions104124
loadScripts1031828125510010991208
setupStore1474461624
numNetworkReqs393185163779
Power User HomeuiStartup54101886178963316611613580
load12951099318023313101552
domContentLoaded12751090314322712921492
domInteractive40203224433105
firstPaint254801506217280333
backgroundConnect263931514380280836028940
firstReactRender23173742631
initialActions104113
loadScripts1059894286921510821252
setupStore1364861523
numNetworkReqs1365733147145241
🧭 User Journey Benchmarks
BenchmarkMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P75 (ms)P95 (ms)
Onboarding Import WalletimportWalletToSocialScreen2192182211221221
srpButtonToSrpForm91909109191
confirmSrpToPwForm21212202222
pwFormToMetricsScreen15151501515
metricsToWalletReadyScreen15151501515
doneButtonToHomeScreen52850255017531550
openAccountMenuToAccountListLoaded3022289531099631003109
total39273756407512540214075
Onboarding New WalletcreateWalletToSocialScreen2192182190219219
srpButtonToPwForm1061051071106107
createPwToRecoveryScreen888088
skipBackupToMetricsScreen38363913939
agreeButtonToOnboardingSuccess16151601616
doneButtonToAssetList57448870693667706
total96587310939010511093
Asset DetailsassetClickToPriceChart948010811104108
total948010811104108
Solana Asset DetailsassetClickToPriceChart957611314103113
total957611314103113
Import Srp HomeloginToHomeScreen2383231224756724182475
openAccountMenuAfterLogin945614739136147
homeAfterImportWithNewWallet23972235258412423852584
total4819475248564748564856
Send TransactionsopenSendPageFromHome26242822828
selectTokenToSendFormLoaded31263743637
reviewTransactionToConfirmationPage1034850119814611401198
total1097911125313611941253
SwapopenSwapPageFromHome1339417632165176
fetchAndDisplaySwapQuotes268426842684026842684
total2825277828593128482859
🌐 Dapp Page Load Benchmarks

Current Commit: a6135e0 | Date: 3/23/2026

📄 Localhost MetaMask Test Dapp

Samples: 100

Summary

  • pageLoadTime-> current mean value: 1.11s (±83ms) 🟡 | historical mean value: 1.05s ⬆️ (historical data)
  • domContentLoaded-> current mean value: 786ms (±91ms) 🟢 | historical mean value: 741ms ⬆️ (historical data)
  • firstContentfulPaint-> current mean value: 91ms (±45ms) 🟢 | historical mean value: 91ms ⬆️ (historical data)

📈 Detailed Results

Metric Mean Std Dev Min Max P95 P99
pageLoadTime 1.11s 83ms 1.03s 1.41s 1.33s 1.41s
domContentLoaded 786ms 91ms 721ms 1.37s 1.01s 1.37s
firstPaint 91ms 45ms 68ms 532ms 108ms 532ms
firstContentfulPaint 91ms 45ms 68ms 532ms 108ms 532ms
largestContentfulPaint 0ms 0ms 0ms 0ms 0ms 0ms
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 4.7 KiB (0.07%)
  • ui: 3.37 KiB (0.04%)
  • common: 2.58 KiB (0.02%)

…itical error restore flow (#41093)

## **Description**

This secondary PR will be merged into this [primary
PR](#40306) soon.
It's just kept as separate for now to facilitate reviews.

[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/41093?quickstart=1)

## **Changelog**

CHANGELOG entry: null

## **Related issues**

See [primary
PR](#40306)

## **Manual testing steps**

See [primary
PR](#40306)

## **Screenshots/Recordings**

<img width="1721" height="745" alt="Screenshot 2026-03-20 at 17 22 01"
src="https://github.com/user-attachments/assets/880481a2-744c-40a3-aae5-cec2ceb3a78f"
/>

## **Pre-merge author checklist**

- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Medium risk because it changes the critical startup error restore path
and background initialization sequencing, relying on persisted
`storage.local` state and tab handoff behavior across
`runtime.reload()`.
> 
> **Overview**
> Refactors the critical error restore flow to **open a
`https://metamask.io/restoring#...` tab, persist restore metadata in
`storage.local`, and trigger a `runtime.reload()`-based restart**
instead of reinitializing the background in-place.
> 
> On restart, `background.js` now checks for a pending restore,
initializes from the IndexedDB backup, sets `FirstTimeFlowType.restore`,
and then **hands off the restoring tab to the extension UI** (only if
the tab URL still matches, including locale redirects). Tests and e2e
flows were updated to match the new behavior, including more robust
window/tab handling after reload and optional skipping of multichain
sync waits in CI.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
c3d55e6. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…and rename to tab-handoff

Co-locate tab handoff logic with the other critical-error modules
(recovery, tracking) for better discoverability.

Made-with: Cursor
},
this.addMultichainWalletButton,
],
[this.addMultichainAccountButton, this.addMultichainWalletButton],
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

After a critical-error restore, the multichain account button displays "Syncing..." while background sync is in progress, so matching by both data-testid and text "Add account" causes a timeout in CI where sync takes longer. Removing the text requirement lets checkPageIsLoaded succeed as soon as the button element exists, and the new waitForSync option controls whether to additionally wait for sync to complete.

…airCallback

Now that the critical error handler uses runtime.reload() instead of
in-process reinit, repairAndReinitialize has only one caller. Inline
it to match the original main structure.

Made-with: Cursor
…r in repair.ts

Both runRepairAndReloadExtension and runRepairAndReloadPorts were
thin wrappers. Inline them at their single call sites to reduce
indirection and minimize the diff against main.

Made-with: Cursor
@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 bot commented Mar 24, 2026

Builds ready [f00fc58]
⚡ Performance Benchmarks
👆 Interaction Benchmarks
BenchmarkMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P75 (ms)P95 (ms)
Load New Accountload_new_account28526830314296303
total28526830314296303
Confirm Txconfirm_tx6079605061072160996107
total6079605061072160996107
Bridge User Actionsbridge_load_page21318624019216240
bridge_load_asset_picker23718428842285288
bridge_search_token7337307362736736
total12241079138410112591384
🔌 Startup Benchmarks
BenchmarkMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P75 (ms)P95 (ms)
Standard HomeuiStartup14931272173910015331682
load1234102014469312821401
domContentLoaded1227101614289012791383
domInteractive3117123232593
firstPaint161681234133190352
backgroundConnect22320530214227245
firstReactRender20125052129
initialActions1011214
loadScripts102081712118810631173
setupStore1485671624
numNetworkReqs393187163478
Power User HomeuiStartup4642169616053247254638276
load12991113184613013371595
domContentLoaded12761100183212213081550
domInteractive40212203833126
firstPaint250821852296266349
backgroundConnect160628912861209521563970
firstReactRender26175062836
initialActions104112
loadScripts1048886157611510741317
setupStore1665191934
numNetworkReqs1317727339142229
🧭 User Journey Benchmarks
BenchmarkMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P75 (ms)P95 (ms)
Onboarding Import WalletimportWalletToSocialScreen2192172201220220
srpButtonToSrpForm93929519395
confirmSrpToPwForm22222302223
pwFormToMetricsScreen15151601616
metricsToWalletReadyScreen16151711617
doneButtonToHomeScreen54552656817568568
openAccountMenuToAccountListLoaded3044291231208831133120
total3957384340217340194021
Onboarding New WalletcreateWalletToSocialScreen2182172201219220
srpButtonToPwForm1091091100109110
createPwToRecoveryScreen889099
skipBackupToMetricsScreen39364124041
agreeButtonToOnboardingSuccess16151601616
doneButtonToAssetList50849452110512521
total9008899118901911
Asset DetailsassetClickToPriceChart92879849398
total92879849398
Solana Asset DetailsassetClickToPriceChart77521041891104
total77521041891104
Import Srp HomeloginToHomeScreen2319224923794723352379
openAccountMenuAfterLogin543980155380
homeAfterImportWithNewWallet16173682637102323652637
total411827915521110047805521
Send TransactionsopenSendPageFromHome371752134652
selectTokenToSendFormLoaded34274253442
reviewTransactionToConfirmationPage1079887132316711681323
total1168964138114712621381
SwapopenSwapPageFromHome1188516730136167
fetchAndDisplaySwapQuotes268926762700926982700
total2807276928492928262849
🌐 Dapp Page Load Benchmarks

Current Commit: f00fc58 | Date: 3/24/2026

📄 Localhost MetaMask Test Dapp

Samples: 100

Summary

  • pageLoadTime-> current mean value: 1.04s (±44ms) 🟡 | historical mean value: 1.04s ⬇️ (historical data)
  • domContentLoaded-> current mean value: 733ms (±65ms) 🟢 | historical mean value: 732ms ⬆️ (historical data)
  • firstContentfulPaint-> current mean value: 83ms (±43ms) 🟢 | historical mean value: 87ms ⬇️ (historical data)

📈 Detailed Results

Metric Mean Std Dev Min Max P95 P99
pageLoadTime 1.04s 44ms 1.02s 1.38s 1.09s 1.38s
domContentLoaded 733ms 65ms 708ms 1.33s 765ms 1.33s
firstPaint 83ms 43ms 64ms 508ms 88ms 508ms
firstContentfulPaint 83ms 43ms 64ms 508ms 88ms 508ms
largestContentfulPaint 0ms 0ms 0ms 0ms 0ms 0ms
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 7.72 KiB (0.11%)
  • ui: 3.28 KiB (0.04%)
  • common: 2.55 KiB (0.02%)

…during onboarding

The merge from main changed onboarding MetaMetrics flow to use
ensureParticipateInMetaMetricsIsUnchecked(), which takes ~10s. This gave
the background enough time to write the backup to IndexedDB, causing
simulateBackgroundStateSyncHang to block state sync when
handleSidepanelPostOnboarding navigated to home.html — even though the
hang should only activate after a runtime.reload().

Fix: check backup existence once at startup in startExtensionInitialization
and share the result (backupExistedAtStartup) with both
simulateBackground*Hang checks, so they only trigger after a restart.

Made-with: Cursor
…ame flag

- Replace backup?.KeyringController with hasVault(backup) in
  background.js and persistence-manager.ts for consistency
- Compute hasVault once (backupHasVault) and share between the
  simulateBackground*Hang snapshot and pending-restore check
- Rename backupExistedAtStartup → inTestHasVault for consistency
  with inTestRestoreFlow

Made-with: Cursor
…ession

Rename types, functions, variables, and comments for consistency:
- PendingCriticalErrorRestore → CriticalErrorRestoreSession
- readPendingCriticalErrorRestore → readCriticalErrorRestoreSession
- clearPendingCriticalErrorRestore → clearCriticalErrorRestoreSession
- pendingRestore → restoreInProgress

Made-with: Cursor
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
68.7% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 bot commented Mar 24, 2026

Builds ready [40192a0]
⚡ Performance Benchmarks
👆 Interaction Benchmarks
BenchmarkMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P75 (ms)P95 (ms)
Load New Accountload_new_account28226531620274316
total28226531620274316
Confirm Txconfirm_tx6088606161122361106112
total6088606161122361106112
Bridge User Actionsbridge_load_page21919224421239244
bridge_load_asset_picker2592472678264267
bridge_search_token7607557654763765
total1256121513063312691306
🔌 Startup Benchmarks
BenchmarkMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P75 (ms)P95 (ms)
Standard HomeuiStartup15161260181812015791745
load12521040149010513051459
domContentLoaded12441034147510213001426
domInteractive3118150242695
firstPaint179731239168222359
backgroundConnect23321238421241256
firstReactRender21133962136
initialActions108224
loadScripts1029827125410010801210
setupStore1473751625
numNetworkReqs393186163879
Power User HomeuiStartup56221983168873244605614038
load14101182190116214911701
domContentLoaded13831175186915014591666
domInteractive45193805236145
firstPaint269881619220315470
backgroundConnect2208310139773049282710360
firstReactRender25175762835
initialActions105113
loadScripts1142933157913612171414
setupStore1687481724
numNetworkReqs1428729442151241
🧭 User Journey Benchmarks
BenchmarkMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P75 (ms)P95 (ms)
Onboarding Import WalletimportWalletToSocialScreen2182172201219220
srpButtonToSrpForm97969919699
confirmSrpToPwForm23232302323
pwFormToMetricsScreen16151601616
metricsToWalletReadyScreen16161601616
doneButtonToHomeScreen64154476977677769
openAccountMenuToAccountListLoaded2946291129742329532974
total40023863416310240494163
Onboarding New WalletcreateWalletToSocialScreen2182182190219219
srpButtonToPwForm1111101121112112
createPwToRecoveryScreen999099
skipBackupToMetricsScreen39374224042
agreeButtonToOnboardingSuccess17171701717
doneButtonToAssetList52048155224531552
total91687594526936945
Asset DetailsassetClickToPriceChart796297128497
total796297128497
Solana Asset DetailsassetClickToPriceChart85798948789
total85798948789
Import Srp HomeloginToHomeScreen2314221224357723442435
openAccountMenuAfterLogin553974135774
homeAfterImportWithNewWallet15062432372102023572372
total389525724837100746814837
Send TransactionsopenSendPageFromHome402657114857
selectTokenToSendFormLoaded38344133941
reviewTransactionToConfirmationPage1219931149723814801497
total12931004159323915421593
SwapopenSwapPageFromHome963917053148170
fetchAndDisplaySwapQuotes269226872698426922698
total2795274228584728472858
🌐 Dapp Page Load Benchmarks

Current Commit: 40192a0 | Date: 3/24/2026

📄 Localhost MetaMask Test Dapp

Samples: 100

Summary

  • pageLoadTime-> current mean value: 1.03s (±39ms) 🟡 | historical mean value: 1.04s ⬇️ (historical data)
  • domContentLoaded-> current mean value: 723ms (±37ms) 🟢 | historical mean value: 729ms ⬇️ (historical data)
  • firstContentfulPaint-> current mean value: 78ms (±10ms) 🟢 | historical mean value: 88ms ⬇️ (historical data)

📈 Detailed Results

Metric Mean Std Dev Min Max P95 P99
pageLoadTime 1.03s 39ms 1.00s 1.32s 1.05s 1.32s
domContentLoaded 723ms 37ms 702ms 1.00s 741ms 1.00s
firstPaint 78ms 10ms 60ms 160ms 88ms 160ms
firstContentfulPaint 78ms 10ms 60ms 160ms 88ms 160ms
largestContentfulPaint 0ms 0ms 0ms 0ms 0ms 0ms
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 7.95 KiB (0.11%)
  • ui: 3.25 KiB (0.04%)
  • common: 2.55 KiB (0.02%)

Copy link
Copy Markdown
Contributor

@davidmurdoch davidmurdoch left a comment

Choose a reason for hiding this comment

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

mostly just a review of the background.js changes. i'll review more files later!

testingFlags?.simulateBackgroundStateSyncHang ||
testingFlags?.simulateBackgroundInitializationHang
) {
await persistenceManager.open();
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.

ah, just noticed there is a potential race condition bug in persistence manager, as multiple callsites could try to open the db at the same time.

might need to fix persistenceManager's open so multiple opens can't be "active" at the same time. Maybe open here should be guarded with a if (this.#openPromise) return await this.#openPromise;, and then getBackup itself could also call this.#open() on its own so we don't have to call it here too.

}

try {
controller.onboardingController.setFirstTimeFlowType(
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.

can this even throw?

Comment on lines +2440 to +2446
initBackground(backup);
try {
await isInitialized;
} catch (error) {
log.error('critical-error-restore: initialization failed', error);
return;
}
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.

why not await initBackground(backup) here instead of calling then awaiting isInitialized? I'm guessing there would be a bug here if you did, related to initBackground never rejecting since it is already wrapped in a try/catch itself?

Perhaps the pre-existing vault corruption repairCallback function already has this bug, as this is almost the same code. Hmm, this is worth discussing I think.

connectWindowPostMessage(port);
connectWindowPostMessage(
port,
isMetaMaskUIPort ? removeCriticalErrorListeners : undefined,
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.

do we need the ternary here? removeCriticalErrorListeners is only defined when isMetaMaskUIPort is true

}
}
if (!process.env.SKIP_BACKGROUND_INITIALIZATION) {
async function startExtensionInitialization() {
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.

startExtensionInitialization calls initBackground which calls initialize.

Wonder if we can either improve the naming here, or just the code organization in a way that we don't need 3 cascading initialization functions. Worth talking about i think.

return;
}

const restoreInProgress = await readCriticalErrorRestoreSession(browser);
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.

this introduces a new point of failure, as readCriticalErrorRestoreSession reads from storage.local, which is probably the root of almost all of our problems this PR is working to improve. haha

Perhaps readCriticalErrorRestoreSession should try/catch itself and always swallow all errors (but log them, or course)?

Comment on lines +2421 to +2428
const backupHasVault = hasVault(backup);

if (
testingFlags?.simulateBackgroundStateSyncHang ||
testingFlags?.simulateBackgroundInitializationHang
) {
inTestHasVault = backupHasVault;
}
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.

The inTest related logic is hard to follow. This was already a confusing pattern before your PR (and I'm to blame, as I added the first instance of inTest ? getManifestFlags().testing : undefined right in this same file!😅), but now that you are storing global state in runtime code specifically for tests, it gets even harder. haha

What if, and this is just an idea off the top of my head -- not a request for changes, we did something like a test helper thing.

import { inTestFlags, inTestState } = "./some-path/in-test-helper.ts";

// then we could do things like: 
inTestFlags?.has(["simulateBackgroundStateSyncHang", "simulateBackgroundInitializationHang"]);

// and
inTestState?.get("value");
inTestState?.set("value", something);
inTestState?.remove();

// and maybe in the future if we find a need:
inTestDb?.set/get
// in-test-helper.ts pseudo code
const inTest = process.env.IN_TEST;
export const inTestFlags= inTest ? getTheFlagsMap() : null;
export const inTestState = inTest ? new Map() : null;
export const inTestDb= inTest ? getTheDbImpl() : null;

A potential big downside is that this might not be compiled out completely like it can when everything is defined in a single file. This can be solved in webpack config rules, but it is really gross to do it (and we don't do anything like this yet).

What do you think? Is it worth discussing further?

}

if (restoreInProgress) {
await clearCriticalErrorRestoreSession(browser);
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.

this is also sort of another new point of failure. hmmm. it might not be possible to avoid adding one here though, since we really DO need to clear out the old restore session, or we'll send the user into a loop. If we reject when trying to clear the restore session, we'll still them into a loop... but at least we'll have logs telling us where things when wrong.

I'm just mentioning this in case you hadn't yet thought of it, and could think of a better idea than I can.

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

Labels

size-XL team-extension-platform Extension Platform team

Projects

Status: Needs more work from the author

Development

Successfully merging this pull request may close these issues.

[Sentry] [Bug]: Metamask fails to load if no response from RPC

4 participants