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 3a010499..428f9150 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 @@ -9,129 +9,195 @@ status: draft --- # TD: Pixel Definition: CPM Permanent Breakage Metric (macOS/iOS) -> Internal metrics, keep out of public PRs. Companion: [[cpm-wake-init-detection-tech-design-2026-11-10]] (adds `last_stage`). Where we are: [[cpm-project-summary-2026-11-03]]. +> Internal metrics, keep out of public PRs. Companion: [[cpm-wake-init-detection-tech-design-2026-11-10]] (adds `last_stage` to the pixel defined here). Where we are: [[cpm-project-summary-2026-11-03]] (section "Состояние на 10 ноября"). Review of the earlier attempt that this design replaces: same section. **Author:** Alex M **Reviewer:** TBD (previous CPM pixel designs: Michal) **Stakeholders:** Michal, Konrad, Russell (from the previous Tech Designs; confirm) **Project:** [CPM: root-cause the stuck embedded extension messaging (macOS/iOS)](https://app.asana.com/1/137249556945/task/1216761517055116) -**Task:** to create ("Permanent breakage metrics") -**Previous Tech Designs:** [[cpm-readiness-telemetry-tech-design-2026-10-15]], [[cpm-m3-telemetry-privacy-triage]] -**Privacy Triage:** to request (new parameters on an existing pixel) +**Task:** to create ("Permanent breakage metrics", from the plan in [[cpm-project-summary-2026-11-03]]) +**Previous Tech Designs:** [[cpm-readiness-telemetry-tech-design-2026-10-15]], [[cpm-m3-telemetry-privacy-triage]] (both describe pixels this design supersedes) +**Privacy Triage:** to request (new pixel names, see Additional Considerations) ## Background & Requirements -The goal is permanent CPM failures below 1% of DAU. There is no pixel for it; the number is derived from `web_extension_cpm_episode` with a long SQL. The episode is wrong in two ways: +The project goal is permanent CPM failures below 1% of DAU (0.2–0.5% counts as success). Today that number does not exist as a pixel. It is derived from `web_extension_cpm_episode` with a long SQL: a four-second navigation timeout opens an episode, a later timeout or reload confirms it, a five-minute deadline or a response closes it. Three problems: -- **A timer decides.** A four-second navigation timeout opens an episode. On iOS 7.244, 6.0% of episodes closed with a late answer on the same page, 42% of those later than 30 s. Slow answers count as failures. -- **Episodes are lost.** An open episode lives in memory and is dropped when the app exits. +- **The timer measures slowness, not breakage.** On iOS 7.244, 6.0% of episodes closed with a late answer on the same page, 42% of those later than 30 s. A page that answers after 40 s is counted as a failure. +- **Episodes are lost.** An unfinished episode lives in memory; closing or killing the app drops it. The pages that stay broken longest are the ones that end with the app. +- **The pixel carries what the investigation needed** (diagnostic booleans, `bg_events`, load fields) and is being trimmed to five parameters (PR 2 in the stack below). What remains cannot be turned into a small, fixed dashboard set. -Requirements: a metric that does not depend on a timer, as a rate against DAU; no new pixel names; no page content, URLs or persistent identifiers. +Requirements: + +- A small fixed set of metrics, as rates against DAU, for the health indicators dashboard. +- Detection that does not depend on a timer and does not depend on reaching a deadline before the app exits. +- No page content, URLs or persistent identifiers. +- Runs next to the episode pixel for one release, so the two series can be compared before the old one is removed. ## Problem Statement -Change what opens and closes a `web_extension_cpm_episode` so that it reports pages that really never got the extension's answer, and so that permanent breakage can be read from its parameters without a long derivation. +Define and measure, per platform and version: + +1. **Breakage:** an http(s) page that finished loading with the extension loaded, and never got the extension's answer while its document lived. +2. **Persistent breakage:** several documents in a row with no answer in between, meaning the extension is broken for the user, not for one page. +3. How many installs per day are affected, and how many breakages happen. + +The wake investigation (which init step stops) is separate: [[cpm-wake-init-detection-tech-design-2026-11-10]]. ## Recommended Approach -Keep the pixel: name, `dailyAndCount` frequency, metadata on both platforms and the parameters `reason`, `tabs_affected`, `repeat_failures`, `reload_state`. Change the detection under it. +A page is **healthy** as soon as an answer for its document arrives, at any time while the document lives (a late answer is not a failure). A page that never got the answer is **judged when its document ends**. There are no navigation timers. The approach is additive: the existing monitor, its timers and the episode pixel stay unchanged; the new logic is a second consumer of the same events. -**A page is healthy when an answer reaches it at any time while its document lives. A page that never got an answer is judged when its document ends. There is no navigation timer.** +### What was wrong with the first attempt + +The first attempt (branch `alex/cpm-breakage-pixel`, commit `e6419bb910`) already used page-end judging, but combined it with deleting the episode pixel and had these defects, all addressed below: + +1. Unanswered measurements older than five minutes were dropped, so the longest-silent pages were never reported. +2. No minimum lifetime: a page left 200 ms after loading counted as broken. +3. A web content process crash counted as extension breakage. +4. `background` was derived from "background load took at least one second" and from one app-wide signal, not from the page's navigation. +5. Pages alive when the app exits were never judged. +6. The macOS pixel definition omitted `pixelSource` and `channel` and used iOS-only suffixes. +7. `navigationWaitContexts` leaked an entry when the context was replaced, and `cpmStage` parsing had no test. + +### Detection rules + +Per eligible document: http(s), not a browser error page, not a history restore from cache (existing eligibility rules, unchanged). | Event | Effect | |---|---| -| Navigation finished, extension loaded | The document becomes judgeable; its clock starts. If the extension loads later, it becomes judgeable then. | -| Attributed answer (dashboard refresh; `response_sent` in the companion design) | Healthy. If an episode is open, it closes. | -| Document ends: new navigation, tab closed | If unanswered and alive at least **30 s**: a failure. It opens the episode, or adds to the open one. | -| App suspends (iOS background, macOS sleep) or macOS terminates | Same judgement for every pending judgeable document, once; an open episode closes and is sent. Nothing is judged while suspended. | -| Web content process terminated | Not judged. The tab's next navigation gets `reason=tab_crash`. | -| Extension signals `config_unavailable` or `settings_missing` | Failure at once, no lifetime limit. | -| Answer that cannot be tied to one document | That document is not judged. | +| Navigation finished, extension loaded | Document becomes **judgeable** and its clock starts. If the extension is not loaded yet, it becomes judgeable when the extension loads while the page lives. | +| Attributed dashboard response (later also `response_sent`, see the companion design) | Healthy. Ends judging for this document. Resets the streak. | +| Response that cannot be tied to one document | The document is not judgeable (neither outcome provable). | +| Document ends by a new navigation or tab close | Judge now. | +| App suspends (iOS to background, macOS sleep) or macOS terminates | Judge every pending judgeable document now, once. The processes still run at this moment; nothing is judged while suspended. | +| Web content process terminated | **Not judged.** The silence is the crash's, not the extension's. The tab's next navigation is reported with `reason=tab_crash`. | +| Extension signals `cpmStage` = `config_unavailable` or `settings_missing` | Breakage at once (`ended_by=explicit_signal`), no lifetime limit. | +| Extension reload | Existing rule: evidence from the replaced context is discarded. | -The episode keeps its shape: it opens on the first failure, collects `tabs_affected` and `repeat_failures` for further failures, and closes on the first healthy answer or at suspension. The five-minute deadline is dropped. A failure never retracts. Permanent breakage is an episode with `tabs_affected` and `repeat_failures` above a threshold, read from the parameters as today. +A document is reported only if it lived at least **30 s** after it became judgeable (`minimum lifetime`). A shorter silence is ambiguous: it may be a slow wake. The 30 s value follows WebKit's unload timer and the late-answer data above; it is a constant in the first release. Shorter and longer lifetimes are told apart in analysis by the `lifetime` parameter. -### Pixel changes +A judged document is never judged again and never retracted: an answer after the report changes nothing. Documents whose process or app ends without a suspension event (kill, crash of the app) are lost; this is the known bias, now limited to the time between the last suspension and the kill. -`web_extension_cpm_episode` (existing; trimmed to five parameters by PR 2): +Measurement state does not expire after five minutes. The old five-minute lifetime stays only for the restart observation, which is unrelated. -| Parameter | Change | +### Persistence + +The monitor keeps a streak: the number of consecutive judged-broken documents since the last healthy one (any tab). When it reaches **3** the persistent pixel fires once for that streak. A healthy answer resets the streak. The streak lives in memory for the process lifetime. A relaunch resets it (see open questions). + +`tabs` on the persistent pixel records whether the streak's documents were in one tab or several. This keeps the cross-tab distinction the earlier analysis found important (cross-tab permanent was the figure used against the 1% target). + +### Pixels + +| Pixel | Trigger | Frequency | +|---|---|---| +| `web_extension_cpm_breakage` | A document is judged broken. Sent at that moment, once per document. | Daily + count (`dailyAndCount`) | +| `web_extension_cpm_breakage_persistent` | A streak reaches 3 documents. Once per streak. | Daily + count (`dailyAndCount`) | + +`_daily` estimates installs per day, `_count` counts events. Both metadata sets follow the existing episode pixel on each platform: `appVersion` on both; macOS also `pixelSource` and `channel`, suffix `daily_count`; iOS keeps its existing suffix set. The macOS definition must match what is sent (item 6 above). + +`web_extension_cpm_breakage` parameters: + +| Parameter | Values | |---|---| -| `reason`, `tabs_affected`, `repeat_failures`, `reload_state` | Unchanged values. They now count judged documents, not timeouts. | -| `outcome` | Values become `answered` (a later document got an answer) and `unanswered` (the app suspended with the episode open). The timer-based values are removed. | -| `background` | New: `woken` (a background view was created during the navigation), `warm`, `unknown`. From `didCreateBackgroundWebView`, correlated to the navigation, not from load duration. | -| `last_stage` | New, in the companion design. | +| `reason` | `session_restoration`, `tab_crash`, `extension_reload`, `other`. Context of the document's navigation. Same values as the episode pixel. | +| `background` | `woken` (the extension's background view was created during this navigation), `warm` (a background view existed and none was created), `unknown` (no background view known). Comes from the manager's `didCreateBackgroundWebView` delegate callback, correlated to the navigation, not from load duration. | +| `ended_by` | `navigation`, `tab_closed`, `app_suspended`, `explicit_signal`. | +| `lifetime` | Time from judgeable to judged: `30s_to_2m`, `2m_to_10m`, `over_10m`. Omitted for `explicit_signal`. | +| `streak` | Breakages in a row including this one: `1`, `2`, `3_plus`. | -The macOS definition must match what is sent: it needs `pixelSource` and `channel` and the `daily_count` suffix, not the iOS suffixes (the first attempt got this wrong). +`web_extension_cpm_breakage_persistent` parameters: -### Code +| Parameter | Values | +|---|---| +| `tabs` | `one` or `many`: whether the streak's documents were in one tab or several. | -- `CPMMessagingHealthMonitor`: the episode is opened by document-end judgement instead of the timeout path. A per-document record (judgeable since, judged) is independent of the timeout measurement. Existing URL and tab correlation is reused. -- New event `backgroundViewCreated`, sent by the manager from `didCreateBackgroundWebView`. -- macOS sends the existing suspension event on termination too. -- Test for `cpmStage` parsing in `AutoconsentWebExtensionMessageHandler`; fix the `navigationWaitContexts` leak when a context is replaced. +The `last_stage` parameter for wake diagnosis is added by [[cpm-wake-init-detection-tech-design-2026-11-10]], not here. No URL, document, tab or process identifier is sent. -### What the first attempt got wrong (`e6419bb910`) +### The metric set for the dashboard -Unanswered measurements older than five minutes were dropped; no minimum lifetime; a tab process crash counted as breakage; `background` from "load took a second" and one app-wide signal; pages alive at app exit never judged; macOS definition mismatch; `navigationWaitContexts` leak; no `cpmStage` test. It also deleted the episode pixel together with the cleanup. +| Metric | Source | Meaning | +|---|---|---| +| Installs with persistent breakage / DAU | `breakage_persistent_daily`, split by `tabs` | The success metric (target below 1%). | +| Installs with any breakage / DAU | `breakage_daily` | Upper bound; includes single-page failures. | +| Breakages per day | `breakage_count`, split by `reason`, `background`, `ended_by` | Volume and mix, for diagnosis. | + +Everything is per platform and app version. Rates use the existing DAU series. + +### Where it lives in the code + +- `CPMMessagingHealthMonitor` (existing, per-tab document state already tracks response, judgeable and crash state) gains a per-document record independent of the timeout measurement: when it became judgeable, whether it was judged, the navigation kind. Judging and the streak are small additions to existing handlers; the correlation logic is reused, not duplicated. +- New event `CPMMessagingHealthEvent.backgroundViewCreated`, sent by the manager from `didCreateBackgroundWebView`. It replaces `backgroundLoaded(wasWoken:)` from the first attempt. +- macOS also sends the existing suspension event on termination (`applicationWillTerminate`); iOS and macOS sleep already send `interruptionBegan`. +- `AutoconsentWebExtensionMessageHandler` already separates the `cpmStage` signals; they get a test. +- `CPMMessagingBreakageReport` and the two pixel events in `WebExtensionPixelFiring`. Definitions in both platforms' `web_extensions.json5` and `params_dictionary.json5`. ### PR stack -| PR | Content | State, 10 Nov | -|---|---|---| -| 1, [#8025](https://github.com/duckduckgo/apple-browsers/pull/8025) | Remove ineffective recovery fixes and flags | pushed, in review | -| 2, `alex/cpm-pixel-cleanup` (`f04c93730f`) | Remove diagnostics, trim the episode pixel. No detection change. Danger label. | local; 1104 package tests pass; not pushed; app builds and pixel validation not run | -| 3, new | This design | not started | -| 4, new | Marks and `last_stage` ([[cpm-wake-init-detection-tech-design-2026-11-10]]) plus extension #3727 | native part drafted in `alex/cpm-init-diagnostics` | +| PR | Branch | Content | State, 10 Nov | +|---|---|---|---| +| 1 | `alex/cpm-episode-metrics`, [#8025](https://github.com/duckduckgo/apple-browsers/pull/8025) | Remove ineffective recovery fixes, their flags and pixels | pushed, in review | +| 2 | `alex/cpm-pixel-cleanup` (`f04c93730f`) | Remove the diagnostics, trim the episode pixel to five parameters. No new pixel, no detection change. Needs the Danger label (existing pixel parameters change). | local; 1104 package tests pass, SwiftLint clean on the changed core files; app builds and pixel validation not run; not pushed | +| 3 | new, on top of 2 | This design: two pixels, document-end judging, streak | not started | +| 4 | new, on top of 3 | Init progress marks, `last_stage` ([[cpm-wake-init-detection-tech-design-2026-11-10]]) plus extension [#3727](https://github.com/duckduckgo/duckduckgo-privacy-extension/pull/3727) | native part drafted in `alex/cpm-init-diagnostics` (`adb28e643d`), needs the same fixes | +| 5 | later | Remove the episode pixel and its timers and states, one release after PR 3 | after comparison | + +Old branches `alex/cpm-breakage-pixel` and `alex/cpm-init-diagnostics` stay as sources only. ### Pros -- No new pixel, no new definition to approve; dashboards keep one series. -- A late answer is never a failure. -- Permanent breakage is read from the existing parameters. +- Measures the user-facing fact (this page never got an answer), not an elapsed time. +- A late answer is never a failure, which removes the 6% late-answer error class and its tail. +- Detected breakages are sent at once, so closing the app loses only documents that were never judged. +- Persistent breakage has its own daily pixel: the success metric is one series, not a join. +- Additive: the episode pixel and the recovery logic keep working, so the new numbers can be validated against the old. ### Cons -- The series changes meaning at the version that ships this. Compare by app version; the episode counts before and after are not the same measure. -- An episode is sent at close, so an app killed without suspending still loses the open one. -- 30 s hides breakage on short pages. +- Pages that live until a kill are not judged. Persistent breakage that a user never ends (leaves the page open for hours) appears only at suspension. +- 30 s minimum lifetime hides breakage on short pages. If most breakage is on short pages the metric underestimates; `lifetime` shows how much sits near the limit. +- A page-based streak does not equal the old "no answer for five minutes". The two series will differ, and the comparison needs interpretation. +- The streak resets on relaunch; iOS terminates apps often, so persistent breakage across launches is undercounted. ### Risks -- Healthy evidence missing in a working page (attribution gaps: `other_tab_response` was 37–53% of outcomes) counts as a failure. Documents with an unattributable answer are not judged. -- Judging at suspension reports silence of at least 30 s before suspension; a wake begun just before could be reported. -- The Debug "Simulate CPM Messaging Breakage" switch must keep hiding answers from the monitor. +- **Pixel volume.** A user with persistent breakage fires one count request per page. Mitigation: soft cap of 20 `breakage` count requests per process; the persistent pixel is unaffected by the cap. Volume to be estimated from `web_extension_cpm_episode` counts before release. +- **Healthy evidence that is missing in a working page** (dashboard refresh not delivered, wrong attribution). Counted as breakage. The earlier analysis found `other_tab_response` at 37–53% of outcomes, an attribution problem. Mitigation: documents with an unattributable response are not judged, and `unknown` attribution is visible only locally for now. Compare with the episode series. +- **Judging at suspension** assumes silence for 30 s before suspension means breakage. A wake that began just before suspension could be reported. Mitigation: the lifetime limit; the suspension case is separated by `ended_by`. +- **Simulation switch.** The Debug "Simulate CPM Messaging Breakage" switch hides dashboard responses from the monitor and must keep doing so for both detectors. ## Testing -- Unit tests per row of the table: late answer is healthy; unanswered under 30 s is not a failure; unanswered over 30 s on navigation, tab close, suspension; process termination not judged and the next navigation tagged `tab_crash`; judged once; extension not loaded at finish; unattributable answer; explicit signals; `tabs_affected` and `repeat_failures` counts; `outcome` values; `background` inside and outside the navigation window; simulation switch. -- `cpmStage` parsing test; `navigationWaitContexts` test. -- Pixel definitions pass both platforms' validators; parameter lists match what is sent. -- Manual: [[CPM Breakage Testing Steps]] updated for the Debug switch. -- Real WebKit: the CPM repro harness ([PR #7378](https://github.com/duckduckgo/apple-browsers/pull/7378)) with injected error 6 and idle wake. Not run yet. +- Unit tests for each rule in the table: late answer before document end is healthy; unanswered document shorter than 30 s is not reported and does not touch the streak; unanswered longer document is reported on navigation, tab close, suspension; process termination is not judged and tags the next navigation; judged once; extension not loaded at finish becomes judgeable on load; unattributable response; explicit signals; streak reaches 3 once, resets on a healthy answer; `tabs` value; `background` from the view-created event inside and outside the navigation window; simulation switch hides answers; extension reload evidence. +- Handler test for `cpmStage` parsing; test for `navigationWaitContexts` after a context replacement. +- Pixel definitions: schema validators of both platforms in CI; parameter lists match what is sent (compare the macOS definition against the event). +- Manual: updated steps in [[CPM Breakage Testing Steps]] (Debug simulation switch: load a page for 35 s without an answer, navigate away, expect one `breakage`; three in a row, expect `persistent`). +- Real WebKit: the CPM repro harness ([PR #7378](https://github.com/duckduckgo/apple-browsers/pull/7378)) with the injected error 6 and idle-wake fixtures to see both pixels with the expected values. Not run yet. ## Additional Considerations (if applicable) **Privacy** -New parameters (`background`, `last_stage`) and new `outcome` values on an existing pixel; enumerated, no URLs, identifiers or page content. Privacy Triage is required for new parameters on an existing pixel. Not requested yet. +Two new pixel names with enumerated, bucketed parameters. No URLs, page content, tab or document identifiers, or process IDs. The streak is a local integer that is never sent; only `streak` as a bucket. Because the pixel names are new, Privacy Triage is required before release. Not requested yet. Combined distinguishability of `reason`, `background`, `ended_by`, `lifetime`, `streak` and the platform metadata is small (five low-cardinality enums). **Security** -No new private WebKit selectors; `didCreateBackgroundWebView` is already implemented in the manager. +No new private WebKit selectors. `didCreateBackgroundWebView` is already implemented in the manager. **Site Breakage** -None. The change only observes events; recovery is unchanged. +None intended. The detector only observes events; it sends no messages and changes no recovery behavior. Existing recovery (error-6 restart, network recovery, upgrade before load) is unchanged. **Experimentation** -None. Comparison is across app versions. +No experiment. The comparison is observational: the new and old series for the same versions during the overlap release. **Operational** -- Danger label (parameters of an existing pixel change). Pixel expiry follows the existing definition (2027-01-15). -- Dashboard: installs with a permanent episode / DAU from `_daily`, per platform and version; episodes per day from `_count`, split by `reason` and `background`. -- Data checks: `metrics.pixels_aggregation` (`pixel_id = 'web'`) for counts, `metrics._pixels_new` (`pixel_id = 'web'`, one day) for parameters. +- Overlap: ship PR 3 next to the episode pixel for one release; compare "installs with persistent breakage / DAU" with the existing permanent-failure series by platform. Expected: new is lower where late answers dominated, higher where the episode never reached its deadline. Remove the episode pixel (PR 5) when the difference is explained. +- Pixel expiry follows the existing definitions (2027-01-15). +- Add the three metrics to the health indicators dashboard. Data checks use ClickHouse: pixel counts from `metrics.pixels_aggregation` (`pixel_id = 'web'`), parameters from `metrics._pixels_new` (`pixel_id = 'web'`, one day). +- No remote kill switch is proposed. If volume surprises, the soft cap and the next release are the lever. **Question for reviewers:** is a feature flag required for a new pixel here? **Localization / Internationalization** @@ -139,6 +205,8 @@ None. ## Open questions -1. Minimum lifetime 30 s: keep, or tune from episode data first? -2. Threshold for "permanent" on `tabs_affected` and `repeat_failures`: keep the current one? -3. Judge at suspension, or only at document end? +1. Minimum lifetime 30 s and streak 3: acceptable as first constants, or tune from the episode data first? +2. Persist the streak across launches (a local counter)? It makes persistent breakage visible across iOS terminations at the price of local state. +3. Flag or no flag for a new pixel. +4. Judge at suspension: accept, or only at document end? +5. Soft cap value. diff --git a/work/projects/cpm-project-summary-2026-11-03.md b/work/projects/cpm-project-summary-2026-11-03.md index 25c49184..92c81c7f 100644 --- a/work/projects/cpm-project-summary-2026-11-03.md +++ b/work/projects/cpm-project-summary-2026-11-03.md @@ -110,7 +110,7 @@ Open: **Что неверно в `e6419bb910`** (проверено по коду): тихие замеры старше 5 минут сбрасываются (`discardExpiredState`); нет минимального времени жизни страницы; падение процесса вкладки засчитывается поломкой; `background` берётся из «загрузка ≥ 1 с» и одного общего сигнала; страницы, живущие до выхода приложения, не оцениваются; определение macOS без `pixelSource` и `channel`, суффиксы от iOS; утечка `navigationWaitContexts`; нет теста на разбор `cpmStage`. **Tech design (черновики, ревьюер не назначен):** -- [[cpm-permanent-breakage-metric-tech-design-2026-11-10]]: новых пикселей нет, меняется детект под существующим `web_extension_cpm_episode`: эпизод открывается оценкой страницы по концу документа (минимум 30 с жизни) вместо таймера 4 с, закрывается здоровым ответом или приостановкой приложения; параметры `outcome` и `background`. +- [[cpm-permanent-breakage-metric-tech-design-2026-11-10]]: два пикселя (`web_extension_cpm_breakage`, `web_extension_cpm_breakage_persistent`), оценка страницы по концу документа, минимум 30 с жизни, серия из 3 подряд, суждение при приостановке приложения, `background` по `didCreateBackgroundWebView`. Работает рядом с эпизодным пикселем один релиз. - [[cpm-wake-init-detection-tech-design-2026-11-10]]: метки `cpmInitProgress` из расширения, `last_stage` в пикселе, `response_sent` как здоровый сигнал, изменение расширения (PR #3727, ветка `alex/cpm-init-progress`; метки `page_init_sent`, `config_read`, `settings_checked`, `site_checked` в рабочем дереве без коммита и без запуска тестов). -Открытые вопросы для ревью: порог 30 с; порог «перманентного» по `tabs_affected` и `repeat_failures`; оценивать ли при приостановке; считать ли `response_sent` здоровым до проверки без слушателя; шесть меток или меньше. +Открытые вопросы для ревью: порог 30 с и серия 3; сохранять серию между запусками; считать ли `response_sent` здоровым до проверки без слушателя; шесть меток или меньше; нужен ли флаг для нового пикселя. diff --git a/work/projects/cpm-wake-init-detection-tech-design-2026-11-10.md b/work/projects/cpm-wake-init-detection-tech-design-2026-11-10.md index dfb6685a..772aa4a9 100644 --- a/work/projects/cpm-wake-init-detection-tech-design-2026-11-10.md +++ b/work/projects/cpm-wake-init-detection-tech-design-2026-11-10.md @@ -10,7 +10,7 @@ status: draft --- # TD: Pixel Definition: CPM Wake Init Detection (macOS/iOS + embedded web extension) -> Internal metrics, keep out of public PRs. Builds on [[cpm-permanent-breakage-metric-tech-design-2026-11-10]] (the detection this design extends). Where we are: [[cpm-project-summary-2026-11-03]]. Wake background and what was ruled out: [[cpm-ios-idle-wake-investigation-2026-10-30]], [[cpm-post-release-analysis-2026-10-27]]. +> Internal metrics, keep out of public PRs. Builds on [[cpm-permanent-breakage-metric-tech-design-2026-11-10]] (the pixel this design extends). Where we are: [[cpm-project-summary-2026-11-03]]. Wake background and what was ruled out: [[cpm-ios-idle-wake-investigation-2026-10-30]], [[cpm-post-release-analysis-2026-10-27]]. **Author:** Alex M **Reviewer:** TBD (previous CPM pixel designs: Michal) @@ -54,7 +54,7 @@ Collect no page content, URLs or persistent identifiers. ## Recommended Approach -The extension reports **progress marks** for each main-frame init as fire-and-forget native notifications. The app keeps the furthest mark per document, treats `response_sent` as healthy evidence, treats `response_failed` as an immediate failure, and adds `last_stage` to `web_extension_cpm_episode`. +The extension reports **progress marks** for each main-frame init as fire-and-forget native notifications. The app keeps the furthest mark per document, treats `response_sent` as healthy evidence, treats `response_failed` as an immediate breakage, and adds `last_stage` to `web_extension_cpm_breakage`. ### Marks @@ -74,24 +74,24 @@ Marks are best effort: they carry `{ tabId, url, stage }`, never wait and never - `AutoconsentWebExtensionMessageHandler` gets a `cpmInitProgress` method. Unknown stage names are ignored without an error, so a newer extension cannot make an older app answer with failures. - `CPMMessagingHealthEvent.initProgress` carries the mark; the monitor stores `lastStage` per document and clears it on a new navigation, a cancelled navigation and an extension reload. -- `response_sent` counts as healthy evidence for the document, like a dashboard response. `response_failed` judges a pending document at once. Healthy evidence is either the dashboard refresh or `response_sent`; the open question below asks whether `response_sent` should count before it is verified. +- `response_sent` counts as healthy evidence for the document, like a dashboard response. `response_failed` judges a pending document at once (`ended_by=explicit_signal`). Healthy evidence is either the dashboard refresh or `response_sent`; the open question below asks whether `response_sent` should count before it is verified. - The Debug "Simulate CPM Messaging Breakage" switch hides marks as well as dashboard responses. ### Pixel change -The existing `web_extension_cpm_episode` (detection changed in [[cpm-permanent-breakage-metric-tech-design-2026-11-10]]) gains one parameter: +`web_extension_cpm_breakage` (defined in [[cpm-permanent-breakage-metric-tech-design-2026-11-10]]) gains one parameter: | Parameter | Values | |---|---| -| `last_stage` | `none` (no mark reached the app), `page_init_sent`, `received`, `config_read`, `settings_checked`, `site_checked`. `response_sent` cannot appear on a failure, because it is healthy evidence. `response_failed` appears when it was the reason. | +| `last_stage` | `none` (no mark reached the app), `page_init_sent`, `received`, `config_read`, `settings_checked`, `site_checked`. `response_sent` cannot appear on a breakage, because it is healthy evidence. `response_failed` appears when it was the reason. | -No new pixel. The name, frequency and the other parameters are as in the companion design. Both platforms' definitions and `params_dictionary.json5` get the parameter. +No new pixel. Names, frequency and the other parameters are unchanged. Both platforms' definitions and `params_dictionary.json5` get the parameter. ### How to read `last_stage` The marks are relayed by the background, so a dead background cannot report. `none` therefore cannot tell a missing content script, a lost message to the background and a lost notification apart. -| `last_stage` on an episode | What it says | Next action | +| `last_stage` on a breakage | What it says | Next action | |---|---|---| | `none` | nothing reached native: content script not injected, `init` and the mark lost on the way to the background, or the background cannot notify native | check injection (`tab_has_extension_user_scripts` is gone); WebKit-side loss on wake (R1, H2) | | `page_init_sent` | the content script sent; the background never processed `init` | message lost between content script and worker listener after a wake: content-script retry of `init`, WebKit bug report | @@ -154,7 +154,7 @@ Compatibility and sequencing: **Privacy** -One new enumerated parameter on the existing episode pixel. The marks carry the tab ID and the URL from the extension to the app inside the app (the dashboard refresh already does), but neither enters a pixel or a log (the monitor's log descriptions omit URLs). Privacy Triage to be requested once for both designs. Not requested yet. +One new enumerated parameter on a pixel that is itself new in [[cpm-permanent-breakage-metric-tech-design-2026-11-10]]. The marks carry the tab ID and the URL from the extension to the app inside the app (the dashboard refresh already does), but neither enters a pixel or a log (the monitor's log descriptions omit URLs). Privacy Triage to be requested once for both designs. Not requested yet. **Security** @@ -171,7 +171,7 @@ None. The comparison is across releases and channels (see the heisenbug risk). **Operational** - Order of landing: native handler and `last_stage` (PR 4 in the stack of the companion design) can ship before the extension; the extension PR merges after review and enters the app through the usual embedded-files update. -- After the first public release with marks: `last_stage × background × platform` over the episode count, and the share of episodes by stage, to pick the fix according to the table above. Data checks as in the companion design. +- After the first public release with marks: `last_stage × background × platform` over the breakage count, and the share of breakages by stage, to pick the fix according to the table above. Data checks as in the companion design. - The marks and the parameter are temporary investigation tools. Remove `config_read`, `settings_checked`, `site_checked` and `last_stage` when the wake group has a fix or is closed (project plan: close the project and reset the target if the new metrics show nothing actionable). The expiry date follows the pixel definition (2027-01-15). - Not in scope: per-stage durations (a time bucket between marks), the background's own lifecycle metrics, and the idle-unload distance. They are the next candidates if `last_stage` leaves the wake group unexplained.