[2026-09-17] eagle: family/how-to/zont-config-compiler.md family/tech/zont-scenario-logic-11109.md work/projects/cpm-m3-telemetry-privacy-triage.md

This commit is contained in:
Alexey Martemyanov
2026-09-17 15:33:05 +06:00
parent d0dfc989e6
commit 4eae38bab8
3 changed files with 214 additions and 172 deletions
@@ -1,94 +1,107 @@
# Tech Design 2 / Privacy Triage: Apple — CPM M3 Telemetry
# TD: Pixel Definition: CPM M3 Telemetry (macOS/iOS)
Status: **DRAFT — privacy review required**
**Name:** Alex M · **Email:** amartemyanov@duckduckgo.com · **Objective:** O-E
**PR:** [Add PR link]
**Project:** https://app.asana.com/1/137249556945/project/1163321984198618/task/1216761517055116?focus=true
**Baseline:** [[cpm-extension-health-pixels-privacy-triage]]
**Scope:** Current implementation in `codex/cpm-failure-telemetry`, reviewed 2026-09-17. iOS 18.4+ / macOS 15.4+. Describes implemented collection, not the proposed reduced design.
**Author:** Alex M
**Reviewer:** TBD
**Stakeholders:** Michal, Konrad, Russell (from the first CPM Tech Design; confirm for M3)
**Project:** [CPM: root-cause the stuck embedded extension messaging (macOS/iOS)](https://app.asana.com/1/137249556945/task/1216761517055116)
**Previous Tech Design:** [CPM breakage pixels](https://app.asana.com/1/137249556945/task/1218005985662983)
## 1. Purpose and approach
## Background & Requirements
The first design detects CPM initialization failures, persistent messaging failures and recovery. M3 adds facts that help distinguish background-process failure, context-load errors and tab wiring problems.
The first CPM pixels detect initialization failure, a stuck messaging episode and recovery. They do not explain whether a failure coincides with background-process termination, a context-load error or incorrect tab wiring.
An in-memory recorder observes extension lifecycle, WebKit process callbacks and critical memory pressure. When an existing initialization-failure or stuck pixel fires, it attaches a best-effort snapshot. There are no new JavaScript probes, page-content reads or periodic telemetry uploads. The delegate proxy forwards WebKit callbacks and is remotely switchable; this switch disables the proxy, **not all M3 collection**.
Forced background-process termination can reproduce a persistent CPM outage. M3 adds native-side diagnostics to investigate whether the same failure occurs in production on iOS and macOS, without relying on the extension's messaging channel.
A separate new pixel reports extension reload operations that fail before the replacement context becomes active. Graveyard mitigation and A/B cohorts are outside M3.
## Problem Statement
## 2. Pixels and frequency
Distinguish background-process/context failures from tab-local problems and identify failed extension reload operations. Collect the diagnostic state alongside existing failures, without page content, URLs or persistent identifiers.
Names below are base names; PixelKit adds frequency/platform suffixes. iOS uses its standard platform/form-factor suffixes; macOS keeps its standard naming policy. These are not new explicit `platform` / `form_factor` payload fields. Standard metadata includes `appVersion`; macOS schemas also declare `pixelSource` and `channel`.
M3 provides evidence for the production-validation milestone; it does not implement graveyard mitigation or an A/B experiment.
| Pixel | Change / trigger | Frequency |
## Recommended Approach
Keep the existing CPM failure/stuck detection. A shared in-memory recorder collects lifecycle events, critical-memory-pressure notifications and current WebKit/tab state. When a failure or stuck pixel fires, attach a best-effort snapshot. No additional JavaScript probes or periodic telemetry uploads are introduced.
A forwarding delegate proxy observes background-process termination and responsiveness callbacks. The original delegate owns the proxy; the proxy references it weakly. A remote flag enables/disables proxy installation, not the entire diagnostics recorder.
| Pixel | Trigger | Frequency |
|---|---|---|
| `debug_web_extension_cpm_initialization_failed_after_<reason>` | Add the snapshot when an eligible finished navigation fails the existing CPM response check. | Daily |
| `debug_web_extension_cpm_messaging_stuck_<reason>` | Add the snapshot when a later eligible navigation confirms the failure episode. | Daily + count; once per stuck episode |
| `debug_web_extension_reload_failed` | **New:** an extension context unload/load operation fails during reload; not limited to CPM. | Daily + count |
| `debug_web_extension_cpm_initialization_failed_after_<reason>` | Existing initialization failure; add diagnostic parameters. | Daily |
| `debug_web_extension_cpm_messaging_stuck_<reason>` | Existing failure confirmed by a later eligible navigation; add diagnostic parameters. | Daily + count, once per episode |
| `debug_web_extension_reload_failed` | **New:** extension unload/load fails during a reload operation. Applies to all supported extension types. | Daily + count |
For both CPM families, `<reason>` is `session_restoration`, `tab_crash`, `extension_reload` or `other`. The stuck reason comes from the initial failure. This includes the `stuck_extension_reload` variant omitted from the first note's table.
`<reason>`: `session_restoration`, `tab_crash`, `extension_reload`, `other`. PixelKit applies standard frequency/platform suffixes. Standard metadata includes `appVersion`; macOS also declares `pixelSource` and `channel`.
Existing recovery pixels and `debug_web_extension_cpm_messaging_extension_reload_failed` gain **no** diagnostic parameters. The latter means CPM failed after a successful extension reload; the new pixel instead means the reload operation itself failed.
Recovery pixels remain unchanged. The new reload-failure pixel reports a failed reload operation, unlike the existing `cpm_messaging_extension_reload_failed`, which reports CPM failure **after a successful reload**.
## 3. What is actually sent
### Failure / stuck parameters
### CPM failure / stuck snapshot
Unavailable optional facts are omitted. Boolean values are `true` / `false`.
Booleans are strings `true\|false`. Unavailable optional facts are omitted, not sent as false.
| Parameter | Collected fact / transmitted values |
| Parameter | What is collected |
|---|---|
| `extension_context_loaded` | Whether the tracked context is loaded. Boolean. |
| `background_view_alive` | Whether the recorder's current background WKWebView still exists. Boolean. |
| `background_web_process_alive` | Whether its current process identifier is nonzero. Boolean; **PID is not sent**. |
| `background_web_process_responsive` | WebKit's process-responsiveness verdict via SPI. Boolean. |
| `network_process_restarted` | Current network PID differs from the value captured at context load. Boolean; not a restart count or proof of causality. |
| `tab_known_to_webkit` | Failing tab is present in the context's open-tabs set. Boolean. |
| `tab_controller_matches_context` | Failing tab and context use the same extension controller. Boolean. |
| `tab_has_extension_user_scripts` | Tab's registered user scripts include the expected extension world. Boolean; does not prove execution. |
| `native_handler_registered` | Autoconsent native-message handler is registered. Boolean; does not prove a reply was delivered. |
| `memory_pressure_critical` | Age of last observed critical-pressure notification: `none\|under_1_min\|under_5_min\|under_30_min\|over_30_min`. No memory size is sent. |
| `background_view_create_count` | Views created since context load: `0\|1\|2\|3_to_5\|over_5`. |
| `background_view_leaked_count` | Tracked live views beyond the expected current view; same count buckets. Indicates retained views, not a proven leak. |
| `extension_context_errors` | `none`, or accumulated distinct error descriptors since context load. Context errors map to names; other/underlying errors include **domain and numeric code**, not localized text. No explicit payload cap. |
| `background_events` | Recent lifecycle sequence, described below. Omitted when empty. |
| `extension_context_loaded` | Context is loaded — boolean. |
| `background_view_alive` | Current background WKWebView exists — boolean. |
| `background_web_process_alive` | Background process identifier is nonzero — boolean; PID is not sent. |
| `background_web_process_responsive` | WebKit's process-responsiveness verdict — boolean. |
| `network_process_restarted` | Network PID differs from the value at context load — boolean; PIDs are not sent. |
| `tab_known_to_webkit` | Failing tab is in WebKit's open-tabs set — boolean. |
| `tab_controller_matches_context` | Tab and context use the same extension controller — boolean. |
| `tab_has_extension_user_scripts` | Registered scripts include the expected extension world — boolean, not proof of execution. |
| `native_handler_registered` | Autoconsent native-message handler is registered — boolean, not proof of message delivery. |
| `memory_pressure_critical` | Age of last observed critical pressure: `none`, `under_1_min`, `under_5_min`, `under_30_min`, `over_30_min`. |
| `background_view_create_count` | Creations since context load: `0`, `1`, `2`, `3_to_5`, `over_5`. |
| `background_view_leaked_count` | Retained views beyond the expected current view; same buckets. Not proof of a leak. |
| `extension_context_errors` | `none` or accumulated error descriptors: mapped context-error names, with raw domain/code for other or underlying errors. No localized text; no explicit payload cap. |
| `background_events` | Recent lifecycle sequence with relative ages in whole seconds. |
The recorder retains at most 40 lifecycle events in memory. A snapshot takes the latest 12; serialization removes oldest entries until the value fits 255 characters. Format: `event_name@-seconds`, relative to snapshot time, rounded to whole seconds.
The recorder keeps 40 events in memory; the pixel includes at most the last 12 and 255 characters, dropping oldest entries first. Format: `event_name@-seconds`, relative to snapshot time.
Event names: `load`, `view`, `dealloc`, `unresponsive`, `responsive`, `proxy_on`, `proxy_off`, `died_<reason>`, `error_<descriptor>`. Death reasons: `memory_limit\|cpu_limit\|requested_by_client\|crash\|shared_crash_limit\|unknown`. Error timeline entries represent background-load errors. Names are lowercased, character-filtered and limited to 64 characters; **filtering is not an error-domain allowlist**.
Events: `load`, `view`, `dealloc`, `unresponsive`, `responsive`, `proxy_on`, `proxy_off`, `died_<reason>`, `error_<descriptor>`. Termination reasons: `memory_limit`, `cpu_limit`, `requested_by_client`, `crash`, `shared_crash_limit`, `unknown`. Error events represent background-load failures. Event labels are lowercased, character-filtered and capped at 64 characters.
Example: `view@-60,died_crash@-42,error_background_failed_to_load@-41`. These are internally generated event labels, not identifiers supplied by a website.
Example: `view@-60,died_crash@-42,error_background_failed_to_load@-41`.
### New reload-failure pixel
### Reload-failure parameters
| Parameter | Allowed values |
| Parameter | Values |
|---|---|
| `extension_type` | `embedded\|darkMode\|adBlocking\|searchToken\|unknown` |
| `reload_trigger` | `data_clearing\|scriptlet_update\|explicit` |
| `reload_phase` | `unload\|load\|lightweight_load\|full_load\|fallback_load` |
| `reload_error` | `already_loaded\|not_loaded\|base_url_in_use\|no_background_content\|background_failed_to_load\|unknown\|other` |
| `extension_type` | `embedded`, `darkMode`, `adBlocking`, `searchToken`, `unknown` |
| `reload_trigger` | `data_clearing`, `scriptlet_update`, `explicit` |
| `reload_phase` | `unload`, `load`, `lightweight_load`, `full_load`, `fallback_load` |
| `reload_error` | `already_loaded`, `not_loaded`, `base_url_in_use`, `no_background_content`, `background_failed_to_load`, `unknown`, `other` |
Unlike `extension_context_errors`, `reload_error` is an allowlisted category: it includes no raw domain, numeric code, underlying error or message.
These are allowlisted enums; this pixel does not send raw error domains, codes or messages.
## 4. Privacy review questions
## Testing
**Browsing data or identifiers?** No intentional collection of URLs, domains visited, search queries, titles, page content, message payloads, user IDs, tab/document/context IDs, extension UUIDs or process IDs in these new payloads. Internal object/ID comparisons emit booleans. Error domains are error namespaces, not visited website domains, but are currently not allowlisted.
- Unit tests: parameter serialization, buckets, timeline limits, attachment to failure/stuck pixels, reload-error mapping, delegate forwarding, ownership and runtime flag changes.
- Integration validation on iOS/macOS: force background-process termination, check the emitted diagnostics, test proxy on/off and extension reload failures. Confirm unknown SPI values are omitted.
- Privacy validation: inspect actual payloads and run both platform schema validators. Confirm URLs, identifiers and error messages are absent. Review exact-second timelines and error-domain/code values before release.
**Could events be linked or unusually specific?** No explicit correlation identifier is added. However, exact-second event sequences and arbitrary error-domain/code combinations can be high-cardinality. This draft does **not** assert that linkability is impossible.
## Additional Considerations (if applicable)
**Sensitive user actions?** `reload_trigger=data_clearing` reveals that a failing reload followed data clearing. It does not identify cleared sites or data. This requires explicit triage consideration.
**Privacy**
**Temporary or permanent?** Intended for diagnosis and the M4 validation gate. Current schemas do not implement an expiry/removal deadline; agree one before rollout. No change to the baseline health pixels' intended lifetime.
New pixel and additional parameters require Privacy Triage. No intentional collection of browsing URLs, search queries, page content, message payloads, tab/context IDs or PIDs in these payloads. Exact-second timelines and unrestricted error-domain/code combinations can be high-cardinality; bucketing/allowlisting needs a decision. `data_clearing` discloses the operation preceding a failed reload.
**Self-service eligible?** Not asserted. Exact seconds conflict with the repository's coarse-bucketing guidance; error descriptors, the data-clearing trigger and retention/removal policy need a privacy decision.
Current local diagnostics logs separately include raw errors with `privacy: .public`; remove/redact these before production. Pixel approval does not approve those logs.
## 5. Boundaries and release gates
**Security**
**Local logs are separate from pixel payloads.** Current recorder code also logs process IDs and raw localized/underlying errors with `privacy: .public`. These logs may expose paths or URLs and must be removed/redacted before production; pixel approval does not approve them.
Uses guarded private WebKit selectors for process/script facts. Validate supported OS versions; missing facts must not block pixel delivery.
**What these pixels cannot establish:** no denominator of all process deaths; deaths followed by normal recovery may produce no diagnostic pixel. A failing tab does not prove an all-tabs outage. The rolling timeline may lose the original trigger, and deferred snapshots can observe newer state.
**Site Breakage**
**Observer effect:** the findings note records that installing the delegate proxy changed the forced-process-death reproduction. Validate the current proxy on/off on both platforms before treating M3 as an unaffected production baseline.
No intended page-behavior changes. However, the investigation recorded a changed reproduction with the delegate proxy installed; validate the current proxy on/off before treating observations as an unaffected baseline.
**Approval requested:** necessity and format of the snapshot fields, coarse replacement or explicit exception for exact-second timelines, allowlisting/bounding of error descriptors, the data-clearing trigger, and a diagnostic sunset date.
**Experimentation**
Implementation references: `CPMMessagingDiagnostics.swift`, `CPMMessagingDiagnosticsRecorder.swift`, `CPMBackgroundWebViewDelegateProxy.swift`, `WebExtensionPixelFiring.swift`; iOS/macOS pixel schemas. Investigation: [[cpm-web-extension-breakage-findings]]. Milestones: [[cpm-web-extension-breakage-validation]].
None in M3. Graveyard mitigation and A/B testing remain separate milestones.
**Operational**
Update the existing CPM Grafana board for the new fields and reload failures. Agree a diagnostic removal/review date; none is enforced currently. Failure-only snapshots do not provide a denominator of all process deaths or prove an all-tabs outage. Rolling timelines can lose the trigger; deferred snapshots can observe newer state.
**Localization / Internationalization**
None.