Інцидент 2026-06-11 — обвал у звітах GA4 після авто-деплою аналітики
Контекст
Section titled “Контекст”Багатофазний рефакторинг шару аналітики у Abitly Web (abitly-org/abitly-frontend-v2), окремими комітами feat(analytics): … (phase N):
- phase 3–4 (10 черв, здорові): єдиний registry + emitter, прибрано lake-house collector.
- phase 5 / 5c (
7f84c31c7,59ee99c68):page_viewстає code-owned (RouteTracker.tsxпушить його вdataLayer→ GTM → GA4), host-allowlist gate на кожну подію, consent-mode default. Коментарі в коді вимагають вимкнути GA4 enhanced-measurement page_view іgtag send_page_view=false. - phase 6 (
02530393eта сусідні): видалено code-side pixel-шар (pixels/google.ts,meta.ts,tiktok.ts,PixelManager.tsx) — «GTM-only pixels».
Деплой prod іде через CodePipeline abitly-prod-frontend автоматично на push у main (V2 trigger{} → PUSH_BRANCH). Деталі → deploy-pipeline · сервіс → Abitly Web · колектор → Analytics.
Хронологія (UTC+3)
Section titled “Хронологія (UTC+3)”| Час (+03) | UTC | Подія | Стан |
|---|---|---|---|
| 11 черв 02:21 | 10 черв 23:21 | Pipeline 62c71091a (phase 3–4) — Succeeded → task-def rev 23 | 🟢 baseline (10 черв = 22 515 users) |
| 11 черв 13:13 | 10:13 | Push 7f84c31c7 (phase 5/5c) → авто-pipeline Succeeded → task-def rev 24 | 🔴 початок зламаного контракту page_view |
| 11 черв 13:00→далі | 10:00→далі | У звітах GA4 active users/sessions падають; у сирому експорті page_view навпаки росте (over-firing) | 🟠 звіти ламаються, збір — ні |
| 11 черв 16:08 | 13:08 | Push 86fd21a5 (fix offers, не аналітика) → rev 25 | 🟠 |
| 11 черв 21:58 | 18:58 | Push 589c9d1a (phase 6) → rev 26 | 🟠 |
| 12 черв ~00 | 11 черв ~21 | Засновник помітив просадку у GA4 Reports snapshot → старт розслідування | 🟡 detection |
| 12 черв 02:24 | 11 черв 23:24 | Merge 3414e42f (PR #422, revert) → авто-pipeline | 🟡 fixing |
| 12 черв ~02:27 | 11 черв ~23:27 | Pipeline Succeeded → task-def rev 27, rollout COMPLETED, 1/1 | 🟢 збір нормалізовано (звіти — перевірити) |
| 12 черв ~02:31 | 11 черв ~23:31 | Перевірка на живому сайті: page_view → GA4 G-XKDRSM3T0Z = 204 | 🟢 collection verified |
Вікно впливу: ~13 год (10:13 → ~23:27 UTC) зламаного контракту page_view → деградація звітів GA4 та маркетинг-пікселів. Сайт і збір подій працювали весь час. Сирі події збереглись у BigQuery — втрати даних немає.
Першопричина (deep-dive за BigQuery)
Section titled “Першопричина (deep-dive за BigQuery)”Калібрування: 10 черв — джерела збігаються
Section titled “Калібрування: 10 черв — джерела збігаються”Спершу довели, що raw-експорт і Data API міряють те саме (на фіналізованій таблиці 10 черв):
| 10 черв | BigQuery raw | GA4 Data API | збіг |
|---|---|---|---|
| page_view / views | 132 698 | 124 962 | ✅ ~6% |
| sessions | 33 616 (session_start) | 32 040 | ✅ ~5% |
11 черв: джерела РОЗХОДЯТЬСЯ у протилежні боки
Section titled “11 черв: джерела РОЗХОДЯТЬСЯ у протилежні боки”| 11 черв (вся доба) | BigQuery raw | GA4 Data API (звіти) |
|---|---|---|
| page_view | 271 391 (↑ +105% до 10-го) | 25 829 (↓ −79%) |
| sessions | 29 108 (session_start, ≈ норма) | 5 215 (↓ −84%) |
| first_visit | 13 565 (≈ норма, −16%) | — |
| user_engagement | 12 818 (≈ норма, −10%) | — |
| scroll | 15 969 (≈ норма, −12%) | — |
| active users | distinct user_pseudo_id ≈ норма | 3 680 (↓ −84%) |
Висновок: GA4 BigQuery export і GA4-звіти живляться з одного колектора. Якщо подія є в BigQuery — вона дійшла до GA4. На 11 черв session_start/first_visit/user_engagement/scroll — усі нормальні, і лише page_view роздутий ×2. Якби це був артефакт intraday-таблиці (подвійний запис), роздулися б усі типи подій. Роздувся лише page_view → це реальний over-firing у застосунку, а не втрата і не артефакт експорту.
Сигнатура over-firing (page_view на користувача)
Section titled “Сигнатура over-firing (page_view на користувача)”Погодинно (raw, істинний UTC із event_timestamp):
| Година UTC 11 черв | page_views | distinct users | pv/user |
|---|---|---|---|
| 04–09 (до деплою) | — | — | ~3.9 🟢 |
| 10 (деплой 10:13) | 19 082 | 2 345 | 8.1 🔴 |
| 11 | 31 572 | 3 204 | 9.9 🔴 |
| 13 | 25 095 | 2 143 | 11.7 🔴 |
| 15 | 24 307 | 1 649 | 14.7 🔴 |
Baseline 10 черв у ці ж години: pv/user ≈ 3.0–4.1. Тобто RouteTracker (phase 5) почав слати page_view надлишково (схоже, на кожен ререндер/роут-зміну), а не «перестав слати».
Чому ж обвалились саме звіти (гіпотеза)
Section titled “Чому ж обвалились саме звіти (гіпотеза)”page_view із кривим/надлишковим контрактом (без коректних session/engagement-параметрів, можливо до ініціалізації gtag/consent) шар звітності GA4 не зараховує в active users/sessions, тоді як BigQuery логує сирий хіт. Додатково 11 черв досі не фіналізована (events_intraday_20260611), тож Data API ще й семплить: той самий час «11 черв 10:00» повертав 159 користувачів (запит за 3 дні) або 984 (запит за 2 дні) — розкид 6×. Тому «−84%» у звітах = поломка контракту + семплінг/латентність незавершеної доби, а не реальний відтік. (Частина просадки у звітах може самовідновитись після фіналізації events_20260611 — окрема перевірка за 24–48 год.)
Таймзонне уточнення
Section titled “Таймзонне уточнення”Вимір GA4 Data API hour/date_hour — у reporting-таймзоні property (Europe/Kyiv, +03), не UTC. Доказ: raw-пік 10 черв припадає на 11:00 UTC, а Data API показав пік на годині 14 (11 UTC = 14 Київ). Тож первинне твердження «обвал о годині 10 UTC, збіг із деплоєм 10:13 до хвилини» — хибне: година 10 у Data API = 07:00 UTC, тобто за 3 год ДО деплою; а в сирих даних у цю годину жодного обвалу немає. Для точної хвилинної атрибуції — лише raw event_timestamp (UTC).
Маскувальні фактори
Section titled “Маскувальні фактори”- Сайт працював — жодного 5xx, жодного алерту. Зламався контракт даних, а він не має health-check.
- «Обвал у звітах» ≠ «обвал збору». Founder бачив GA4 Reports (шар звітності), де −84%; сирий колектор працював. Без звірки з BigQuery це виглядало як катастрофа втрати даних.
- Часткова доба + семплінг незавершеної доби дали нестабільні погодинні числа (159 vs 984), які створили хибне відчуття «краю до хвилини».
- Realtime ~202 users виглядала нормальною — бо збір реально працював.
- Consent-mode (phase 5c) — хибний слід.
ConsentModeDefaultставитьdeniedлише для EEA+UK+CH; аудиторія ~98% Україна (UA →granted). - Doc-хиба в хабі. deploy-pipeline стверджував, що
DetectChanges:false= «push не деплоїть автоматично». Насправді це CodePipeline V2 з блокомtrigger{ push{ branches=[main] } }→ автодеплой увімкнено. (Виправлено.)
Залишковий ризик після відкату
Section titled “Залишковий ризик після відкату”Сирі події після деплою відкату (~23:27 UTC 11 черв, таблиця events_intraday_20260612) у перші нічні години все ще показують pv/user ≈ 6–9 (вибірка мала, ~десятки користувачів — це сигнал, не вирок).
Чому code-only відкат може не вилікувати over-firing: конфіг GTM-контейнера і тумблер GA4 enhanced-measurement page_view живуть у консолях GTM/GA4, не в репозиторії фронтенду. Якщо phase-5 додав page_view-тег у GTM (або вимкнув enhanced measurement), відкат коду цього не скасовує → можуть одночасно стріляти (1) відновлений code-side gtag page_view і (2) залишковий GTM-тег → знову подвійний page_view.
Дія: звірити консольний стан (один-єдиний джерело page_view), потім підтвердити по BigQuery, що денний pv/user 12 черв повернувся до ~3.9. TODO: зафіксувати результат після появи денного трафіку 12 черв.
Чому це не зловили раніше
Section titled “Чому це не зловили раніше”| Можлива гарантія | Чи була | Чому не спрацювала |
|---|---|---|
Smoke-тест аналітики в CI/CD (assert коректний page_view у GA4 + немає дублів) | ❌ ні | Пайплайн перевіряє лише next build |
| GA4-алерт на аномалію (падіння users або сплеск views/user) | ❌ ні | Custom Insight не налаштовано — помітили вручну |
| Звірка зі сирим джерелом (BigQuery export) до висновку «втрата даних» | ❌ ні | Дивились лише GA4 Reports → хибний діагноз «дані втрачено» |
| Staging-перевірка контракту GTM/GA4 | ❌ ні | Багатофазний рефактор котився прямо в prod |
| Version-control для GTM/console-конфігу | ❌ ні | Тумблери enhanced-measurement/GTM-теги поза репо → відкат коду їх не повертає |
Виправлення (що зробили й що ще треба)
Section titled “Виправлення (що зробили й що ще треба)”Хірургічний відкат шару аналітики до 62c71091a (= task-def rev 23), зберігаючи непов’язані зміни (fix(offers) 86fd21a5, regCta→utils/, typo univeristy→university); прибрано 2 orphan-тести phase-5.
Валідація перед merge: yarn typecheck — 0 нових помилок проти main; @/lib/analytics/* резолвляться; ціль байт-у-байт = rev 23; next build пройшов.
Жива перевірка: page_view → GA4 G-XKDRSM3T0Z 204. ⚠️ Важливо: 204 підтверджує збір (який і не ламався), а не здоров’я звітів і не відсутність дублів.
Ще треба (open):
- Звірити GTM-контейнер + enhanced-measurement → лишити одне джерело
page_view. - Підтвердити по BigQuery: денний pv/user 12 черв ≈ 3.9 (інакше — лікувати дублі в GTM/консолі).
- Перевірити, чи звіти GA4 за 11 черв частково самовідновились після фіналізації
events_20260611. - За потреби — відновити «офіційний» ряд за 11 черв із BigQuery (дані не втрачено).
Хід розслідування (інструменти, що спрацювали)
Section titled “Хід розслідування (інструменти, що спрацювали)”- GA4 BigQuery export (
bqCLI, проєктabitly-447911, datasetanalytics_472437676) — ground truth. Per-UTC-hourpage_view/distinct-users/sessions ізevent_timestamp; порівняння типів подій (тільки page_view роздутий → over-firing). Без цього діагноз був би хибним. - Windsor.ai MCP (GA4 connector) — денні/погодинні Data API-числа; виявив семплінг (159 vs 984) і київську таймзону погодинних звітів.
gh/ GitHub — коміти фронтенду у вікні (RouteTracker.tsx,Scripts.tsx,track.ts,isAnalyticsAllowedHost.ts).- AWS CLI / Terraform
cloud-infrastructure— підтвердити авто-тригер V2, час деплою, мапінг task-def↔commit. - Playwright MCP — живі мережеві хіти
g/collect(перевірка збору, не звітів).
Превентивні заходи
Section titled “Превентивні заходи”- Звіряти аномалії GA4 зі сирим BigQuery-експортом ДО висновків. «Обвал у звітах» ≠ «втрата даних». Експорт — джерело правди.
- Post-deploy smoke-тест аналітики: Playwright assert-ить, що
page_view(а) шле хіт і (б) шле його рівно раз на навігацію (anti-duplication). - GA4 anomaly alert на падіння users і на сплеск
views/user(over-firing має власну сигнатуру). - Version-control / документувати GTM-контейнер і GA4-тумблери; відкат коду їх не повертає — тримати їх у синхроні з кодом.
- Не вимикати GA4 enhanced-measurement
page_view, доки code-owned заміна не підтверджена end-to-end у звітах (не лише 204) на prod; перехід — паралельно з дедуплікацією заevent_id. - Staging-режим для міграцій трекінгу — не котити багатофазні рефактори аналітики прямо в prod, тим паче в пік вступної кампанії.
- Feature-flag / канарка для змін пайплайна подій GA4.
Ключові ідентифікатори
Section titled “Ключові ідентифікатори”| Поле | Значення |
|---|---|
| GA4 property / measurement ID | «Abitly» 472437676 / stream G-XKDRSM3T0Z |
| BigQuery export | проєкт abitly-447911, dataset analytics_472437676 (events_YYYYMMDD + events_intraday_*) |
| Репо фронтенду | abitly-org/abitly-frontend-v2 (main) |
| Pipeline | abitly-prod-frontend (CodePipeline V2, тригер PUSH_BRANCH на main) |
| Зламаний деплой | 7f84c31c7 (phase 5/5c), exec старт 2026-06-11T10:13Z |
| Останній здоровий | 62c71091a = task-def rev 23 |
| Виправлення | PR #422 → 3414e42f → task-def rev 27 |
| Аварійний відкат (instant) | aws ecs update-service --cluster abitly-prod-frontend --service abitly-prod-frontend --task-definition abitly-prod-frontend:23 |
- GA4 Reports ≠ GA4 collection. Обвал у звітах може співіснувати з повністю здоровим збором. Перша звірка аномалії — із сирим BigQuery-експортом, не з UI. Інакше ризик хибного «дані втрачено» (тут — дані цілі).
- Сигнатура важлива. Пропорційний обвал users↔sessions у звітах виглядав як «мертвий page_view», але сирі дані показали over-firing ×2 при нормальному трафіку. Завжди дивись і на
views/user. - Авто-деплой на push + відсутність smoke-тесту аналітики = тихі поломки контракту даних деплоять самі себе. «Сайт працює» маскує поломку, бо в даних немає health-check.
- Конфіг поза репо (GTM-контейнер, GA4-тумблери) ламає відкати. Відкат коду не повертає консольних змін — тримай їх версіонованими/задокументованими.
- Погодинний GA4 Data API на незавершеній добі — семплиться й у tz property. Для хвилинної атрибуції бери raw
event_timestamp(UTC) з BigQuery, не Data API-години. - Звіряй конфіг із живим джерелом правди. Хаб казав «автодеплою немає» — AWS показав протилежне (V2
trigger{}).
Пов’язана документація
Section titled “Пов’язана документація”- Abitly Web service card — рантайм, env, GTM-sync
- Analytics / collector
- Deploy pipeline — тригери CodePipeline/Amplify
- Deploy rollback — ECS task-def revision
- Service down — first-look triage