[2026-09-09] eagle: work/projects/cpm-web-extension-breakage-findings.md
This commit is contained in:
@@ -783,3 +783,34 @@ Keys read by the recorder: `lifecycle` (string), `receivedInitResponse` (bool),
|
||||
- OSLogStore-based counters (per request).
|
||||
- iOS wiring (factory passes no recorder; behaviour unchanged there).
|
||||
- Breakage-report fields — `CPMMessagingDiagnosticsRecorder.snapshot().pixelParameters` is ready to be merged into `CookieConsentInfo` when wanted.
|
||||
|
||||
|
||||
## New failure class: background → app native messaging (2026-09-09, late)
|
||||
|
||||
Found by reading the actual `init` handler in `background-embedded.js` instead of WebKit. This class needs **no WebKit anomaly** and produces the exact all-tabs symptom and the exact health-monitor "stuck" signature.
|
||||
|
||||
### Mechanism (extension side, `background-embedded.js`)
|
||||
|
||||
`handleAutoConsentMessage` (2537) awaits three native requests before it ever sends `initResp`: `remoteConfigJson` (`getResourceIfNew`, 2555), `checkAutoconsentSetting` (`isAutoconsentSettingEnabled`, 2590), `checkAutoconsentEnabledForSite` (`isFeatureEnabled`, 2613). `_request` (2954) swallows any error or 20 s timeout and returns `undefined`; `checkAutoconsentSetting` turns that into `{ enabled: false }` (3040) → heuristic mode `""` → **"autoconsent setting not enabled" → `return` without `initResp`** (2592-2606). The follow-up `refreshDashboardState(setting_disabled)` is itself a native call, so when native messaging is broken nothing reaches the app → the health monitor sees no dashboard response → `cpmInitializationFailed` → `cpmMessagingStuck`. Every tab, every navigation, until the extension is reloaded. Content↔background works fine the whole time, so all WebKit-side probes read healthy.
|
||||
|
||||
### App side: how native messaging breaks
|
||||
|
||||
`WebExtensionContext::sendNativeMessage` calls the controller delegate directly (`WebExtensionContextAPIRuntimeCocoa.mm:338`), no permission or app-id check. The app (`WebExtensionManager+NativeMessaging.swift:122-151`) routes by `(context.uniqueIdentifier, featureName)` and returns **`nil`** on `.noHandler` and `.failure`; the extension turns `nil` into `throw "unexpected response type"` → `undefined` → disabled. Handlers exist only if `willLoad` registered them for the live context's identifier.
|
||||
|
||||
Paths that remove handlers while a context stays loaded:
|
||||
- `reloadExtension(identifier:)` (`WebExtensionManager.swift:414-449`): unload → `unregisterHandlers` → `loadWebExtension`. `WebExtensionLoader.loadWebExtension` has an idempotent branch (`WebExtensionLoader.swift:70-84`): if a context with that identifier is already in the controller it returns **without calling `willLoad`** → handlers stay unregistered. Reachable when another load of the same identifier interleaves between the unload and the load (e.g. scriptlet-triggered `reloadExtension(for:)` from `WebExtensionScriptletCoordinator.swift:165` racing the Fire `reloadInstalledExtensions`; the coordinator serializes only its own ops). Pinned by `testWhenExtensionIsAlreadyLoaded_ThenSecondLoadDoesNotCallDelegate`.
|
||||
- Any future code path calling `unregisterHandlers` without a matching `willLoad`.
|
||||
|
||||
Also possible without unregistering: a handler that never replies (delegate is `async`; a hang on the main actor) → 20 s timeout per request → three requests per `init` → same "disabled" outcome ~60 s later.
|
||||
|
||||
### Deterministic reproduction (added)
|
||||
|
||||
Debug → Web Extensions → **Break CPM Native Messaging (unregister handlers)** (`WebExtensionManager.unregisterNativeMessageHandlersForDebugging`). Then load a CMP page in any tab/window: banner not handled everywhere; Console shows `⚠️ No handler for extension '<uuid>' feature 'autoconsent'`; snapshot shows `native_handler_registered=false` with everything else green; health monitor fires `initialization_failed` then `stuck`. Reload the extension from the debug menu → recovers. This is the first reproduction that matches the production signature end to end.
|
||||
|
||||
### Diagnostics added
|
||||
|
||||
`native_handler_registered` (router lookup identical to `routeMessage`) in snapshot/pixels. In production stuck pixels, `native_handler_registered=false` = this class; `=true` with all probes green points at the timeout variant (handler hang) or at the content-script marker being absent.
|
||||
|
||||
### What this does and does not prove
|
||||
|
||||
Proves the class produces the symptom and that the code has a path (idempotent load after unregister) that leaves the live context without handlers. Does **not** yet prove that path fires in production — that is what `native_handler_registered` in the stuck pixels and Console `No handler for extension` lines on internal machines will tell.
|
||||
|
||||
Reference in New Issue
Block a user