Files
obsidian-vault/work/projects/cpm-wake-init-detection-tech-design-2026-11-10.md
T

7.9 KiB

tags, updated, status
tags updated status
work
cpm
tech-design
pixel
web-extension
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.

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_stage mix with and without notifications, and drop config_read and site_checked if the cost shows.
  • response_sent can 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 how last_stage=response_sent is 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_sent and 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_stage for an injected stall at each step, and a page without the script not yielding response_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

  1. Seven notifications per page or fewer: are config_read and site_checked worth it for the first read?
  2. Send page_init_sent only in the embedded build, or accept the extra message everywhere?
  3. Is a switch inside the extension needed, or is release-channel rollout enough?
  4. Who reviews the extension change, and can the update of the embedded files be timed with the app release that carries the app side?