203 lines
21 KiB
Markdown
Executable File
203 lines
21 KiB
Markdown
Executable File
# CPM Web Extension Breakage — статус и проверка причин
|
||
|
||
> **Текущий milestone, 2026-10-05: проверить attribution data и найти причины CPM breakage.** Исторический PR-план ниже — не текущая оценка готовности. Подробная трассировка: [[cpm-web-extension-breakage-findings]]; архитектура: [[cpm-service-worker-architecture-and-diagnostics]].
|
||
|
||
## Сейчас: attribution и breakage reasons
|
||
|
||
**Граница данных.** До 2026-10-05 macOS 1.210 использовалась в internal testing примерно неделю; iOS 7.240 ещё не опубликована в App Store. macOS 1.211 / iOS 7.241 — ранние internal-сборки. Считать public incidence только по данным после начала public rollout (ожидается 2026-10-06), отдельно по платформе и версии. Internal-тесты могли специально воспроизводить failure, поэтому частоту и причинность по ним не выводим.
|
||
|
||
**Что видно в attribution-bearing `debug.web.extension.cpm.messaging.stuck.*` за завершённые 2026-09-28—10-04:**
|
||
|
||
| Версия | `.count` | `.daily` | Статус |
|
||
|---|---:|---:|---|
|
||
| macOS 1.210 | 131 | 82 | Internal testing; не оценка затронутых пользователей. |
|
||
| iOS 7.240 | 6 | 5 | До App Store release. |
|
||
| macOS 1.211 / iOS 7.241 | 0 | 0 | Включены в проверку; attributed stuck пока нет. |
|
||
|
||
`.count` — репорты, `.daily` — дневные пиксели, не уникальные пользователи. Большой поток iOS stuck на 7.238/7.239 в основном **без** новых attribution-полей; его нельзя смешивать с 7.240. Сверка фильтров Kibana и Grafana ещё нужна. [Breakage board](https://grafana.duckduckgo.com/d/dfon7whlilnggb/cpm-weird-error-investigation?orgId=1).
|
||
|
||
**Проверяемые сигнатуры, пока не root causes:**
|
||
|
||
1. `bg_events`: повторный `dealloc` есть в 88/131 macOS 1.210 `.count` stuck. Internal forced-failure testing может полностью объяснять этот сигнал; он **не доказывает** естественную смерть WebContent-процесса или эффект graveyard.
|
||
2. Tab-side: в 24/131 macOS stuck отсутствуют extension scripts. В 17 из 24 `tab_controller_match=false`, хотя `tab_in_context=true`; 10 из 17 помечены `app_version_change=updated`. Гипотеза: старая/restored вкладка сохраняет прежний `webExtensionController` после создания или замены WebExtensionManager. В остальных 7 controller совпадает, но `bg_view_creations=0` (6 после update): это больше похоже на недошедший до background-view/injection startup, а не на доказанное удаление scripts. `removeAllUserScripts()` остаётся отдельной кодовой гипотезой без подтверждения этими пикселями.
|
||
3. `base_url_changed`, reload и другие причины анализировать отдельно: совпадение с stuck ещё не подтверждает причинность.
|
||
|
||
**Следующая проверка:** на public macOS 1.210 / iOS 7.240 с 2026-10-06 раздельно считать affected daily users (recovered и permanent) против DAU той же версии; разбирать ordered `bg_events` и tab-поля по каждой сигнатуре. Воспроизвести на macOS 1.210 update/session restore и off→on Web Extensions с уже открытой вкладкой: сравнить старую и новую вкладки по controller, scripts и CPM outcome. До этого не ранжировать причины по internal `.count`.
|
||
|
||
## Как получить и проверить attribution data
|
||
|
||
**Источник:** Grafana ClickHouse datasource `dekdbd6bqq1hcd` (`grafana-clickhouse-datasource`), таблица `metrics.pixels`. Использовался API через `/Users/admin/DuckDuckGo/grafana-dashboards/tools/grafana-query/read-dashboard.py`, **не** браузер и не локальный Apple worktree. Авторизация этого helper — `grafana-auth`/SSO либо `GS_GRAFANA_API_KEY`; токены в документ не записывать. [Board](https://grafana.duckduckgo.com/d/dfon7whlilnggb/cpm-weird-error-investigation?orgId=1) — визуальная сверка, SQL ниже — источник приведённых чисел.
|
||
|
||
1. Сохранить нужный SQL-блок ниже в `/tmp/cpm-query.sql`. Для исторической проверки оставить UTC-окно `[2026-09-28, 2026-10-05)`; для public заменить начало на фактический день публикации и указать **только завершённые UTC-дни**. Сверять `date`, `agent`, `appVersion`, суффикс `.count`/`.daily` и наличие `context_loaded` с Kibana.
|
||
2. Из `/Users/admin/DuckDuckGo/grafana-dashboards` выполнить этот read-only API-запрос. Заменить `FROM`/`TO` вместе с датами SQL; пустой Grafana frame без ошибки перепроверить повторным запросом, а не трактовать как ноль:
|
||
|
||
```bash
|
||
uv run --with requests python3 - /tmp/cpm-query.sql 2026-09-28T00:00:00Z 2026-10-05T00:00:00Z <<'PY'
|
||
import importlib.util
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
path = Path('tools/grafana-query/read-dashboard.py').resolve()
|
||
spec = importlib.util.spec_from_file_location('grafana_reader', path)
|
||
reader = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(reader)
|
||
target = {'refId': 'A', 'rawSql': Path(sys.argv[1]).read_text(),
|
||
'editorType': 'sql', 'queryType': 'table', 'format': 1}
|
||
raw = reader.query_ds_query('dekdbd6bqq1hcd', 'grafana-clickhouse-datasource',
|
||
[target], reader.parse_time(sys.argv[2]), reader.parse_time(sys.argv[3]))
|
||
result = raw.get('results', {}).get('A', {})
|
||
if result.get('error'):
|
||
raise SystemExit(result['error'])
|
||
if not result.get('frames'):
|
||
raise SystemExit('No data frame returned; retry/inspect Grafana response')
|
||
for frame in result['frames']:
|
||
print('\t'.join(f['name'] for f in frame['schema']['fields']))
|
||
for row in zip(*frame['data']['values']):
|
||
print('\t'.join(map(str, row)))
|
||
PY
|
||
```
|
||
|
||
**SQL 1 — покрытие по всем версиям, включая пиксели без attribution.** Не фильтровать версии заранее: так обнаруживаются 1.211/7.241 и большой legacy iOS-поток без новых полей. `attributed` — строки с live-параметром `context_loaded`; `rows` — все stuck-пиксели. `.count` и `.daily` не складывать.
|
||
|
||
```sql
|
||
SELECT
|
||
toString(date) AS day,
|
||
agent,
|
||
arrayStringConcat(arraySlice(splitByChar('.', extractURLParameter(request, 'appVersion')), 1, 3), '.') AS app_version,
|
||
multiIf(match(pixel, '\\.daily(\\.ios\\.(phone|tablet))?$'), 'daily',
|
||
match(pixel, '\\.count(\\.ios\\.(phone|tablet))?$'), 'count', 'other') AS frequency,
|
||
count() AS rows,
|
||
countIf(extractURLParameter(request, 'context_loaded') != '') AS attributed,
|
||
countIf(extractURLParameter(request, 'bg_events') != '') AS chain_present
|
||
FROM metrics.pixels
|
||
WHERE date >= toDate('2026-09-28') AND date < toDate('2026-10-05')
|
||
AND agent IN ('ddg_ios', 'ddg_mac_desktop')
|
||
AND pixel_id = 'debug'
|
||
AND pixel LIKE 'debug.web.extension.cpm.messaging.stuck.%'
|
||
GROUP BY day, agent, app_version, frequency
|
||
ORDER BY day, agent, app_version, frequency;
|
||
```
|
||
|
||
**SQL 2 — macOS 1.210 tab-сигнатуры и предшествующие события.** Только attributed `.count` stuck; каждое число — репорты, не люди. `bg_events` упорядочены oldest→newest, но содержат лишь ограниченный хвост и грубые age buckets; `dealloc` не равен доказанному process death. Проверить эту же выборку с `.daily` для чувствительности к повторным репортам.
|
||
|
||
```sql
|
||
WITH events AS (
|
||
SELECT
|
||
extractURLParameter(request, 'tab_in_context') AS tab_in_context,
|
||
extractURLParameter(request, 'tab_controller_match') AS controller_match,
|
||
extractURLParameter(request, 'tab_has_ext_scripts') AS scripts_present,
|
||
extractURLParameter(request, 'bg_view_creations') AS view_creations,
|
||
extractURLParameter(request, 'app_version_change') AS version_change,
|
||
splitByChar(',', decodeURLComponent(extractURLParameter(request, 'bg_events'))) AS chain
|
||
FROM metrics.pixels
|
||
WHERE date >= toDate('2026-09-28') AND date < toDate('2026-10-05')
|
||
AND agent = 'ddg_mac_desktop' AND pixel_id = 'debug'
|
||
AND pixel LIKE 'debug.web.extension.cpm.messaging.stuck.%'
|
||
AND match(pixel, '\\.count$')
|
||
AND extractURLParameter(request, 'appVersion') LIKE '1.210.%'
|
||
AND extractURLParameter(request, 'context_loaded') != ''
|
||
)
|
||
SELECT
|
||
count() AS reports,
|
||
countIf(arrayCount(x -> startsWith(x, 'dealloc@'), chain) >= 2) AS repeated_dealloc,
|
||
countIf(scripts_present = 'false') AS scripts_absent,
|
||
countIf(scripts_present = 'false' AND controller_match = 'false' AND tab_in_context = 'true') AS controller_mismatch,
|
||
countIf(scripts_present = 'false' AND controller_match = 'false' AND version_change = 'updated') AS mismatch_after_update,
|
||
countIf(scripts_present = 'false' AND controller_match = 'true' AND view_creations = '0') AS same_controller_zero_views,
|
||
countIf(scripts_present = 'false' AND controller_match = 'true' AND view_creations = '0' AND version_change = 'updated') AS zero_views_after_update
|
||
FROM events;
|
||
```
|
||
|
||
**Методика интерпретации:** сначала SQL 1 проверяет покрытие версии/платформы и отсутствие attribution; затем SQL 2 группирует совместные признаки вместо чтения одиночного поля как root cause. Для конкретной сигнатуры дополнительно сгруппировать `bg_events` с сохранением порядка, reason из имени pixel, `app_version_change` и `bg_view_alive`; сверить с исходным кодом `CPMMessagingDiagnosticsRecorder` и созданием `WebExtensionManager` на `apple-browsers.git/main`. Internal forced-failure testing, app update и повторные `.count` могут смещать выборку. Public user-rate и recovered/permanent требуют отдельного user-level числителя и DAU **той же версии**; эти SQL их не вычисляют.
|
||
|
||
## Исторический статус и PR-план (2026-09-23—09-25)
|
||
|
||
> Ниже сохранён прежний план. Его формулировки о shipped status, готовности PR и дизайне graveyard A/B не являются текущим release/experiment contract; сверять с `apple-browsers.git/main` и актуальной privacy config.
|
||
|
||
## Решение
|
||
|
||
Есть два независимых класса проблем:
|
||
|
||
- **Per-tab lifecycle** исправлен в M1 и уже merged.
|
||
- **All-tabs CPM outage:** M3 telemetry merged; M4 hung recovery и объединённый M5+M6 graveyard A/B — независимые follow-up PRs.
|
||
|
||
M4 перезагружает Web Extensions при первом сигнале `hung`, который подтверждает global CPM outage после предшествующего `messaging init failure`. Сам `messaging init failure` остаётся диагностическим: он не меняет состояние расширений.
|
||
|
||
## Статус проблем
|
||
|
||
| Проблема | Статус | Что делаем |
|
||
|---|---|---|
|
||
| Forced background-process death → persistent all-tabs CPM outage | **Подтверждено** | M3 измеряет production incidence; объединённый M5+M6 PR лечит и сравнивает control/treatment. |
|
||
| Natural crash/memory/CPU termination → production outage | **Не проверено в production** | Один production release M3 до включения M5/M6. |
|
||
| Close Other Tabs, popup duplicate, pin/unpin/reorder/move/merge/window close | **Исправлено, merged** | M1 PR #6827. |
|
||
| Нестабильный Web Extension base URL | **Исправлено локально** | M2 задаёт deterministic base URL до load/reload и отвергает malformed identifier без crash. |
|
||
| Startup/session-restore content-script miss | **Подтверждено как transient** | Не объясняет persistent all-tabs outage. |
|
||
| Failed background load навсегда блокирует WebKit | **Отвергнуто** | WebKit повторяет load; нужна повторяемая причина/traffic. |
|
||
| Critical memory pressure как root cause | **Не воспроизведено** | Остаётся diagnostic correlation M3. |
|
||
| Event starvation / JSC hang / native messaging stall | **Не проверено** | Остаётся в M3 diagnostic buckets. |
|
||
| Network process restart | **Отвергнуто** | На проверенной сборке CPM восстановился. |
|
||
| removeAllUserScripts / flag transition / scriptlet reload / page JS как all-tabs cause | **Отвергнуто** | Это не persistent global failure mode. |
|
||
|
||
## Milestones
|
||
|
||
| Milestone | Branch / worktree | Готовность |
|
||
|---|---|---|
|
||
| M1 tab lifecycle | `codex/cpm-tab-lifecycle-fixes` | **Merged:** [PR #6827](https://github.com/duckduckgo/apple-browsers/pull/6827), CI green. |
|
||
| M2 stable base URL | `alex/cpm-stable-base-url` — `/Users/admin/DuckDuckGo/apple-browsers.git/cpm-stable-base-url` | **Production-ready:** 2 files; 11/11 targeted tests. Добавлена safe URL construction и negative validation вместо force-unwrap. |
|
||
| M3 attribution telemetry | `alex/cpm-failure-telemetry` — `/Users/admin/DuckDuckGo/apple-browsers.git/cpm-failure-telemetry` | **Merged 2026-09-23:** [PR #6844](https://github.com/duckduckgo/apple-browsers/pull/6844), CI green. Bucketed diagnostics, app launch/version attribution, `extension_updated`, `base_url_changed`, `recovery_from`, reload-error pixel; recorder и proxy имеют отдельные flags. [[cpm-m3-telemetry-privacy-triage]]. |
|
||
| M4 hung recovery | `codex/cpm-hung-recovery` — `/Users/admin/DuckDuckGo/apple-browsers.git/cpm-hung-recovery` | **In progress:** первый `messaging init failure` открывает diagnostic episode; первый последующий `hung` перезагружает Web Extensions. |
|
||
| M5+M6 graveyard mitigation + A/B | `alex/cpm-graveyard-mitigation` — `/Users/admin/DuckDuckGo/apple-browsers.git/cpm-graveyard-mitigation` | **Production-ready, default off; one PR over merged M3:** one retained view maximum, 3-second bounded hold, weak scheduled release, runtime cleanup, lazy stable cohort enrollment, control/treatment isolation, bounded pending outcomes, operational and experiment pixels. 431/431 WebExtensions tests; iOS/macOS builds and pixel validators pass; earlier real forced-kill iOS E2E recovered with release at ≈3.15 s. |
|
||
| M7 state-restoration readiness gate | `alex/cpm-state-restoration-race` — `/Users/admin/DuckDuckGo/apple-browsers.git/cpm-state-restoration-race` | **Implemented on macOS and iOS from current `origin/main` (`b93091e769`):** initial main-frame HTTP(S) navigation waits for Web Extension `loadAndSync`, bounded by shared `withTimeout(3)`. Timeout stops only the cancellation-aware waiter; the underlying load continues and its retained task is cleared on completion/cancellation. WebExtensions lifecycle suite: 20/20. iOS Swift compilation reached the app target successfully; the generic-simulator build then stopped in existing environment-dependent scripts because `TARGET_DEVICE_OS_VERSION` and a signing identity were unavailable. The 100-cycle macOS app-restart/session-restore UI regression was imported from the WIP worktree; its run was skipped by request. SwiftLint and diff/project-file checks pass. |
|
||
|
||
All milestone diffs pass `git diff --check`; changed Swift lines are within 150 columns.
|
||
|
||
## M7 plan — state restoration / Web Extension load race
|
||
|
||
**Confirmed race:** `setupWebExtensions()` creates the manager/controller before state restoration, but starts `WebExtensionLifecycleCoordinator.loadAndSync()` without awaiting it. `stateRestorationManager.applicationDidFinishLaunching()` runs immediately afterward. A restored tab can therefore begin its main-frame navigation while the CPM `WKWebExtensionContext` is not loaded, so WebKit cannot inject the CPM content script at document start. The existing Content Blocking navigation responder already waits for internal user scripts and rule lists; M7 adds the equivalent readiness boundary for Web Extensions.
|
||
|
||
1. Retain only the first coalesced `loadAndSync` task in `WebExtensionLifecycleCoordinator`, clear it on completion/cancellation, and expose a cancellation-aware waiter that never cancels the underlying load. **Done.**
|
||
2. Inject the waiter into both macOS `PrivacyDashboardTabExtension` and iOS `TabViewController`. Gate initial main-frame HTTP(S) navigation with the shared `WebExtensionNavigationGate`, which uses project `withTimeout(3)`. **Done.**
|
||
3. Keep the wait startup-only: after initial `loadAndSync`, the provider returns `nil`, so later navigations avoid an async hop. Non-web and subframe navigations do not wait. **Done.**
|
||
4. Cover blocking/release/cancellation, timeout without cancellation of the underlying load, late completion, default timeout, and navigation scope. WebExtensions lifecycle suite is 20/20; SwiftLint and structural checks pass. **Done.**
|
||
5. Add the existing WIP UI regression that restarts the macOS app 100 times, restores the Autoconsent page, and verifies CPM injection after every restart. **Included; execution intentionally skipped by request.**
|
||
|
||
Technical decision: awaiting `initialLoadAndSync.value` directly inside structured `withTimeout` was hang-tested and rejected. The timeout cancels its child operation, but a child suspended on `Task.value` does not finish from that cancellation, so the task group cannot return. The small cancellation-aware waiter gives `withTimeout` a cancellable wait without cancelling the retained load task.
|
||
|
||
## Stack and Git state
|
||
|
||
M3 merged: [PR #6844](https://github.com/duckduckgo/apple-browsers/pull/6844), merge commit `2d58ad8cf4fae728a92e2aae0d3ec6073ddf49f2`.
|
||
|
||
M2 — independent stable-base-URL fix. Вошедший в M3 `base_url_changed` только сообщает о смене URL между загрузками контекста с одинаковым identifier; URL и identifier не отправляются. Это не фикс M2.
|
||
|
||
Stacked PRs:
|
||
|
||
```
|
||
M3 telemetry — merged into origin/main
|
||
├── M4 hung recovery
|
||
└── M5+M6 graveyard mitigation + A/B — one PR
|
||
```
|
||
|
||
M5+M6 объединены на `origin/main` (`2d58ad8cf4fae728a92e2aae0d3ec6073ddf49f2`) без commit: в worktree лежат 19 unstaged файлов mitigation и A/B. Старый M6 worktree сохранён только как источник/backup и не является отдельным PR. M4 не изменён.
|
||
|
||
Не включать generated `Package.resolved` churn.
|
||
|
||
## Experiment contract
|
||
|
||
- Experiment: `cpmBackgroundGraveyardExperiment`.
|
||
- Cohorts: `control`, `treatment`.
|
||
- Enrollment: only on an actual eligible CPM background-process termination.
|
||
- Primary framework metric: `cpmBackgroundGraveyardOutcome`, window `0...1`, values `healthy`, `initialization_failed`, `not_measured`.
|
||
- Rollback: disable both `cpmBackgroundGraveyardExperiment` and `cpmBackgroundGraveyard`.
|
||
|
||
Full definition and config draft: [[cpm-background-graveyard-experiment-definition]].
|
||
|
||
## Remaining gates
|
||
|
||
1. Обновить и проверить M4 поверх merged M3. Открыть M2, M4 и объединённый M5+M6 PRs в topology выше.
|
||
2. Create separate iOS and macOS experiment-log tasks.
|
||
3. Create Privacy Configuration + privacy/schema review tasks.
|
||
4. Create Data Science sample-size/design and Native Apps Experiment Analysis task.
|
||
5. Ship M3 first; enable M5/M6 only if production telemetry confirms the process-death hypothesis.
|
||
|
||
M3 merged, но ещё не подтверждён как shipped production release. Feature flag registry tasks созданы отдельно для iOS/macOS; ссылки — в [[cpm-m3-telemetry-privacy-triage]]. Это обновление затрагивает только Obsidian.
|