[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
|
## 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
|
## 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
|
## 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].
|
1. While a page initializes, the extension already sends the app a message from that tab. The extension does not change.
|
||||||
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.
|
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. 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].
|
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. Two pixels, no expiry date (permanent metric):
|
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` | 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 is silent and the extension answered nothing in between. Once per series. | 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
|
## 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.
|
[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.
|
||||||
|
|
||||||
[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.
|
|
||||||
|
|
||||||
## Testing
|
## 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. Privacy
|
||||||
1. Two new pixel names, one enumerated parameter, no URLs or identifiers. Privacy Triage not filed yet.
|
1. Two new pixel names and one enumerated parameter; no URLs, page content or identifiers. Privacy Triage is 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.
|
|
||||||
@@ -17,58 +17,40 @@ status: draft
|
|||||||
|
|
||||||
## Background & Requirements
|
## 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
|
## 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
|
## 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.
|
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. 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.
|
2. For each tab the app remembers the furthest step reached.
|
||||||
3. `web_extension_cpm_breakage` gets one parameter, `last_stage` [1]:
|
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] |
|
| `none` | no notification (the background may not have run at all) |
|
||||||
| `page_init_sent` | the page script sent `init`; the background never processed it |
|
| `page_init_sent` | the page script sent `init`, the background never processed it |
|
||||||
| `received` | `init` arrived; configuration or storage read did not finish |
|
| `received` | the background got `init`, the configuration or storage read did not finish |
|
||||||
| `config_read` | waiting for the user setting from the app |
|
| `config_read` | the configuration was read, the user setting did not arrive |
|
||||||
| `settings_checked` | waiting for the site check |
|
| `settings_checked` | the user setting arrived, the site check did not finish |
|
||||||
| `site_checked` | all checks passed; no answer arrived |
|
| `site_checked` | all checks finished, no answer reached the app |
|
||||||
| `response_sent` | the answer was sent to the page, but no dashboard refresh followed [2] |
|
| `response_sent` | the background finished sending the answer to the page, no dashboard refresh followed |
|
||||||
| `response_failed` | sending the answer to the page failed |
|
| `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.
|
4. These notifications and the parameter are temporary and are removed when the wake failures are fixed or the investigation 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`.
|
|
||||||
|
|
||||||
## Testing
|
## 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. Privacy
|
||||||
1. One new enumerated parameter. Tab number and URL stay inside the app. Privacy Triage not filed yet.
|
1. One new enumerated parameter. The tab number and URL stay inside the app. Privacy Triage is 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.
|
|
||||||
Reference in new issue
Block a user