Files
obsidian-vault/work/projects/cpm-pr0-episode-telemetry-summary-2026-10-16.md
T

135 lines
12 KiB
Markdown
Executable File

---
title: CPM PR0 — complete telemetry change
date: '2026-10-16'
tags:
- work
- cpm
- telemetry
updated: '2026-10-20'
---
# CPM PR0: one terminal report per failure episode
**Verified 20 October 2026:** [#7376](https://github.com/duckduckgo/apple-browsers/pull/7376), head `5d1cb4abd2`, targets `release/ios/7.243.0`. The implementation is pushed; the PR is **closed without merge**, with APPROVED review and green exact-head CI. This describes that branch, not shipped behavior. [macOS #7484](https://github.com/duckduckgo/apple-browsers/pull/7484) still has the earlier `was_stuck` contract, no two-phase history, and a release-base conflict.
**Implemented change:**
Replace separate `init_failed`, `stuck`, `recovery[with/without reload]` and CPM post-reload-failure events with `web_extension_cpm_episode`. Each report contains the initial failure and its observed outcome, so diagnosis no longer requires joining separate failure, stuck and recovery events.
**When it fires:**
The first `4s` timeout **opens** an episode and captures one diagnostic snapshot.
The pixel is **submitted when the episode is closed** (a qualifying dashboard response or a 5-minute deadline closes).
Further failures or extension reloads are recorded in the same episode and included in its final report. If no qualifying response arrives, the report is sent 5min after the first failure, even if the user does not open or reload another page (with `outcome=no_response_before_deadline`). The existing rules for confirming a persistent failure and automatically reloading the extension stay the same.
A dashboard response tells us the extension answered the app. It does not confirm that the page received its initialization reply (`initResp`). For example, tab B may answer while the original failing tab A is still broken.
Deadline expiry does not prove permanent failure; unfinished episodes can be lost if the monitor or process is destroyed.
**Reporting frequency:**
Both platforms use `daily+count` (`.dailyAndCount`). The first completed episode each day sends `_daily` and `_count` with the same summary; every later completed episode sends `_count` only. Daily deduplication is by event name, across all outcomes. `_daily` estimates installations with a completed failure episode that day; `_count` counts completed episodes. These are not distinct people or all affected installations: unfinished episodes can be lost. Old separately deduplicated failure/stuck/recovery totals need a new baseline before comparison.
**Attribution:**
**Five added summary parameters:**
- `reason`: the first timeout's context — `session_restoration`, `tab_crash`, `extension_reload` or `other`. [Previously](https://app.asana.com/1/137249556945/project/481882893211075/task/1218619301158188?focus=true) encoded in failure pixel names.
- `stuck`: `false-one`, `true-one`, `false-many` or `true-many`. This replaces the boolean `was_stuck` added in the earlier version of this PR. The true/false part says whether a later eligible non-restoration navigation, begun after the first failure, also timed out. The one/many part says whether timeouts were observed in one or multiple distinct tabs during the episode, including concurrent and restoration timeouts. Repeated timeouts in the same tab still count as one. One does not prove other tabs healthy; many does not prove all tabs failed. Tab identifiers stay local.
- `outcome`: `failed_measurement_response` for the measurement that timed out; `failed_tab_response` for a later measurement on a failing tab; `other_tab_response` for another tab; `unattributed_response` for an ambiguous current-document match; or `no_response_before_deadline`.
- `reload_state`: `none` (no reload observed), `pending` (replacement started), `failed` (replacement failed), `loaded` (replacement loaded), or `response_timed_out` (its first eligible messaging measurement timed out). Loaded alone does not prove messaging recovery.
- `response_after`: elapsed time since the first failure, bucketed as `under_30s`, `30s_to_1m` or `1m_to_deadline`. Omitted for the no-response outcome.
**Four added background-load parameters:**
Ported and adapted from `alex/cpm-messaging-readiness-telemetry`:
- `bg_ready_after` — background-load evidence at the first failure. Values: `none` (no current view and no attributed error 6 for the latest attempt), `pending`, `failed`, or completion-time buckets `under_1s`, `1s_to_4s`, `4s_to_10s`, `10s_to_30s`, `over_30s`. An attributed error 6 keeps the value `failed` after its view disappears; a replacement view/context resets that attribution.
- `bg_view_age` — age of the current background view: `under_4s`, `4s_to_8s`, `8s_to_30s`, `30s_to_2m`, `over_2m`. Distinguishes a fresh wake from a failure long after view creation; omitted when no current view exists.
- `bg_load_error_after` — time from the affected view's creation to WebKit error 6: `under_5s`, `5s_to_25s`, `25s_to_35s`, `over_35s`. Tests whether the failure coincides with the 30-second background-load timer.
- `bg_load_error_app_state` — app state when that error occurred: `active`, `inactive` or `background` (macOS: active/inactive). Helps distinguish foreground failures from background suspension conditions.
These values are captured in the first-failure snapshot and sent in the same terminal episode report, retaining `daily+count`. Unavailable values are omitted. The two error fields describe only the first newly observed error 6 for the corresponding background-load attempt. Repeated error-list notifications preserve its original time and app state; a replacement view/context clears that attribution. Errors seen before any view was created do not qualify.
Readiness observation shares the existing `cpmBackgroundDelegateProxy` switch. It calls the public `loadBackgroundContent` API on the next main-queue turn after a new view is created. Disabling observation invalidates pending callbacks; reenabling on an existing view leaves readiness unknown until the next view creation. Error 6 also marks the observed load failed if WebKit never completes its load callback. Later readiness changes and errors do not replace the episode's first-failure snapshot. `pending` means the load was still pending at that snapshot; it may complete or fail later. For example, a snapshot taken 4 seconds after view creation stays `pending` even if error 6 arrives at 30 seconds; that later error does not update the load fields. If observed before stuck confirmation, it appears in the bg_events continuation.
The added fields' combined distinguishability with the retained `bg_events` still needs checking. Daily deduplication and the absence of identifiers do not guarantee that rare payloads cannot be correlated. The triage summary quoted in the discussion used `was_stuck`; the current four-value `stuck` and two-phase history must be reflected in the reviewed contract. This documentation update does not establish privacy approval for those later changes.
`app_state` and `open_tabs` remain deferred to targeted startup/no-view diagnostics; `loading_tabs` is not planned for this change. These additions do not change the four-second detector or add page-side `initResp` acknowledgement.
**Eight existing diagnostic booleans** from [✓ TD: CPM failure pixels attribution](https://app.asana.com/1/137249556945/project/481882893211075/task/1218619301158188?focus=true): all are captured at the first failure; unavailable facts are omitted.
- `context_loaded` — extension context loaded; `bg_view_alive` — current background view exists.
- `bg_process_alive` — its process is alive; `bg_process_responsive` — WebKit considers it responsive.
- `tab_in_context` — failing tab registered in WebKit; `handler_registered` — autoconsent native handler registered.
- `tab_controller_match` — tab and context controllers match; `tab_has_ext_scripts` — extension scripts remain registered.
`bg_events` contains the first-failure history and, for confirmed stuck episodes, a `;` followed by events observed afterwards up to that confirmation, before its automatic recovery. Non-stuck episodes keep only the first-failure history. A standalone `...` at the start of either phase means older events were dropped. Both phases share a limit of 12 events and 255 characters, including separators and markers. Oldest initial entries are dropped before continuation entries, so a long continuation can remove the entire initial history. No budget is reserved for either phase; reserving one was discussed but is not implemented. Sanitized `token@age` entries retain order and new repeated events. The initial snapshot is not replayed into the continuation. There is no general deduplication; only consecutive identical load-error notifications within one second are suppressed. Ages use `30s`, `1m`, `2m`, `5m`, `15m` and `over_15`, relative to the first failure before `;` and stuck confirmation after it. An empty phase has no events. Consumers split on both `,` and `;`, ignoring empty entries and standalone `...`. Example: `...,load@1m,view@30s;error_e:6@30s`. No second full diagnostic snapshot is collected.
Standard metadata remains: `appVersion` on both platforms, plus `pixelSource` and `channel` on macOS.
**Remove retired pixels from** [✓ TD: CPM failure pixels attribution](https://app.asana.com/1/137249556945/task/1218619301158188)**:**
- `debug_web_extension_cpm_initialization_failed_after_session_restoration`
- `debug_web_extension_cpm_initialization_failed_after_other`
- `debug_web_extension_cpm_initialization_failed_after_tab_crash`
- `debug_web_extension_cpm_initialization_failed_after_extension_reload`
- `debug_web_extension_cpm_messaging_stuck_session_restoration`
- `debug_web_extension_cpm_messaging_stuck_tab_crash`
- `debug_web_extension_cpm_messaging_stuck_extension_reload`
- `debug_web_extension_cpm_messaging_stuck_other`
- `debug_web_extension_cpm_messaging_recovered_without_extension_reload`
- `debug_web_extension_cpm_messaging_recovered_after_extension_reload`
- `debug_web_extension_cpm_messaging_extension_reload_failed`
**Parameters removed from transmission:**
`context_errors`, `network_restarted`, `critical_memory_age`, `bg_view_creations`, `bg_view_leaked_count`, `app_version_change`, `app_launch_age`, `background_graveyard_enabled` and `recovery_from`. Episode state and response attribution replace `recovery_from`. Graveyard attribution is also removed from the diagnostic model and monitor, including the unused attribution state and methods. The graveyard mitigation itself is unchanged. The retired parameter encoder is removed so only the episode report builds the transmitted payload. App tests added for the dropped open/loading-tab measurements are removed with their project entries.
**What this still cannot diagnose:**
A late same-measurement dashboard response without reload helps identify a transient timeout. `stuck=false-*` only means persistence was not confirmed; `true-*` still cannot exclude a working page whose dashboard confirmation was lost. There is no page-side `initResp` ACK or last-confirmed-stage field. One/many measures observed failing tabs, without testing every tab or grouping failures by window.
The final outcome of an initially pending load is not reported explicitly. Pre-stuck errors can reach the continuation, but successful completion does not update the frozen load fields. Network-replacement attribution relative to the current background view is also missing. The separate `web_extension_cpm_network_process_recovery_applied` in [#7459](https://github.com/duckduckgo/apple-browsers/pull/7459) uses daily+count with no custom parameters; it counts a restart being applied, not all crashes or successful recovery. These follow-up measurements are not part of #7376. Detailed gaps and production truncation evidence: [[cpm-status-2026-10-15#6. Телеметрия]].
**Validation and lifetime:**
On `5d1cb4abd2`: 777 WebExtensions tests passed, zero failures; strict SwiftLint passed for all six changed Swift files; both platforms' pixel definitions passed validation. Tests used an isolated package copy with refreshed dependency resolution; the worktree's user-edited lockfile was preserved. All four required CI result checks and Danger are green on this head. The episode pixel is temporary; both definitions list `expires: 2027-01-15`, which does not itself remove runtime code.