diff --git a/work/projects/cpm-permanent-breakage-metric-tech-design-2026-11-10.md b/work/projects/cpm-permanent-breakage-metric-tech-design-2026-11-10.md index dec451ce..4a22ff39 100644 --- a/work/projects/cpm-permanent-breakage-metric-tech-design-2026-11-10.md +++ b/work/projects/cpm-permanent-breakage-metric-tech-design-2026-11-10.md @@ -4,10 +4,11 @@ tags: - cpm - tech-design - pixel -updated: '2026-11-11' + - web-extension +updated: '2026-11-12' status: draft --- -# TD: Pixel Definition: CPM Permanent Breakage Metric (macOS/iOS) +# TD: Pixel Definition: CPM Permanent Breakage Metric (macOS/iOS + embedded web extension) **Author:** Alex M **Reviewer:** TBD @@ -22,35 +23,30 @@ We need a measure that does not depend on a navigation timer, can be expressed a ## Problem Statement -Find the tabs whose pages never get a message from the extension, and tell a broken page from a broken extension. +Rework the health monitor so that it finds the tabs whose pages never get a message from the extension, and tells a broken page from a broken extension. That requires knowing which tab of the app each message belongs to, and the extension names a tab only by an id that WebKit assigns and the app cannot see. ## Recommended Approach -1. While a page initializes, the extension already sends the app a message from that tab. The extension does not change. -2. When a page loads that should get such a message, the app starts a 30-second wait. The page counts if it is http(s), the extension is loaded, and it is not the browser's error page, a back/forward navigation or a restored session. If no message arrives in 30 seconds, the tab is **silent**. A message that arrives later still counts as an answer. Time the app spends suspended is not counted. -3. There is no timer. The app checks for expired waits whenever another event comes in: a page loads, a tab closes, a message arrives, the app goes to the background. -4. A message carries the extension's own tab number and the page URL, not the app's tab identifier. The app learns the number from the first message whose URL (host and path) matches exactly one open tab. A message that matches several tabs counts as an answer for all of them [1]. -5. Two pixels: +1. **Detection.** While a page initializes, the extension sends the app a message from that tab. When a page loads that should get such a message, the app waits 30 seconds. The page counts if it is http(s), the extension is loaded, and it is not the browser's error page, a back/forward navigation or a restored session. If no message arrives, the tab is **silent**. A message that arrives later still counts as an answer; a message saying that the extension cannot answer (no configuration or settings) does not. Time the app spends suspended is not counted. There is no timer: the app checks for expired waits whenever another event comes in (a page loads, a tab closes, a message arrives, the app goes to the background). +2. **Matching problem.** A message carries the extension's tab id and the page URL. The app cannot see the tab id, so it can only match the message to a tab by the URL (host and path). Two tabs on the same page cannot be told apart: the message counts as an answer for both, and a silent tab hides behind a working one. +3. **Solution.** The extension adds to each message the tab's index in its window, the id of that window and the ids of all windows, as the browser reports them. The app numbers its windows in the order it hands them to WebKit, and WebKit gives each window its id from a growing counter in that same order. So the k-th smallest of the window ids is the k-th window of the app, and the tab is the one at the index in that window. The app remembers which tab the extension's tab id belongs to until the tab closes or the extension reloads. If the app cannot tell the window (an older extension, or the app and the extension count different numbers of windows), it matches by the URL as before. +4. Two pixels: | Pixel | When it is sent | Frequency | |---|---|---| | `web_extension_cpm_breakage` | A tab becomes silent. Parameter `tabs`: `one` if no other tab was silent since the extension last sent a message (the page is the likely cause), `many` if another one was (the extension is the likely cause). | Daily + count | | `web_extension_cpm_breakage_persistent` | A second tab becomes silent and the extension sent no message in between. Once per such series. | Daily + count | -6. Remove `web_extension_cpm_episode`, `web_extension_cpm_background_load_restart` and `web_extension_cpm_network_process_recovery_applied`, together with the code that only fed them. The new pixels must ship in the same release, otherwise there is no CPM breakage pixel at all. -7. The success metric is the number of installs per day with the persistent pixel, divided by DAU. - -## Notes - -[1] Matching by URL is a guess: two tabs on the same host and path cannot be told apart, and in rare cases a message is credited to the wrong tab. An alternative is to have the extension send its tab index, which the app knows for every tab; that needs an extension change. +5. Remove `web_extension_cpm_episode`, `web_extension_cpm_background_load_restart` and `web_extension_cpm_network_process_recovery_applied`, together with the code that only fed them. The new pixels must ship in the same release, otherwise there is no CPM breakage pixel at all. +6. The success metric is the number of installs per day with the persistent pixel, divided by DAU. ## Testing -Unit tests for each rule above. +Unit tests for each rule above, including windows and tabs that are opened, closed, reordered, pinned and moved between windows. ## Additional Considerations (if applicable) -- **Privacy:** Two new pixels. Only `web_extension_cpm_breakage` has a parameter, `tabs`: `one` or `many`. +- **Privacy:** Two new pixels. Only `web_extension_cpm_breakage` has a parameter, `tabs`: `one` or `many`. The tab index and window ids go only from the extension to the app on the device, never into a pixel. - **Security:** None. - **Site Breakage:** None. - **Experimentation:** None.