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 c42e0c38..752b5ece 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 @@ -40,18 +40,18 @@ The monitor keeps, per tab: the time of the latest eligible commit (until a mess | Event | Effect | |---|---| -| Commit of an http(s) page, extension loaded, not an error page, not a history restore, not a restored session | The tab waits for a message. A message up to 2 s before the commit answers it (the extension can message a tab before the app reports the commit). | +| Commit of an http(s) page, extension loaded, not an error page, not a history restore, not a restored session | The tab waits for a message. A message up to 2 s before the commit answers it (the extension can message a tab before the app reports the commit), also for a tab whose extension tab is not learned yet. | | Message from the current extension (dashboard refresh) | Answers the tab's page, even when late. Ends the series of silent tabs. | -| `config_unavailable` or `settings_missing` refresh | Not an answer: the extension said it cannot answer the page's init. | -| Any event, with a commit older than 30 s and no message | The tab is silent: reported once. | +| `config_unavailable` or `settings_missing` refresh | Not an answer: the extension said it cannot answer the page's init. It still shows the extension is alive (restart budget reset). | +| Any event, with a commit older than 30 s and no message | The tab is silent: reported once. Tabs found silent in the same check are all `many`. | | New commit or tab close | A page that already waited 30 s is reported first; a younger one is dropped. | | Web content process terminated | The tab's page is dropped, not reported. | -| App suspended (iOS background, macOS sleep) | Nothing is checked, and the suspended time is added back to every wait. | -| Extension reload | All waits, mappings and series are forgotten. | +| App suspended (iOS background, macOS sleep) | Nothing is checked, and the suspended time is added back to every wait; a page committed while suspended starts waiting at resume. The clock is time since boot, so a wall-clock change cannot make tabs overdue. | +| Extension reload | All waits, mappings and series are forgotten; the open pages stay, so tabs are learned again. | ### Tying a message to a tab -A message carries the extension's tab identifier and the page URL. WebKit creates its extension tab object lazily, and neither public nor private API exposes its identifier, so the app learns it: the first message from an extension tab that matches exactly one committed page (host and normalized path, the same match the privacy dashboard uses) fixes that tab's identifier; later messages use it directly. A message that cannot be tied to one tab (two tabs on the same URL, no tab identifier, no match) answers every tab it could belong to, so the metric prefers missing a breakage to reporting a working tab. A tab closing forgets its identifier. +A message carries the extension's tab identifier and the page URL. WebKit creates its extension tab object lazily, and neither public nor private API exposes its identifier, so the app learns it: the first message from an extension tab that matches exactly one committed page (host and normalized path, the same match the privacy dashboard uses) fixes that tab's identifier; later messages use it directly. A message teaches a tab only while that tab still waits for its first message: a message about an already answered page may belong to the page before. A message that cannot be tied to one tab (two tabs on the same URL, no tab identifier, no match) answers every tab it could belong to, so the metric prefers missing a breakage to reporting a working tab. A tab closing forgets its identifier. ### Pixels @@ -66,7 +66,7 @@ A message carries the extension's tab identifier and the page URL. WebKit create | Parameter | Values | |---|---| -| `tabs` | `one`: no other tab was silent since the extension last sent a message; the page is the likely cause. `many`: another tab was already silent; the extension is the likely cause. | +| `tabs` | `one`: no other tab was silent since the extension last sent a message; the page is the likely cause. `many`: another tab was silent too, before or in the same check; the extension is the likely cause. | `web_extension_cpm_breakage_persistent` has no parameters. `last_stage` is added by the companion design. @@ -83,9 +83,9 @@ The episode pixel `web_extension_cpm_episode` with its timers, episode state and | PR | Content | State, 11 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` (`73bdf742b0`) | All removal. The monitor shrinks to learning which extension tab belongs to which app tab and letting the manager reset its restart budget. Tabs report one commit event with an eligibility flag; navigation start, finish and failure events are gone. | local, one commit on `6928847174`; 1004 package tests pass; not pushed | -| 3, `alex/cpm-breakage-metric` (`cf83e35c12`) | Per-tab silence, the two pixels, `tabs`, definitions, adapters | local, one commit on PR 2; 1034 package tests pass; validators pass; not pushed | -| 4, `alex/cpm-init-marks` (`6e28de5191`) | Init progress marks and `last_stage`; extension #3727 | local, one commit on PR 3; 1044 package tests pass; not pushed | +| 2, `alex/cpm-pixel-cleanup` (`de24abf498`) | All removal. The monitor shrinks to learning which extension tab belongs to which app tab and letting the manager reset its restart budget. Tabs report one commit event with an eligibility flag; navigation start, finish and failure events are gone. | local, one commit on `6928847174`; 1004 package tests pass; not pushed. iOS error pages are ineligible; back/forward is read at navigation start | +| 3, `alex/cpm-breakage-metric` (`c6f163df4e`) | Per-tab silence, the two pixels, `tabs`, definitions, adapters | local, one commit on PR 2; 1047 package tests pass; validators pass; not pushed | +| 4, `alex/cpm-init-marks` (`b241294712`) | Init progress marks and `last_stage`; extension #3727 | local, one commit on PR 3; 1069 package tests pass; not pushed | ### Known limits @@ -96,7 +96,8 @@ The episode pixel `web_extension_cpm_episode` with its timers, episode state and - A message within 2 s before a commit is counted as that commit's answer even when it belonged to the previous page. - The dashboard refresh is sent independently of `initResp`, so a page whose `initResp` was never delivered counts as answered. - The first series has no overlap with the episode series: comparison is by app version against the previous derived number. -- The restart budget now resets on any message from the current embedded extension, not on a message matched to a page. +- The restart budget now resets on any dashboard refresh from the current embedded extension, not on a message matched to a page, and without the old http(s) and generation filters. +- A page that navigates before the 30 s window ends is dropped, so a user who navigates every 20 s with a dead extension is never reported. - If the native channel itself is lost, every tab looks silent and `tabs=many` follows: the metric cannot tell a lost channel from a broken extension. ### Cons diff --git a/work/projects/cpm-project-summary-2026-11-03.md b/work/projects/cpm-project-summary-2026-11-03.md index 68fb5610..17730384 100644 --- a/work/projects/cpm-project-summary-2026-11-03.md +++ b/work/projects/cpm-project-summary-2026-11-03.md @@ -123,9 +123,9 @@ Open: **Пиксели.** Остаются два: `web_extension_cpm_breakage` (параметр `tabs`: `one` или `many`, то есть была ли уже молчащая вкладка с последнего сообщения расширения) и `web_extension_cpm_breakage_persistent` (вторая молчащая вкладка в серии, один раз на серию, без параметров). Старые `web_extension_cpm_episode`, `web_extension_cpm_background_load_restart`, `web_extension_cpm_network_process_recovery_applied` удалены в PR 2, отдельного PR 5 нет. Метки `cpmInitProgress` и `last_stage` (PR 4) привязаны к молчащей вкладке; метка никогда не считается ответом. **Раскладка (локально, по одному коммиту на ворктри, не запушено, PR не создавались).** -- PR 2 `alex/cpm-pixel-cleanup`: `73bdf742b0` на `6928847174`. Вся чистка, монитор урезан до сопоставления вкладок и сброса бюджета перезапусков; вкладки шлют одно событие коммита с флагом `isEligible`, события старта/финиша/ошибки навигации удалены. 1004 теста пакета. -- PR 3 `alex/cpm-breakage-metric`: `cf83e35c12` на PR 2. Молчание по вкладкам, два пикселя, определения обеих платформ, адаптеры. 1034 теста пакета. -- PR 4 `alex/cpm-init-marks`: `6e28de5191` на PR 3. Метки init по вкладкам и `last_stage`. 1044 теста пакета. Расширение `#3727` не менялось. +- PR 2 `alex/cpm-pixel-cleanup`: `de24abf498` на `6928847174`. Вся чистка, монитор урезан до сопоставления вкладок и сброса бюджета перезапусков; вкладки шлют одно событие коммита с флагом `isEligible` (iOS: error page не измеряется, back/forward читается на старте навигации), события старта/финиша/ошибки навигации удалены. 1004 теста пакета. +- PR 3 `alex/cpm-breakage-metric`: `c6f163df4e` на PR 2. Молчание по вкладкам, два пикселя, определения обеих платформ, адаптеры. После ревью: вкладки, замолчавшие в одной проверке, все `many`; сообщение до коммита отвечает и для ещё не выученной вкладки; монотонные часы; refresh без ответа (`config_unavailable`/`settings_missing`) не ответ, но сбрасывает бюджет перезапусков. 1047 тестов пакета. +- PR 4 `alex/cpm-init-marks`: `b241294712` на PR 3. Метки init по вкладкам и `last_stage`; метка учитывается только для текущей страницы вкладки, метки до коммита приписываются новой странице. 1069 тестов пакета. Расширение `#3727` не менялось. Старые коммиты сохранены как `refs/backup/cpm-old-pr3` (`82d29f412a`) и `refs/backup/cpm-old-pr4` (`e8dc96dec2`) в репозитории `apple-browsers.git`. **Проверено:** `swift test` пакета на каждой ветке, мутационные проверки (ответ после проверки, нет сдвига на приостановку, persistent на одной вкладке, нет допуска, метка считается ответом, ранг понижается), валидаторы определений пикселей обеих платформ, SwiftLint strict на изменённых файлах, `xcodebuild build-for-testing` macOS. **Не запускалось:** app-hosted тесты macOS и iOS (кроме трёх тестов `NavigationPixelNavigationResponderTests`, запущенных до запрета), сборка iOS (падает на двух script phases, окружение), реальный WebKit. 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 dc1f0d36..c5b03ca5 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 @@ -73,7 +73,7 @@ Marks are best effort: they carry `{ tabId, url, stage }`, never wait and never ### Native side - `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 the furthest stage per tab and forgets it at the next commit (unless it is within the 2 s tolerance), when the tab closes or crashes and on an extension reload. +- `CPMMessagingHealthEvent.initProgress` carries the mark; the monitor stores the furthest stage per tab for the tab's current page only (a mark about another page is not counted) and keeps marks for 2 s so a mark that arrives just before the commit counts for the new page; the stage is reset at every commit, when the tab closes or crashes and on an extension reload. - A mark does not answer a page and does not end a series. The breakage report takes `last_stage` from the tab it reports, not from any other tab. - The Debug "Simulate CPM Messaging Breakage" switch hides marks as well as dashboard refreshes, and marks from an old embedded extension are ignored. @@ -140,13 +140,13 @@ Compatibility and sequencing: - **The marks change the behavior they measure** (a heisenbug). Mitigation: internal builds first, compare breakage rates and the `last_stage` mix between releases with and without marks; if the cost is visible, drop `config_read` and `site_checked` in the next extension release. - **`response_sent` can be a false positive.** If `tabs.sendMessage` resolves without a listener (WebKit can resolve `undefined` for a message with no receiver), a page without the content script would show `response_sent`. It never answers a page, so the metric is not affected; only the reading of `last_stage=response_sent` is. A real-WebKit test with no listener should settle it. - **Marks arrive out of order or late.** Handled by rank; a tab reported before its last mark arrives carries an earlier `last_stage`. Marks arriving after the report are ignored. -- **A mark for a tab that is not mapped yet** is dropped when its URL matches two tabs, so `none` also covers marks lost to attribution. +- **A mark for a tab that is not mapped yet** is dropped when its URL matches two tabs (a mark before the first commit is kept for 2 s and counted for the page that commits), so `none` also covers marks lost to attribution. - **A hostile page forges marks.** The content script runs in an isolated world; the page cannot call `chrome.runtime`. The background accepts a page-side mark only for `page_init_sent` and only from the main frame. ## Testing - Extension: `make unit-test` (the cookie-prompt-management suite), `npm run tsc`, `npm run lint`. The uncommitted part has not been run. -- Native unit tests (PR 4, package suite 1044 tests): the report carries the furthest stage of the silent tab and not of another tab; no marks give `none`; a mark is not an answer; unknown stage ignored by the handler; a mark that cannot be tied to one tab dropped; a mark just before the commit belongs to the new page and an older one is forgotten; old extension and simulation ignored; closed tab forgets its marks; `last_stage` in the pixel on both platforms. Mutation checks (a mark answers, rank lowers, stage kept forever, stage always cleared) each fail a test. App-hosted tests were not run. +- Native unit tests (PR 4, package suite 1069 tests): the report carries the furthest stage of the silent tab and not of another tab; no marks give `none`; a mark is not an answer; unknown stage ignored by the handler; a mark that cannot be tied to one tab dropped; a mark just before the commit belongs to the new page and an older one is forgotten; old extension and simulation ignored; closed tab forgets its marks; `last_stage` in the pixel on both platforms. Mutation checks (a mark answers, rank lowers, stage kept forever, stage always cleared) each fail a test. App-hosted tests were not run. - Real WebKit (CPM repro harness, [PR #7378](https://github.com/duckduckgo/apple-browsers/pull/7378)): the full chain shows every mark in order on a healthy page; a page without the content script yields no `response_sent`; an injected stall at each await shows the matching `last_stage`; a delay on the native round trips for `config_read` and `settings_checked`. These runs have not been done. - Pixel definitions pass both platforms' validators in CI. - Privacy check of real payloads: the marks contain `url` on the extension-to-app channel only; the pixel carries the enum only. @@ -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, `alex/cpm-init-marks`, `6e28de5191`, on top of PR 3 `cf83e35c12`) can ship before the extension; the extension PR merges after review and enters the app through the usual embedded-files update. +- Order of landing: native handler and `last_stage` (PR 4, `alex/cpm-init-marks`, `b241294712`, on top of PR 3 `c6f163df4e`) 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 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.