--- title: CPM Web Extension — единый статус (PR, данные, гипотезы, телеметрия) date: '2026-10-15' tags: - work - cpm - status --- # CPM Web Extension: единый статус на 15 октября 2026 Внутренний документ. **Цифры из ClickHouse, в публичные PR, комментарии и issues не переносить** (репозитории `duckduckgo/apple-browsers` и `duckduckgo/duckduckgo-privacy-extension` публичные). Заменяет заметку `cpm-telemetry-data-requirements-2026-10-15` (теперь ссылка сюда). Старые документы с устаревшими числами: [[cpm-web-extension-public-attribution-2026-10-05]], [[cpm-web-extension-data-analysis-summary-2026-10-08]], [[cpm-web-extension-repro-and-fix-2026-10-08]], [[cpm-web-extension-code-guide-2026-10-09]] (карта «большого» worktree, **больше не актуальна**: всё нарезано на PR ниже). ## 1. Коротко - Фикс **B** (падение Network-процесса) проверен локально и красным прогоном на CI. **Основание в данных есть, но неполное:** обогащение в stuck есть, а доказать, что строки это именно мёртвый worker, нельзя (не хватает `network_replaced_since_view`). - Фикс **A** (первая загрузка посреди сессии) реален, но мелкий: около 1% stuck на iOS, на macOS почти не виден, чаще самоизлечивается. - Три самые крупные группы сбоев фиксами A и B **не закрыты**: пробуждение после idle, error 6 (iOS), «нет background view» на запуске (iOS). Механизмы воспроизведены локально на macOS, но проверенного рычага в приложении нет. - Телеметрия PR0 пока в виде, вызвавшем вопросы о склейке пикселей. Решение по правкам не принято (раздел 8). ## 2. Цепочки PR (все в `duckduckgo/apple-browsers`, задачи Asana в телах PR; не читать) **Телеметрия:** | PR | Ветка | База | Что | Статус | |---|---|---|---|---| | #7376 (PR0) | `alex/cpm-messaging-readiness-telemetry` | main | параметры готовности и тайминга на failure/recovery пикселях (без новых пикселей) | CI зелёный, нужен ревью. Ревьюер по пикселям задал вопросы JA1–JA3 о склейке, ответ не отправлен | | #7384 (0b) | `alex/cpm-readiness-baseline` | #7376 | ежедневные базлайн-пиксели `cpm_background_ready_{context_load,wake}`, нагрузка устройства, возраст возобновления | CI в основном зелёный | | #7385 (0c) | `alex/cpm-init-progress-native` | #7384 | `init_stage` на failure-пикселях (нужен JS из privacy-extension#3727) | CI в основном зелёный | | privacy-extension #3727 | `alex/cpm-init-progress` | | уведомления `cpmInitProgress` из встроенного расширения | открыт, нужен ревью | **Harness и фиксы** (красный коммит, затем зелёный; не ребейзить и не форс-пушить, иначе пропадёт доказательство красного прогона): | PR | Ветка | База | Что | Статус | |---|---|---|---|---| | #7378 | `alex/cpm-repro-harness` | main | harness (только тесты); 4 быстрых теста в CI около 3 с, остальное локально через `CPM_REPRO=1` | CI зелёный, нужен ревью. Danger: 1570 добавленных строк (предупреждение). В ветке есть коммит другой сессии «Require initResp in the page…» | | #7379 | `alex/cpm-fix-init-loss` | harness | фикс A: ожидание первой загрузки background после установки/апгрейда посреди сессии | красный прогон и зелёный: CI зелёный | | #7381 | `alex/cpm-fix-network-crash` | фикс A | фикс B: перезапуск background при замене Network-процесса и мёртвом worker'е | **красный прогон подтверждён** (падает `testNetworkProcessCrashKeepsEveryTabWorking`, 3 ретрая, Non-Sandbox). Фикс-коммит `f1085846e7` **локально, не запушен** | | (нет PR) | `alex/cpm-fix-pixels` | фикс B + PR0 | пиксели `background_restart` и `first_load_wait` для измерения эффекта | держим до слияния PR0, A, B | Локальные проверки фикса A: на коде без фикса оба repro-теста красные (5 из 5 итераций), с фиксом 0 из 10 потерь, 761 тест пакета зелёный. Фикс B: на `main` красные тесты падения Network-процесса и смерти worker'а, 5 из 5 итераций. Недоделано в harness и CI: CI гоняет тесты с `-test-iterations 3 -retry-tests-on-failure`, красный тест должен падать стабильно; тесты WebExtensions идут внутри схем `macOS Browser` и `iOS Browser` на `osx-tahoe-27`. Медленные эксперименты (заморозка 45 с, заполнение кэша около 10 минут, медленный старт worker'а) локальные и не для коммита. ## 3. Данные (ClickHouse, 12–14 октября, версии iOS 7.240+ и macOS 1.210+) Единица: `daily`-пиксель = устройство-день на причину, не пользователь и не эпизод. `initialization_failed` только `.daily`; `stuck`, `recovered_*`, `reload_failed`: `daily`+`count`. Параметры снимка есть только на init_failed и stuck. | | iOS | macOS | |---|---|---| | `initialization_failed.daily` | 3 273 025 | 119 950 | | `stuck.daily` | 511 660 | 40 842 | | init восстановились без reload | 1 135 477 (34.7%) | 30 738 (25.6%) | | stuck → восстановились после M4 reload | 293 557 (57.4%) | 21 865 (53.5%) | | `reload_failed.daily` | 140 921 (27.5% от stuck) | 17 796 (43.6%) | **Когорты** (по приоритету: error 6 → нет view → вкладка не подключена → Network заменён → пробуждение (view <30 с после `dealloc`) → недавняя загрузка (`load`/`base_url_changed` ≤2 мин) → other). Доля init → stuck: | Когорта | iOS | macOS | Смысл роста или падения | |---|---|---|---| | error 6 | 6.1% → 21.5% | 1.0% → 1.9% | остаётся сломанной | | нет background view | 48.1% → 10.9% | 3.3% → 1.8% | самоизлечивается | | вкладка не подключена | 0.03% → 0.05% | 6.2% → 8.9% | остаётся сломанной | | Network заменён | 16.8% → 39.7% | 13.6% → 26.6% | остаётся сломанной | | пробуждение | 7.9% → 9.5% | 32.5% → 32.2% | без изменений | | недавняя загрузка контекста | 15.7% → 6.0% | 20.0% → 6.4% | самоизлечивается | | other | 5.3% → 12.5% | 23.4% → 22.2% | | Объёмы `stuck.count` в разных версиях на порядки выше `daily`: дашборды сравнивать только по версии и частоте. ## 4. Основание фиксов - **B:** доля «Network заменён» растёт в 2–2.4× при переходе init → stuck. `reload_failed` высок: reload расширения мёртвый worker не лечит. **Оговорка:** `network_restarted` сравнивает PID с загрузкой *контекста*, а не с последним view. Если view пересоздан после смены процесса, worker свежий и механизм неприменим. По заметке 8 октября механизм объясняет в лучшем случае 20–45% «здоровых» stuck. Причинность подтвердят `network_replaced_since_view` (нет ни в одной ветке) и `init_stage=none` (#7385). - **A:** сигнатура «загрузка контекста ≤2 мин, не при запуске, первый view»: iOS 129k init (3.9%) → 4.6k stuck (0.9%); macOS 505 → 118. Разовая потеря одной навигации (0.23× при переходе в stuck). Сигнатура неточна (захватывает M4-reload). Название связанной Asana-задачи («init loss on background unload») относится скорее к группе пробуждения, чем к фиксу A; несоответствие надо снять с автором задачи. ## 5. Картины событий (stuck.daily, 14 октября, хвост `bg_events` без возрастов) и воспроизведение | Картина | Хвост | iOS / macOS | Воспроизведено? | Рычаг в приложении | |---|---|---|---|---| | **P1. Цикл пробуждения** | `view>proxy_on>dealloc>view>proxy_on` | 16.0k / 4.3k (wake) + 16.3k / 2.4k (старше 30 с) | только с искусственно занятым worker'ом (5 с: init_failed и поздний ответ; 12 с: stuck → M4); органическое пробуждение в тестах проходит. WebKit-баг 324743 (исправлен 22 сентября, нет в 26.7/27.0) | гипотезы: отсчёт 4 с от готовности background («фаза 2 детектора»), ожидание готовности при пробуждении, предпрогрев | | **P2. «Пробуждение → error 6»**, циклы | `proxy_on>dealloc>view>proxy_on>error_E:6` (23.6k), `error>dealloc>view>proxy_on>error` (5.7k), после `died_crash` (1.5k) | iOS; macOS 118 | macOS: заморозка нового процесса на ≥30 с даёт error 6, корзина 25–35 с. iPhone: 4 сценария безуспешно. Главный iOS-хвост **без** `unresponsive` (в воспроизведении он есть) | корень в WebKit (`wakeUpBackgroundContentIfNecessary` взводит 30-секундный таймер до загрузки worker'а). Гипотеза: перезагрузка расширения при возврате на передний план | | **P3. Контекст загружен, view нет** | `load` / `load>base_url_changed` | 17.1k + 0.9k / 162 + 81 | задержка очистки при смене `baseURL` воспроизведена на macOS при большом кэше (около 10 минут заполнения); iOS-гонка запуска (`bg_view_creations=0`, восстановление сессии, первые 30 с) не воспроизведена: холодный старт в harness'е отвечает за миллисекунды | кандидат: стабильный `baseURL` (ветка `cpm-stable-base-url`: не доведён, меняет URL всем расширениям, не прогнан через harness, старая база). Фикс WebKit 318756@main, номер сборки ОС не проверен | | **P4. Первая загрузка** | `load>view>proxy_on` | 6.8k + 2.3k / 570 | да (фикс A) | фикс A | | **P5. Фон выгружен в момент сбоя** | `proxy_on>dealloc>view>proxy_on>dealloc` | 1.9k / 494 | нет | нет гипотезы | | **P6. macOS: вкладка без расширения** | `load>view>proxy_on>dealloc`, с `extension_updated` | iOS ≈0 / 627 + 235 | нет сценария | нет; фиксы привязку вкладки не трогают | | **Network заменён** | | см. раздел 4 | да (краш, worker мёртв при том же view) | фикс B | | **«Здоровый» остаток, other** | | iOS 12.5%, macOS 22% | нет | нет гипотезы | Размер группы не равен возможности её починить: P1 и P3 в основном опаздывающие ответы, а часть «сбоев» это срабатывание детектора («нет ответа за 4 с», опоздавший ответ тоже считается). iOS-когорта «other» на самом деле тот же рисунок P1, но со view старше 30 секунд. ## 6. Телеметрия **Что делает PR0 (#7376):** на 8 failure-пикселей и 2 recovery-пикселя добавлены параметры `bg_ready_after`, `bg_view_age`, `bg_load_error_after`, `bg_load_error_app_state`, `cpm_app_state`, `cpm_open_tabs`, `cpm_loading_tabs` (failure) и `cpm_recovery_document`, `cpm_recovery_after` (recovery). Новых пикселей нет. Recovery-пиксели получают только свои два параметра, `extension_reload_failed` без параметров. **Замечания ревьюера (JA1–JA3):** корреляция между пикселями из-за множества перечислений; сколько пикселей на ошибку; константность `cpm_open_tabs`/`cpm_app_state`; что такое background view. **Что верно по пикселям (исправление моей прежней ошибки):** `initialization_failed` только daily (не больше одного на устройство в день на причину); `stuck` daily+count (daily и count одного события идут с одинаковыми параметрами); snapshot пересобирается при каждой отправке, поэтому быстро меняющиеся поля (`bg_view_age`, `bg_ready_after`) между init_failed и stuck различаются, медленные (`app_state`, `open_tabs`) повторяются. **Уникальность строк `stuck.daily` внутри дня** (доля уникальных по всем параметрам): | Вариант | iOS | macOS | |---|---|---| | сейчас, `bg_events` из 12 событий | 8.3% | 30% | | информативные токены, последние 4 | 2.1% | 5.2% | | скаляры вместо `bg_events` | 0.96% | 4.4% | | без `bg_events` | 0.25% | 2.0% | Верхняя граница прироста от новых полей (синтетическое независимое равновероятное поле): со старым `bg_events` при десяти значениях iOS 21%, macOS 65%; со скалярами 3.6% / 15.8%. **Вывод:** «хуже не будет» можно утверждать только со скалярами. Главный идентификатор уже выкачен (`bg_events`). **`bg_events`:** единственный источник давности загрузки (`load`, `base_url_changed`) и причины смерти (`died_*`). `view`, `dealloc`, `proxy_on`, `error_*` дублируют `bg_view_creations` и `context_errors`; `hold/release/proxy_off` равны нулю. Скаляры воспроизводят когорты загрузки и пробуждения на 100% строк. Теряется: поиск неожиданных последовательностей в остатке (смягчить удалённым флагом). **Минимум данных по гипотезам:** | Поле / пиксель | Вопрос | Где | |---|---|---| | `bg_ready_after`, `bg_view_age` | идёт ли загрузка, пробуждение или давно | PR0 | | `bg_load_error_after`, `bg_load_error_app_state` (только при error 6) | таймер 30 с, состояние приложения | PR0 | | `recovery_document`, `recovery_after` | опоздавший ответ или потерянный | PR0 | | `app_state`, `open_tabs` (грубо, 1 / 2–5 / >5), только на `initialization_failed` | когорта «нет view» на запуске | PR0 | | **новое:** `context_load_cause`, `context_load_age` (вместо `load@` и `extension_updated`) | точная сигнатура A | PR0 | | **новое:** `bg_died_reason`, `base_url_changed` | смерть процесса, очистка при смене baseURL | PR0 | | **новое:** мелкие корзины `app_launch_age` для первых 30 с (<4 / 4–10 / 10–30) | гонка запуска | PR0 | | **новое на stuck:** `stuck_after` | сколько от первого сбоя до stuck | PR0 | | **новое, срочно:** `network_replaced_since_view` | доля реальных мёртвых worker'ов в когорте B (рекордер вычисляет, но только логирует; предложено 8 октября, **нигде не реализовано**) | PR0 | | `init_stage` | дошёл ли init до background | #7385 (срочно) | | `cpm_background_ready_{context_load,wake}` | базлайн здоровых загрузок и пробуждений | #7384 | | `background_restart` (причина, исход; **убрать из него `bg_ready_after`**, он дублирует соседний failure-пиксель), `first_load_wait` | эффект B и A | удержанная ветка | | убрать | `loading_tabs`; из таймлайна `view`, `dealloc`, `proxy_on`, `error_*`, `hold/release/proxy_off` | | Связность стека: #7384 добавляет ещё 5 измерений на те же 8 failure-пикселей (`context_load_to_background_view`, `cpm_low_power_mode`, `cpm_thermal_state`, `cpm_lockdown_mode`, `cpm_resume_age`); оценку склейки надо повторять и по нему. `background_restart` срабатывает вместе с первым `initialization_failed`: общий ключ `bg_ready_after`. ## 7. Критерии успеха и опровержения - **B** подтверждается, если после релиза падают доля «Network заменён» среди stuck и `reload_failed`, `background_restart` показывает восстановление, а у таких stuck `init_stage=none`. - **A** подтверждается, если сокращается когорта «мид-сессионная первая загрузка» (`context_load_cause`). Если нет, основание слабое. - Остаток P1/P3: гипотеза «медленный старт, а не потеря» подтверждается, если `bg_ready_after` у упавших заметно хуже базлайна здоровых загрузок и `recovery_document=failed_navigation`. ## 8. Открытые решения и следующие шаги 1. **Правки PR0** (ждут вашего «да»): скаляры вместо `bg_events`, `context_load_cause`, `stuck_after`, `network_replaced_since_view`, мелкие корзины `app_launch_age`, убрать `loading_tabs`, константы только на `initialization_failed`, `open_tabs` грубо, `bg_load_error_*` только при error 6; после этого ребейз #7384 и #7385. Альтернатива «оставить `bg_events`» требует ответа ревьюеру без утверждения «хуже не будет» и обязательства перемерить после релиза. 2. **Ответ ревьюеру** (публично, **без цифр**): JA1 (init_failed daily-only; stuck daily+count с одинаковыми параметрами; recovered несут только `recovery_*`), JA2 (медленные поля повторяются, быстрые различаются; планируется сократить набор), JA3 (background view = скрытый `WKWebView`, в котором WebKit запускает фоновый скрипт расширения; выгружается через около 30 с простоя и пересоздаётся при пробуждении). Обязательство: перемерить различимость строк после первого релиза. 3. **Запушить фикс B** (`f1085846e7`): красный прогон подтверждён, ждёт зелёного. 4. **Судьба фикса A:** слабое основание в данных, не совпадает с названием задачи; решить, сливать ли без зелёного измерения эффекта через `first_load_wait`. 5. **P3:** довести стабильный `baseURL` (ограничить встроенными расширениями без `web_accessible_resources`, прогнать harness, перенести на свежий `main`), проверить номер сборки ОС с фиксом 318756@main. 6. **Локальные repro для P1–P3** отдельным PR без фиксов (по образцу harness'а, `requireLocalRun`), чтобы гонять после релизов WebKit. 7. **iOS:** воспроизведение error 6 на реальном устройстве; сценарий «вкладка без расширения» на macOS. 8. Блокеры по ревью: #7376 и #7378 ждут ревью; #7384/#7385 стоят на #7376. ## 9. Методика и инструменты - Доступ: `~/Developer/grafana-data-tools` (Grafana → ClickHouse `metrics.pixels`, read-only; авторизация через `grafana-auth`). Ad-hoc SQL запускался отдельным раннером через `~/DuckDuckGo/grafana-dashboards/tools/grafana-query/read-dashboard.py`. Сканы по `pixel LIKE` без фильтра по дате, агенту и `pixel_id='debug'` превышают лимит строк. Временные запросы лежали в папке сессии и могут пропасть; ключевая классификация ниже. - Версии с диагностикой: iOS 7.240+, macOS 1.210+. Окно: 12–14 октября (3 полных дня). Ограничения: сигнатуры эвристические; `network_restarted` только прокси; устройства нельзя проследить от init до stuck, сравниваются только доли; доля не равна пользовательскому эффекту. ```sql -- классификация когорт для init_failed.daily / stuck.daily multiIf( errs LIKE '%ContextErrorDomain:6%', '1_error6', creations = '0', '2_no_background_view', scripts = 'false', '3_tab_not_connected', net = 'true', '4_network_replaced', has(ev,'view@30s') AND arrayExists(x -> startsWith(x,'dealloc@'), ev), '5_wake_view_lt30s', arrayExists(x -> x IN ('load@30s','load@1m','load@2m','base_url_changed@30s','base_url_changed@1m','base_url_changed@2m'), ev), '6_context_load_recent', '7_other') -- параметры: network_restarted, context_errors, bg_view_creations, tab_has_ext_scripts, bg_events (decodeURLComponent) ```