7.9 KiB
tags, updated, status
| tags | updated | status | |||||
|---|---|---|---|---|---|---|---|
|
2026-11-11 | draft |
TD: Pixel Definition: CPM Wake Init Detection (macOS/iOS + embedded web extension)
Author: Alex M Reviewer: TBD (previous CPM pixel designs: Michal) Stakeholders: Michal, Konrad, Russell (from the previous Tech Designs; confirm). Extension change: the privacy-extension owners. Project: CPM: root-cause the stuck embedded extension messaging (macOS/iOS) Task: to create ("Extension lifecycle metrics") Previous Tech Designs: CPM breakage pixels, CPM failure diagnostics Privacy Triage: to request together with the CPM permanent breakage metric design, which this one extends Extension PR: duckduckgo-privacy-extension#3727
Background & Requirements
Failures after a background wake are the largest group with no fix: on macOS 1.214 they are 39% of all failures and 36% of permanent ones (0.68% of DAU); on iOS 7.244, 22% and 15% (0.06% of DAU). The usual causes are ruled out: the background is ready (under one second in 93% of macOS wake episodes), and a plain idle-unload and wake loses no messages in the real-WebKit harness.
What the app cannot see is where the startup message stops. For one page the chain is: the page's script sends init → the background receives it → reads the configuration → reads the user setting → checks the site → answers the page and refreshes the dashboard in the app. The app sees only the last step, so every failure looks the same: no answer.
Requirements: tell where the chain stopped for a page that got no answer, and whether the page really received its answer; no change to what the extension does for the page; no URLs, page content or persistent identifiers collected; an older app must tolerate a newer extension and the other way round.
Problem Statement
For every tab the permanent breakage metric reports as silent, record the furthest init step the extension reported for its page, so the next fix is picked from data.
Recommended Approach
The extension sends a small fire-and-forget notification to the app at each step of init. The app keeps the furthest step per tab and adds it as last_stage to the web_extension_cpm_breakage pixel. A notification never counts as an answer: only the dashboard refresh does. Notifications never create a report; they only describe a tab that is already silent.
last_stage |
The chain stopped |
|---|---|
none |
nothing reached the app (script not injected, or the message or notification was lost) |
page_init_sent |
the page's script sent init; the background never processed it |
received |
init arrived; the configuration or storage read did not finish |
config_read |
waiting for the user setting from the app |
settings_checked |
waiting for the site check |
site_checked |
all checks passed; no answer and no dashboard refresh arrived |
response_sent |
the answer was sent to the page, but no dashboard refresh followed |
response_failed |
sending the answer to the page failed |
The app keeps only the highest step, so a late or out-of-order notification never lowers it, and only notifications for the tab's current page count. Unknown step names are ignored, so a newer extension cannot break an older app. The Debug "Simulate CPM Messaging Breakage" switch hides these notifications as well.
Extension change. Notifications are sent inside the existing init handling: before any wait for the first step, after each check for the next, and around the answer to the page. They never wait, never throw and never change what the page receives; a failure to answer is still reported as before.
Compatibility. The extension ships inside the app, so it reaches users with an app release after its PR merges. The app side can ship first: with no notifications, last_stage is none. A newer extension on an older app is expected to be harmless; this is to be confirmed against the real message router. The page's script is one bundle for every extension build, so page_init_sent also reaches builds that ignore it, at the cost of one extra message per page.
Pros
- Turns "no answer" into a position in a known chain, which maps to the open hypotheses and to concrete fixes.
- Shows cases the dashboard refresh hides, such as an answer that was sent but not received.
- Out of the page's way: no waits, no state changes.
- Both sides tolerate the other being older.
Cons and risks
- Notifications travel through the same background whose failure we study, so the earliest failures end up as
none. - Up to seven extra notifications per page on a path suspected to be fragile. The extra load has not been measured. Mitigation: ship to internal builds first, compare breakage rates and the
last_stagemix with and without notifications, and dropconfig_readandsite_checkedif the cost shows. response_sentcan be misleading: WebKit may report a send as done when no one was listening. It never answers a page, so the metric is not affected, only howlast_stage=response_sentis read.- The extension part reaches users only with an app release, so the first data comes about two release cycles after the work.
- A page cannot forge notifications: the script runs in an isolated world, and the background accepts a page-side notification only for
page_init_sentand only from the main frame.
Testing
- App: the pixel carries the furthest step of the silent tab and not of another tab; no notifications give
none; a notification is not an answer; unknown steps are ignored; stale notifications are forgotten at the next page; the parameter appears in both platforms' pixel definitions. - Extension: the existing unit suite for the cookie prompt management, extended for the order of notifications, a site that disables CPM ending at
site_checked, and sub-frame and unknown-step notifications being ignored. Part of the extension change has not been run yet. - Not done: a real-WebKit run showing every step in order on a healthy page, the matching
last_stagefor an injected stall at each step, and a page without the script not yieldingresponse_sent.
Additional Considerations
Privacy: one new enumerated parameter on a pixel that is new itself. The notifications carry the tab number and the page URL from the extension to the app, inside the app (the dashboard refresh already does); neither enters a pixel or a log. Privacy Triage is to be requested once for both designs.
Security: no new private WebKit selectors. The new message is read-only: the app parses a step name and a tab and answers with nothing.
Site Breakage: no intended change for pages.
Experimentation: none. Rollout is by release channel, internal builds first.
Operational: the app side lands first, the extension after its review and the usual update of the embedded files. After the first public release with notifications, look at last_stage by platform and wake state over the breakage count and pick the fix from the table above. The notifications and last_stage are investigation tools: remove them when the wake group has a fix or is closed. Not in scope: per-step durations and the background's own lifecycle metrics.
Localization / Internationalization: none.
Open Questions
- Seven notifications per page or fewer: are
config_readandsite_checkedworth it for the first read? - Send
page_init_sentonly in the embedded build, or accept the extra message everywhere? - Is a switch inside the extension needed, or is release-channel rollout enough?
- Who reviews the extension change, and can the update of the embedded files be timed with the app release that carries the app side?