[2026-09-18] eagle: work/projects/cpm-m3-telemetry-privacy-triage.md

This commit is contained in:
Alexey Martemyanov
2026-09-18 16:49:33 +06:00
parent 547e5df053
commit 54d69ca885
@@ -2,27 +2,27 @@
**Author:** Alex M
**Reviewer:** TBD
**Stakeholders:** Michal, Konrad, Russell (from the first CPM Tech Design; reviewers for this proposal to be confirmed)
**Stakeholders:** Michal, Konrad, Russell (from the first Tech Design; confirm for this proposal)
**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)
## Background & Requirements
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.
The first CPM pixels detect initialization failure, a stuck messaging episode and recovery. They do not show whether the background process stopped, the extension failed to load, or the tab used the wrong extension configuration.
Terminating the extension's background process can leave CPM unable to respond, even after page reloads or opening new tabs. To investigate these failures in production, the iOS and macOS apps will attach process, extension and tab state to the existing failure pixels. This reporting does not depend on the extension responding.
Terminating the extension's background process can leave CPM unable to respond, even after page reloads or opening new tabs. Add process, extension and tab state to existing iOS/macOS failure pixels, even when the extension cannot respond.
## Problem Statement
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.
Distinguish background-process and extension-context failures from problems affecting one tab, and identify failed reloads. Collect no page content, URLs or persistent identifiers.
This proposal covers diagnostic collection only; it does not change recovery behavior or introduce an experiment.
## 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, capture native state synchronously on the main actor and attach a best-effort snapshot. Callbacks from an earlier context load cannot enter the current timeline. No additional JavaScript probes or periodic telemetry uploads are introduced.
Keep the existing failure/stuck detection. A shared recorder holds lifecycle and critical-memory-pressure events in memory. When a pixel fires, it captures current WebKit/tab state synchronously on the main actor. Earlier context loads cannot add events to this timeline. There are no JavaScript probes or periodic uploads.
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 proxy installation on the current view; disabling restores the original delegates on all surviving observed views, including views retained after unload. It does not disable the entire diagnostics recorder.
A delegate proxy records process termination and responsiveness, then forwards callbacks to WebKit. WebKit's delegate owns the proxy; the proxy holds it weakly. The remote flag enables the proxy on the current view. Disabling restores original delegates on all surviving views, including after unload; other diagnostics remain enabled.
| Pixel | Trigger | Frequency |
|---|---|---|
@@ -36,30 +36,34 @@ Recovery pixels remain unchanged. The new reload-failure pixel reports a failed
### Failure / stuck parameters
Unavailable optional facts are omitted. Boolean values are `true` / `false`.
Unavailable optional facts are omitted. The first nine parameters are booleans (`true` / `false`); PIDs are never sent.
| Parameter | What is collected |
|---|---|
| `context_loaded` | Context is loaded — boolean. |
| `bg_view_alive` | Current background WKWebView exists — boolean. |
| `bg_process_alive` | Background process identifier is nonzero — boolean; PID is not sent. |
| `bg_process_responsive` | WebKit's process-responsiveness verdict — boolean. |
| `network_restarted` | Network PID differs from the value at context load — boolean; PIDs are not sent. |
| `tab_in_context` | Failing tab is in WebKit's open-tabs set — boolean. |
| `tab_controller_match` | Tab and context use the same extension controller — boolean. |
| `tab_has_ext_scripts` | Registered scripts include the expected extension world — boolean, not proof of execution. |
| `handler_registered` | Autoconsent native-message handler is registered — boolean, not proof of message delivery. |
| `critical_memory_age` | Age of last observed critical pressure: `none`, `1m`, `5m`, `30m`, `over_30`. |
| `context_loaded` | Context is loaded. |
| `bg_view_alive` | Current background WKWebView exists. |
| `bg_process_alive` | Background process identifier is nonzero. |
| `bg_process_responsive` | WebKit's process-responsiveness verdict. |
| `network_restarted` | Network PID differs from the value at context load. |
| `tab_in_context` | Failing tab is in WebKit's open-tabs set. |
| `tab_controller_match` | Tab and context use the same extension controller. |
| `tab_has_ext_scripts` | Registered scripts include the expected extension world; not proof of execution. |
| `handler_registered` | Autoconsent native-message handler is registered; not proof of message delivery. |
| `critical_memory_age` | Time since critical memory pressure: `none` (not observed), `1m` (<1 min), `5m` (1<5 min), `30m` (5<30 min), `over_30` (≥30 min). |
| `bg_view_creations` | Creations since context load: `0`, `1`, `2`, `3_to_5`, `over_5`. |
| `bg_view_leaked_count` | Retained views beyond the expected current view; same buckets. Not proof of a leak. |
| `context_errors` | `none` or accumulated error descriptors: raw domain/code pairs, including the immediate underlying error. No localized text; no explicit payload cap. |
| `bg_events` | Recent lifecycle sequence with relative ages in 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.
The recorder keeps 40 events. `bg_events` sends up to the last 12, within 255 characters, dropping older entries first. Format: `event_name@-seconds` before the snapshot.
Events: `load`, `view`, `dealloc`, `unresponsive`, `responsive`, `proxy_on`, `proxy_off`, `died_<reason>`, `error_<descriptor>`, `context_unload_failed_<descriptor>`, `extension_files_remove_failed_<descriptor>`. Termination reasons: `memory_limit`, `cpu_limit`, `requested_by_client`, `crash`, `shared_crash_limit`, `unknown`. The `error_` events represent background-load failures. Unload failures during reload, data clearing or uninstall, and file-removal failures during uninstall, are also recorded for the tracked CPM extension. A failed unload preserves the tracked context; file-removal failures can still be recorded after successful unload. Existing standalone error pixels are unchanged; these timeline entries only travel with a later failure/stuck snapshot. Event labels are lowercased, character-filtered and capped at 64 characters.
Events: `load`, `view`, `dealloc`, `unresponsive`, `responsive`, `proxy_on`, `proxy_off`, `died_<reason>`, `error_<descriptor>`, `context_unload_failed_<descriptor>`, `extension_files_remove_failed_<descriptor>`.
Example: `view@-60,died_crash@-42,error_background_failed_to_load@-41`.
Termination reasons: `memory`, `cpu`, `client`, `crash`, `crash_limit`, `unknown`. The `error_` events record background-load failures. CPM unload failures (reload, data clearing or uninstall) and uninstall file-removal failures are also recorded. Failed unloads keep the context; file-removal errors can still be recorded after unload. These entries reach the server only with a later failure/stuck pixel; existing standalone error pixels are unchanged.
Timeline labels are lowercased, character-filtered and truncated to 64 characters, which can cut off error codes. `context_errors` and reload error fields preserve raw domain/code pairs.
Example: `view@-60,died_crash@-42` means the view was created 60 seconds before the snapshot and its process crashed 42 seconds before it.
### Reload-failure parameters
@@ -67,18 +71,18 @@ Example: `view@-60,died_crash@-42,error_background_failed_to_load@-41`.
|---|---|
| `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_phase` | `unload`, `load`, `light_load`, `full_load`, `fallback_load` |
The three fields above are allowlisted enums. The pixel also sends standard error parameters:
| Parameter | Values |
|---|---|
| `d` (`errorDomain`) | Original NSError domain, without category mapping. |
| `d` (`errorDomain`) | Original NSError domain. |
| `e` (`errorCode`) | Original numeric NSError code. |
| `ud` (`underlyingErrorDomain`) | Immediate underlying NSError domain, when present. |
| `ue` (`underlyingErrorCode`) | Immediate underlying numeric code, when present. |
Original domains/codes are preserved without category mapping. Localized descriptions, file paths and other userInfo contents are not serialized. The underlying error chain is not traversed beyond the immediate error.
Localized descriptions, file paths and other userInfo contents are not serialized. Only the immediate underlying error is included.
## Testing
@@ -90,7 +94,7 @@ Original domains/codes are preserved without category mapping. Localized descrip
**Privacy**
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.
New pixel and additional parameters require Privacy Triage. These payloads do not intentionally collect browsing URLs, search queries, page content, message payloads, tab/context IDs or PIDs. Exact-second timelines and unrestricted domain/code pairs create many distinct values; Privacy Triage must decide whether to bucket or restrict them. `data_clearing` discloses the operation preceding a failed reload.
Local context-error diagnostics log only original and immediate underlying error domains/codes, without localized descriptions or other userInfo contents.
@@ -100,7 +104,7 @@ Uses guarded private WebKit selectors for process/script facts. Validate support
**Site Breakage**
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.
No intended page-behavior changes. The proxy changed the bug's reproduction in an earlier test. Test the current proxy on/off to check whether it still affects the failure.
**Experimentation**
@@ -108,7 +112,7 @@ None. Recovery changes and experiments are outside this proposal.
**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; snapshots are captured synchronously at failure time.
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 cannot measure the failure rate across all process deaths or prove that all tabs failed. Older events, including the trigger, can drop out of the timeline.
**Localization / Internationalization**