[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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user