# CPM Web Extension breakage — полное summary сессии > **Текущее продолжение — 15 октября 2026:** [[cpm-status-2026-10-15]]; актуальная карта worktree — [[cpm-web-extension-code-guide-2026-10-09]] (§0). Разделы 1–9 ниже относятся к 7 октября, §10 — к 9 октября; они не являются последним статусом. > > Добавлены 14 P5/P6 tests, все текущие методы имеют green в раздельных запусках. Найдены и проверены пути browser window-close → WebKit forgetTab → P5/tab=false и условный main-feature-flag handler cycle → replacement controller → P6. Для P5/tab=true, естественной потери metadata, actual iOS suspend/resume и production main-flag transitions доказательств нет. Полный AppKit flow не запускался; browser source contract прочитан, production shared classes и system WebKit исполнены. > > Пакет: 478 XCTest / 28 opt-in skipped / 0 failures; strict SwiftLint — 0. Ранние красные ожидания, проблемы постановки и отрицательные результаты не удалены: см. основной статус §10–12. Состояния PR/CI и нарезка worktree теперь проверены отдельно; прежнее «всё локально» относится к датированной части. **Дата:** 7 октября 2026 **Охват:** public-attribution после релиза iOS 7.240 / macOS 1.210, исходный код Browser и WebKit, release bundle CPM, WebKit tracker и upstream regression tests. ## Короткий вывод > Разделы 1–9 — состояние на 7 октября; раздел 10 — продолжение на 9 октября. Текущий статус — [[cpm-status-2026-10-15]]. Главный production-сигнал — не доказанная потеря `initResp`, а отсутствие в течение 4 секунд native-вызова `refreshCpmDashboardState` после завершения навигации. Поэтому большая группа, где native-снимок выглядит здоровым, означает: WebKit controller/context/scripts могли существовать, но CPM не прислал или native не сопоставил ожидаемый dashboard refresh. Она не доказывает, что extension вообще не загрузился. Наиболее близкие к этому симптому WebKit-кандидаты — гонки при cold start service worker: 1. **317981:** content script отправляет `runtime.sendMessage`, пока background ещё загружается; WebKit не должен терять сообщение. 2. **324743:** worker считается загруженным и получает сообщение до завершения activation/восстановления clients. Два других WebKit-багa могут объяснять ухудшение после перезагрузки/recovery, но пока не доказано, что они происходят в CPM: 3. **322881:** после reload worker теряет существующие extension clients. 4. **324754:** при site isolation перезагруженный worker может оказаться в новом процессе без Web Extension bindings. M4 recovery даёт сильный сигнал восстановления измеряемого background → native пути, но не подтверждает восстановление `initResp` или всей CPM-функции. Следующая работа: воспроизвести upstream-сценарии на system WebKit и одновременно добавить event-level instrumentation, которая разделит content `init`, worker receive, `initResp` и dashboard refresh. --- ## 1. Что измеряет current telemetry ### Основной сигнал После `navigationDidFinish` `CPMMessagingHealthMonitor` ждёт до 4 секунд native `dashboardResponse`. Он приходит только когда background вызывает `refreshCpmDashboardState`, а native handler принимает и сопоставляет его с текущим документом. ```text navigationDidFinish → ожидание dashboardResponse (4 s) → нет ответа → initialization.failed.after.*.daily → при следующем qualifying timeout в пределах episode → messaging.stuck.*.daily → M4 reload embedded extension ``` Следствия: - `initialization.failed` не равно «content script не отправил init». - `initialization.failed` не равно «background не получил init». - `initialization.failed` не равно «initResp не дошёл до content script». - `context_loaded`, живой background view, зарегистрированные scripts и native handler не подтверждают, что service worker уже активирован или обработал именно этот message. `context_loaded` в WebKit означает, что controller уже назначен. Это происходит раньше асинхронной подготовки storage, создания background view, регистрации scripts и injected content. `tab_has_ext_scripts` отражает зарегистрированные `WKUserScript`, а не факт их исполнения в этом документе. ### Почему `initResp` и dashboard refresh не одно и то же В release bundle content script отправляет `init` в background. Успешная ветка background запускает `updateCpmDashboardState(...).then(refreshDashboardState(...))` без `await`, а затем отдельно отправляет `initResp` во вкладку. В части веток (`config_unavailable`, `settings_missing`, `setting_disabled`, `site_disabled`) отправляется dashboard refresh, но `initResp` не отправляется. Значит возможны оба расхождения: - `initResp` был, но refresh задержался, не пришёл или не сопоставился — monitor сообщает failure. - refresh пришёл, но `initResp` не было — monitor считает путь здоровым. Дополнительные реальные задержки до refresh: remote config, native settings/site checks, последовательная async queue, session storage. JS native-request timeout в bundle — 20 секунд, тогда как monitor ждёт 4 секунды. URL сопоставляется по scheme, host, port, path и query; поздний refresh или другой query также дают failure при работающем JS. --- ## 2. Public attribution: охват и контрольные суммы Окно: полный UTC-день 5 октября 2026, только события после release tag каждой платформы. `.daily` — максимум одно событие каждого имени в сутки на установку. Это не пользователи, не эпизоды и не transition rate. | После release tag | iOS 7.240 | macOS 1.210 | |---|---:|---:| | Первое `initialization.failed.after.*.daily` | 6 624 | 1 377 | | Последующее `messaging.stuck.*.daily` | 1 440 | 487 | | Первый неудачный CPM probe после reload | 670 | 232 | | `recovered.after.extension.reload.daily` | 963 = 916 stuck + 47 init | 362 = 354 stuck + 8 init | | Все CPM daily по именам | 11 252 | 2 804 | Контроль: 8 001 init + 1 927 stuck = основные первичные/вторичные строки после tag. До macOS tag в выгрузку попали 28 init, 2 stuck и 39 CPM daily; они вынесены из public-среза. До iOS tag строк этой версии нет. ### Группы снимков на первом failure Каждая строка относится к одной группе по приоритету. Это описание состояния в момент timeout, не первопричина. | Группа | iOS init | macOS init | iOS stuck | macOS stuck | |---|---:|---:|---:|---:| | Native snapshot без видимой ошибки | 3 717 | 911 | 789 | 335 | | Tab attachment / injection | 931 | 116 | 53 | 38 | | Background state | 768 | 158 | 457 | 35 | | Только история context errors | 343 | 158 | 87 | 79 | | Недостаточно tab facts | 865 | 34 | 54 | 0 | Крупнейшая группа — **3 717 iOS / 911 macOS** — не является подтверждением «нормальной загрузки». Она только не содержит видимого native-признака поломки на момент снимка. ### Главные окружающие цепочки - **Session restoration:** 952 iOS / 68 macOS init failures. На iOS у 604 controller совпадает, scripts отсутствуют, background view не создавался; 754 лежат в первых 30 секундах запуска. Это согласуется с временным startup miss, но 260 снимков выглядят здоровыми и требуют timings сообщений. - **Обычные навигации iOS:** часты `load → view → proxy_on` и `load → view → proxy_on → dealloc → view → proxy_on`. `dealloc` означает освобождение background view, не process crash. Явный `died_*` есть лишь в 85 первичных iOS строках. - **После extension reload:** 670 iOS / 232 macOS первых timeout. Типичный хвост содержит `load → base_url_changed → view → proxy_on`; `base_url_changed` сопутствует reload и не доказывает URL defect. - **macOS controller/scripts mismatch:** после tag 77/1 377 init и 35/487 stuck имеют неверный controller и отсутствующие scripts; до tag таких строк нет. 45 из 77 совпали с `app_version_change=updated`, но update сам по себе не доказывает, что живая вкладка осталась на старом controller. Нужен отдельный event смены manager/controller при живой вкладке. - **WebKit error 6:** в 364 iOS stuck снимках есть `WKWebExtensionContextErrorDomain:6`; в 363 перед ним есть `dealloc`. Это `BackgroundContentFailedToLoad`: service worker/background не удалось загрузить. Пиксель не содержит первичную причину registration/load failure. --- ## 3. Связь init failure и stuck В коде связь прямая: первый timeout создаёт `Episode`; stuck появляется только после следующего qualifying timeout, обычно в пределах пяти минут. Причина stuck наследуется от первого failure, а stuck snapshot снимается со второй вкладки. Recorder хранит общую history background context, поэтому `stuck.bg_events` может содержать события до первого init failure. В public-срезе сравнивались длинные цепочки (`>=6` событий) в окне пяти минут: порядок токенов и возраста событий должны были совпадать. Контроль — те же правила, но stuck timestamps сдвинуты на ±10 минут. | Platform | Stuck | Длинная цепочка | Совпала с более ранним init | Контроль +10 / −10 min | Один кандидат | |---|---:|---:|---:|---:|---:| | iOS | 1 440 | 1 025 | 693 | 255 / 270 | 467 | | macOS | 487 | 368 | 168 | 29 / 19 | 148 | Это подтверждает, что stuck чаще продолжает предшествующий timeout-эпизод, чем случайную соседнюю цепочку. Это не соединение одного пользователя: нет reporter ID, `.daily` дедуплицирует события, короткие хвосты недостаточно уникальны. **Важно:** один потерянный `init` сам по себе не доказывает global stuck. Для production stuck нужны два qualifying timeout в одном episode. Поэтому тест «один tab потерял init, второй сам сломался» не был честным доказательством и не получен. Проверять нужно: два timeout в одном стартовом окне → настоящий M4 recovery → третья вкладка без injection. --- ## 4. Проверка релизов и mitigation ### Session-restoration gate M7 gate уже ждёт initial `loadAndSync` перед первой main-frame HTTP(S) navigation restored tab с bounded timeout. По public data временные session-restoration failures **не исчезли**: 952 iOS / 68 macOS. Без связки событий нельзя посчитать, какая часть затем восстановилась. ### Tab lifecycle fixes Новые controller/scripts поля появились только в новом атрибуционном релизе, поэтому старые версии нельзя сравнивать по tab bugs напрямую. Доступен только proxy `stuck.other / init.other` на macOS: | Версия | Proxy | |---|---:| | 1.208 | 39.52% | | 1.209, содержит M1 tab identity fix | 38.81% | | 1.210 public day | 39.81% | Разница 1.208 → 1.209 составляет 0.71 п.п. и не доказывает эффект или его отсутствие: различаются cohort и нагрузка. ### M4 after-stuck recovery M4 делает reload embedded extension после подтверждённого hang. После tag `recovered.after.extension.reload.daily` с `recovery_from=messaging_stuck` есть у 916 iOS и 354 macOS. Отношение всех `recovered.after` к stuck daily: 66.9% iOS и 74.3% macOS; на предыдущих версиях proxy был 3.23% / 2.70%. Это сильный признак, что reload часто возвращает измеряемый background → native refresh. Это **не success rate** и не доказательство восстановленного CPM/initResp. После reload первый probe всё ещё timeout-ится 670 раз на iOS и 232 на macOS. --- ## 5. Другие CPM/Web Extension сигналы После tag Web Extension продолжает работать на значимой части навигаций: | Signal | iOS | macOS | Что доказывает | |---|---:|---:|---| | `autoconsent_init.daily`, `fromExtension=1` | 56 906 | 2 825 | Background получил какой-то init и отправил native pixel; не delivery `initResp` | | `web_extension_loaded.daily` | 61 902 | 2 870 | Manager загрузил хотя бы одно extension | | `web_extension_state_checked.daily` | 62 004 | 2 805 | Lifecycle проверил loaded contexts | | `web_extension_embedded_not_loaded.daily` | 3 389 | 1 | Embedded extension отсутствовал в manager во время проверки | | `web_extension_load_error.daily` | 4 | 4 | Ошибка общей загрузки extension | Также есть extension-sourced `popup-found` и `done`, значит CPM functional path работает у части навигаций. Общие имена пикселей разделены с legacy user script: выводы допустимы только с `fromExtension=1`. На macOS лишь один `embedded_not_loaded.daily` при 77 init failures с controller mismatch/no scripts. Это согласуется с локальной проблемой вкладки при исправном глобальном manager. --- ## 6. WebKit hypotheses ### 317981 — сообщение в cold start worker Upstream regression test `SendMessageFromContentScriptWhileBackgroundIsLoading`: 1. Загружает MV3 extension с non-persistent background. 2. Background синхронно регистрирует `runtime.onMessage`. 3. Сразу открывает web tab без ожидания background readiness. 4. Content script сразу вызывает `runtime.sendMessage`. 5. Ожидает ответ background. Правильное поведение — очередь до registration listener. Без фикса сообщение может быть потеряно. Это ближе всего к цепочке `scripts/context есть → init отправлен → нет response/refresh` и не требует искусственного JS timeout. Статус: fix 317981 присутствует в public Safari/WebKit ветке `safari-7625.1.29.*`. macOS 27 VM из проверки имела WebKit bundle `22625.1.29.11.27`; точное соответствие Apple OS build и WebKit commit нельзя доказать лишь CFBundleVersion. Нужен system-framework regression run на конкретной VM, а не предположение по номеру. ### 324743 — worker получает event до activation Upstream test выгружает worker, будит его `runtime.sendMessage` из extension tab и прямо в message listener проверяет `clients.matchAll()` без polling. Offscreen document обязан быть уже controlled client. Без фикса worker обрабатывает message, пока activation/reconnect ещё не завершились. Это также похоже на primary telemetry: controller/view выглядят живыми, но реальная обработка message идёт в неправильный момент. ### 322881 — reload потерял extension clients Upstream test создаёт offscreen document, проверяет его presence в `clients.matchAll()`, reload-ит background и проверяет, что новый worker вновь видит тот же client. В production relevance пока не установлена: CPM обычно работает с web tab/content script, а не обязательно с extension-owned offscreen page. ### 324754 — worker в новом process без bindings Upstream test включает site isolation, создаёт offscreen document, reload-ит worker, проверяет смену PID и затем наличие `browser.runtime.getURL`. Без фикса JavaScript worker стартует в процессе без Web Extension API bindings. Это хороший кандидат для проблемы после reload/recovery, но не доказанный источник current CPM data. Сначала нужен upstream-shaped reproduction с offscreen/extension page, затем full CPM scenario. ### Связь с error 6 Error 6 частично ложится на путь `refreshCpmDashboardState`: если background не загрузился, он не вызовет refresh. Но error 6 не говорит, где именно сломалось: service-worker registration, process, storage, bindings или другая внутренняя WebKit ошибка. Он не доказывает 317981, 324743, 322881 или 324754. --- ## 7. Fix в browser code В рабочем дереве `alex/cpm-web-extension-ready-handshake` подготовлен некоммитнутый JS fix, ориентированный на потерянное/слишком раннее `init` сообщение: content side ждёт explicit readiness/ответ и повторяет handshake вместо того, чтобы считать один message окончательным. ZIP с extension — release artifact; в нормальной жизни он собирается из исходников расширения и попадает в bundle приложения на build/release pipeline, а не загружается пользователем. Смысл фикса: не менять WebKit и не «лечить» искусственной задержкой, а сделать CPM handshake идемпотентным. Если первый init попал в cold-start race, content script повторно инициирует состояние и получает актуальный dashboard path. Это покрывает потерю первого сообщения/ранний readiness contract, но не восстановит worker без `browser.runtime` и не исправит WebKit process/client bug сам по себе. Существующий fixture с `setTimeout` полезен как test retry protocol, но не доказывает настоящую WebKit race. Он показал важное ограничение: единичная потеря init не делает все tabs stuck автоматически. --- ## 8. План точного воспроизведения без изменения WebKit logic ### Почему не C++ `DYLD_INTERPOSE` Нужные C++ пути выполняются внутри отдельных WebContent/Network processes и используют internal hidden symbols. Вызовы внутри WebKit dylib обычно не проходят через interposable imported symbol stubs. Инъекция dylib в Apple WebContent subprocesses ненадёжна из-за hardened runtime, SIP и XPC environment. Это не практичный первый метод. ### Рабочий harness 1. Создать Swift integration harness с Objective-C runtime swizzle и `RaceGate`; тесты запускать serially, потому что swizzle process-global. 2. Для 317981 не ставить gate: портировать exact upstream cold-start race и многократно запускать на system WebKit. 3. Для 324743 swizzle `-[WKWebExtensionContext loadBackgroundContentWithCompletionHandler:]`: сообщить completion test coordinator раньше, а оригинальный вызов удержать до explicit release. Это контролирует scheduling на публичной ObjC boundary, не меняя extension logic или C++ WebKit code. 4. Для real lifecycle использовать доступные private test SPI динамически: `_terminateServiceWorkers`, `_backgroundWebView`, `_killWebContentProcessAndResetState`, `_webProcessIdentifier`, `_setSiteIsolationEnabled:`. 5. Логировать OS build, WebKit CFBundleVersion, timestamps init/response, background PID до/после, outcome каждой итерации. ### Матрица | Сценарий | Реальный триггер | Успех/отказ | |---|---|---| | 317981 | Сразу открыть tab после extension load | Каждый init получает ответ/refresh | | 324743 | Gate вокруг background load completion, tab уже навигировался | Message не должен исполняться до activation | | 322881 | Создать extension client, `_terminateServiceWorkers`, разбудить message | Client есть в `clients.matchAll()` | | 324754 | Site isolation, убить/restart background, дождаться нового PID | В новом worker есть `browser.runtime.*` | | CPM amplification | Два timeout в одном shared start window, настоящий M4 reload, затем third tab без injection | Third tab работает; иначе доказан cascade/recovery defect | Нельзя подтверждать global stuck одним искусственно потерянным init на одной вкладке. Для честного test нужны два timeout, которые переводят health monitor в stuck, после чего injection выключается перед третьей вкладкой. --- ## 9. Какие данные нужны дальше Приоритетная telemetry с общим, privacy-safe navigation/episode correlation ID: 1. `content_init_sent`. 2. `background_init_received` и окончание async prerequisites. 3. `initResp_sent` / `initResp_received`. 4. `refreshCpmDashboardState_sent` / `received` / URL match result. 5. worker lifecycle: start, activation, PID/process generation, background load error code, recovery generation. Эти точки разделят пять разных случаев, которые сегодня попадают в один `init failure`: content injection miss, runtime message miss, worker activation race, delayed/mismatched dashboard refresh и native routing failure. --- ## 10. Статус на 9 октября Подробно: [[cpm-web-extension-repro-and-fix-2026-10-08]]. Сводки на английском для Asana: [[cpm-web-extension-data-analysis-summary-2026-10-08]] и [[cpm-web-extension-repro-and-fixes-summary-2026-10-08]]. Карта кода по строкам: [[cpm-web-extension-code-guide-2026-10-09]]. **Ничего не закоммичено.** **Воспроизведено и закрыто фиксами (worktree `alex/cpm-web-extension-ready-handshake`):** - Harness на реальном WebKit (раздел 8): production extension, `WebExtensionManager`, coordinator + navigation gate, монитор, M4, production-recorder диагностики. - **Одна вкладка:** потеря `init` при первой загрузке background нового идентификатора (установка или обновление), 317981. При запуске её закрывает gate coordinator'а; установку и обновление посреди сессии закрыл новый gate. - **Полный отказ:** падение Network-процесса оставляет background живым, а worker мёртвым; ломаются все вкладки и открытые документы; M4 чинит только новые навигации. Закрыто watchdog'ом, перезапускающим только background-процесс. - **Смерть worker'а при живом Network-процессе** из приложения не обнаружить; закрыто перезапуском background по первому сбою (стоит одну навигацию). **Риск:** при медленном пробуждении, когда процесс отвечает, перезапуск убьёт грузящийся worker. **Сверка с данными 5 октября:** - Network-процесс объясняет не больше 20–45% «здоровых» `stuck`. - **Группа 1** («здоровые» без смены Network-процесса, iOS 748 / macOS 228 daily): около половины — сразу после пробуждения background после idle-выгрузки; около четверти iOS и 15% macOS — после загрузки или reload контекста. - **Поправка:** `recovered_without_extension_reload(initialization_failed)` к stuck не относится — он закрывает только init-эпизоды. Init → recovery без reload: 23% iOS / 25% macOS устройство-дней; stuck → recovery после M4: 64% / 74%. Сбои после загрузки контекста чаще проходят сами; на пробуждении чаще доходят до stuck. **Новые находки 8–9 октября (worktree `alex/cpm-web-extension-remaining-failures`, без фиксов):** - **Медленный старт worker'а на пробуждении** (> 4 с) воспроизводит сигнатуру группы 1 полностью: `bg_process_responsive=true`, `context_errors=none`, хвост `dealloc → view → proxy_on@30s`. 5 с → `initialization_failed`, затем поздний ответ; 12 с + следующая навигация → `stuck` → M4, который убивает живой медленный worker. Старт до 2,5 с — потерь нет. Почему на устройствах старт бывает долгим — неизвестно. - **Error 6 (iOS, ~1/3 строк):** причина в WebKit — 30-секундный таймер выгрузки взводится до окончания загрузки. Репро на macOS есть, на iPhone (iOS 26.7.1) не воспроизвёлся. - **WebKit 320812 / 312231@main (26.6+):** при смене `baseURL` загрузка контекста удаляет website data старого origin, включая весь дисковый кэш, и только потом инъецирует content scripts и грузит background. У DDG `baseURL` новый на каждом запуске и reload. На Mac с ~20–24 тыс. записей кэша задержка 0,75–1,35 с. Кандидат-митигация — стабильный `baseURL` (ветка `alex/cpm-stable-base-url`). - **Другие баги WebKit:** 324743 (worker «загружен» до завершения регистрации; исправлен 22 сен, нет в 26.7/27.0) и 314194 (Lockdown Mode блокирует все service worker'ы; открыт). - **Сигналы готовности background:** окончание загрузки страницы background view бесполезно (0,1 с); completion `loadBackgroundContent` и первое нативное сообщение worker'а точно отмечают его старт. - **Отброшено:** блокировка устройства / data protection, устаревший URL вкладки на iOS, мигание флага для привязки вкладок на macOS. **Телеметрия фазы 1 (worktree `alex/cpm-messaging-readiness-telemetry`, от свежего `main`):** - На сбоях: `bg_ready_after`, `bg_view_age`, `app_state`, `open_tabs`, `loading_tabs`, `bg_load_error_app_state`, `bg_load_error_after`. - На восстановлениях: `recovery_document`, `recovery_after`. - Проверено на реальном WebKit; 677 тестов пакета, SwiftLint, `validate-pixel-defs`, сборка iOS и macOS — зелёные. **Дальше:** фаза 2 детектора (отсчёт 4 с с момента готовности background, без эскалации до неё), раннее пробуждение background, стабильный `baseURL`, флаги и A/B для фиксов, репорты в WebKit. Не воспроизведены: привязка вкладок на macOS, «нет background view», код 7, первый сбой после M4. --- ## Источники - Public attribution: `cpm-web-extension-public-attribution-2026-10-05.md` и папка `cpm-public-attribution-2026-10-06/` в `apple-browsers.git`. - Historical validation/methodology: `cpm-web-extension-breakage-validation.md`. - WebKit 317981 regression: `Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPIRuntime.mm`, `SendMessageFromContentScriptWhileBackgroundIsLoading`. - WebKit 322881/324743/324754 regressions: `Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPIOffscreen.mm`. - Browser implementation: `CPMMessagingHealthMonitor.swift`, `CPMMessagingDiagnosticsRecorder.swift`, `WebExtensionManager.swift`, `AutoconsentWebExtensionMessageHandler.swift`.