[2026-11-11] eagle: work/projects/cpm-permanent-breakage-metric-tech-design-2026-11-10.md work/projects/cpm-wake-init-detection-tech-design-2026-11-10.md
This commit is contained in:
1 parent
006ded56a8
commit
37fd8acc3b
2 files changed
+36
-64
No files matched your search
@@ -16,49 +16,39 @@ status: draft
|
||||
|
||||
## Background & Requirements
|
||||
|
||||
The goal is permanent CPM failures below 1% of DAU, but no pixel measures it. The number is derived from `web_extension_cpm_episode`, which is a poor basis: a four-second navigation timer opens an episode (on iOS 7.244, 6.0% of episodes closed with a late answer on the same page), and an open episode is lost when the app exits.
|
||||
Cookie pop-up management (CPM) runs in an embedded web extension. It can stop answering and stay broken even after page reloads and new tabs. Our goal is to keep these permanent failures below 1% of daily active users, but nothing measures it directly: the number is derived from the `web_extension_cpm_episode` pixel, and that is a weak basis. An episode starts after a four-second navigation timeout, so a slow but correct answer counts as a failure, and an episode still open when the app closes is lost.
|
||||
|
||||
Requirements: no navigation timer, a rate against DAU, no URLs, page content or persistent identifiers.
|
||||
We need a measure that does not depend on a navigation timer, can be expressed as a share of daily users, and collects no URLs, page content or persistent identifiers.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Report the tabs whose pages never hear from the extension, and tell one broken page from a broken extension.
|
||||
Find the tabs whose pages never get a message from the extension, and tell a broken page from a broken extension.
|
||||
|
||||
## Recommended Approach
|
||||
|
||||
1. The extension already sends the app a message from a tab while a page initializes. A tab is **silent** when it loaded a page that should get a message and heard nothing for 30 s of app time; a late message still counts as an answer. There is no timer: the check runs when another event arrives (page load, tab close, message, app to background or wake) [1].
|
||||
2. Pages that count: http(s), extension loaded, not an error page, not a back/forward restore, not a restored session. Suspended time is not counted.
|
||||
3. A message carries the extension's tab number and the page URL. The app learns which tab it is from the first message that matches exactly one open page (host and path). A message it cannot place answers every tab it could belong to [2].
|
||||
4. Two pixels, no expiry date (permanent metric):
|
||||
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 names the extension's own tab number and the page URL, not our tab. 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:
|
||||
|
||||
| Pixel | Trigger | Frequency |
|
||||
| Pixel | When it is sent | Frequency |
|
||||
|---|---|---|
|
||||
| `web_extension_cpm_breakage` | A tab is silent. Parameter `tabs`: `one` (no other tab was silent since the extension last answered: likely the page) or `many` (another tab was too: likely the extension). | Daily + count |
|
||||
| `web_extension_cpm_breakage_persistent` | A second tab is silent and the extension answered nothing in between. Once per series. | Daily + count |
|
||||
| `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 |
|
||||
|
||||
5. Remove `web_extension_cpm_episode`, `web_extension_cpm_background_load_restart` and `web_extension_cpm_network_process_recovery_applied` with the code that only fed them, in the same release. Success metric: installs with the persistent pixel / DAU.
|
||||
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] A silent tab left open until the app exits is not reported (iOS reports it when going to the background); a page left within 30 s is not reported.
|
||||
|
||||
[2] The mapping is a heuristic: tabs on the same host and path hide each other, and rarely a working extension is reported. An alternative is to have the extension send its tab index; it needs an extension change.
|
||||
[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.
|
||||
|
||||
## Testing
|
||||
|
||||
Unit tests for each rule, pixel definition validators, app-hosted macOS and iOS tests. Not done: a real-WebKit run.
|
||||
Unit tests for each rule above, pixel definition validators and app-hosted macOS and iOS tests. Not done yet: a run against real WebKit.
|
||||
|
||||
## Additional Considerations
|
||||
## Additional Considerations (if applicable)
|
||||
|
||||
1. Privacy
|
||||
1. Two new pixel names, one enumerated parameter, no URLs or identifiers. Privacy Triage not filed yet.
|
||||
2. Security
|
||||
1. No new private WebKit selectors.
|
||||
3. Site Breakage
|
||||
1. None.
|
||||
4. Experimentation
|
||||
1. None.
|
||||
5. Operational
|
||||
1. No new infrastructure.
|
||||
6. Localization / Internationalization
|
||||
1. Not affected.
|
||||
1. Two new pixel names and one enumerated parameter; no URLs, page content or identifiers. Privacy Triage is not filed yet.
|
||||
@@ -17,58 +17,40 @@ status: draft
|
||||
|
||||
## Background & Requirements
|
||||
|
||||
Failures after a background wake are the largest group with no fix: on macOS 1.214, 39% of all failures (0.68% of DAU); on iOS 7.244, 22% (0.06% of DAU).
|
||||
The extension's background is unloaded when idle and woken when a page needs it. Failures after such a wake are the largest group of CPM failures with no fix: on macOS 1.214 they are 39% of all failures (0.68% of DAU), on iOS 7.244 22% (0.06% of DAU).
|
||||
|
||||
For one page, startup is: page script sends `init` → background receives it → reads configuration → reads the user setting → checks the site → answers the page and refreshes the app's dashboard. The app sees only the last step, so every failure looks the same: no answer.
|
||||
For one page, startup has these steps: the page script sends `init`; the background receives it; it reads the configuration; it reads the user setting from the app; it checks the site; it answers the page and sends the app a dashboard refresh. The app sees only the last step, so every failure looks the same: no answer.
|
||||
|
||||
Requirements: find where the chain stopped, without changing what the extension does for the page or collecting URLs or persistent identifiers.
|
||||
We need to find where startup stops, without changing what the extension does for the page and without collecting URLs or persistent identifiers.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
For every tab the permanent breakage metric reports as silent, record the furthest init step the extension reported.
|
||||
For every tab that the permanent breakage metric reports as silent, record the furthest startup step the extension reported.
|
||||
|
||||
## Recommended Approach
|
||||
|
||||
1. The extension sends the app a fire-and-forget notification at each init step ([PR #3727](https://github.com/duckduckgo/duckduckgo-privacy-extension/pull/3727)). It never waits, throws or changes what the page receives.
|
||||
2. The app keeps the furthest step per tab, for its current page only. A notification does not count as the extension answering: the tab is still reported silent unless the dashboard refresh arrives.
|
||||
3. `web_extension_cpm_breakage` gets one parameter, `last_stage` [1]:
|
||||
1. After each step the extension sends the app a short notification ([extension PR #3727](https://github.com/duckduckgo/duckduckgo-privacy-extension/pull/3727)). It does not wait for the app's reply.
|
||||
2. For each tab the app remembers the furthest step reached.
|
||||
3. When a tab is reported as silent, `web_extension_cpm_breakage` carries that step as a new parameter, `last_stage`:
|
||||
|
||||
| `last_stage` | The chain stopped |
|
||||
| `last_stage` | What the app saw last |
|
||||
|---|---|
|
||||
| `none` | nothing reached the app [3] |
|
||||
| `page_init_sent` | the page script sent `init`; the background never processed it |
|
||||
| `received` | `init` arrived; 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 arrived |
|
||||
| `response_sent` | the answer was sent to the page, but no dashboard refresh followed [2] |
|
||||
| `none` | no notification (the background may not have run at all) |
|
||||
| `page_init_sent` | the page script sent `init`, the background never processed it |
|
||||
| `received` | the background got `init`, the configuration or storage read did not finish |
|
||||
| `config_read` | the configuration was read, the user setting did not arrive |
|
||||
| `settings_checked` | the user setting arrived, the site check did not finish |
|
||||
| `site_checked` | all checks finished, no answer reached the app |
|
||||
| `response_sent` | the background finished sending the answer to the page, no dashboard refresh followed |
|
||||
| `response_failed` | sending the answer to the page failed |
|
||||
|
||||
4. The extension ships inside the app, so the app side can ship first. The notifications and `last_stage` are removed when the wake group has a fix or is closed.
|
||||
|
||||
## Notes
|
||||
|
||||
[1] Each stage follows a separate wait in the extension's `init` handling: the cached configuration, the user setting and the site check (the last two are native round trips). A stall at each has a different cause and fix.
|
||||
|
||||
[2] WebKit can report a send as done when nobody was listening, so `response_sent` does not prove the page got the answer.
|
||||
|
||||
[3] Notifications pass through the background we study, so the earliest failures end as `none`.
|
||||
4. These notifications and the parameter are temporary and are removed when the wake failures are fixed or the investigation is closed.
|
||||
|
||||
## Testing
|
||||
|
||||
Unit tests in the app and in the extension. Not done: a real-WebKit run showing the right `last_stage` for a stall at each step.
|
||||
Unit tests in the app and in the extension. Not done yet: a run against real WebKit that shows the right `last_stage` when startup is stalled at each step.
|
||||
|
||||
## Additional Considerations
|
||||
## Additional Considerations (if applicable)
|
||||
|
||||
1. Privacy
|
||||
1. One new enumerated parameter. Tab number and URL stay inside the app. Privacy Triage not filed yet.
|
||||
2. Security
|
||||
1. No new private WebKit selectors; the new message is read-only.
|
||||
3. Site Breakage
|
||||
1. No intended change.
|
||||
4. Experimentation
|
||||
1. None.
|
||||
5. Operational
|
||||
1. First data comes about two release cycles after the work, because the extension ships with the app.
|
||||
6. Localization / Internationalization
|
||||
1. Not affected.
|
||||
1. One new enumerated parameter. The tab number and URL stay inside the app. Privacy Triage is not filed yet.
|
||||
Reference in new issue
Block a user