Перейти до вмісту

Інцидент 2026-06-11 — обвал у звітах GA4 після авто-деплою аналітики

Багатофазний рефакторинг шару аналітики у 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.

Час (+03)UTCПодіяСтан
11 черв 02:2110 черв 23:21Pipeline 62c71091a (phase 3–4) — Succeeded → task-def rev 23🟢 baseline (10 черв = 22 515 users)
11 черв 13:1310:13Push 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:0813:08Push 86fd21a5 (fix offers, не аналітика) → rev 25🟠
11 черв 21:5818:58Push 589c9d1a (phase 6) → rev 26🟠
12 черв ~0011 черв ~21Засновник помітив просадку у GA4 Reports snapshot → старт розслідування🟡 detection
12 черв 02:2411 черв 23:24Merge 3414e42f (PR #422, revert) → авто-pipeline🟡 fixing
12 черв ~02:2711 черв ~23:27Pipeline Succeeded → task-def rev 27, rollout COMPLETED, 1/1🟢 збір нормалізовано (звіти — перевірити)
12 черв ~02:3111 черв ~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 rawGA4 Data APIзбіг
page_view / views132 698124 962✅ ~6%
sessions33 616 (session_start)32 040✅ ~5%

11 черв: джерела РОЗХОДЯТЬСЯ у протилежні боки

Section titled “11 черв: джерела РОЗХОДЯТЬСЯ у протилежні боки”
11 черв (вся доба)BigQuery rawGA4 Data API (звіти)
page_view271 391 (↑ +105% до 10-го)25 829 (↓ −79%)
sessions29 108 (session_start, ≈ норма)5 215 (↓ −84%)
first_visit13 565 (≈ норма, −16%)
user_engagement12 818 (≈ норма, −10%)
scroll15 969 (≈ норма, −12%)
active usersdistinct 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_viewsdistinct userspv/user
04–09 (до деплою)~3.9 🟢
10 (деплой 10:13)19 0822 3458.1 🔴
1131 5723 2049.9 🔴
1325 0952 14311.7 🔴
1524 3071 64914.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 год.)

Вимір 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).

  1. Сайт працював — жодного 5xx, жодного алерту. Зламався контракт даних, а він не має health-check.
  2. «Обвал у звітах» ≠ «обвал збору». Founder бачив GA4 Reports (шар звітності), де −84%; сирий колектор працював. Без звірки з BigQuery це виглядало як катастрофа втрати даних.
  3. Часткова доба + семплінг незавершеної доби дали нестабільні погодинні числа (159 vs 984), які створили хибне відчуття «краю до хвилини».
  4. Realtime ~202 users виглядала нормальною — бо збір реально працював.
  5. Consent-mode (phase 5c) — хибний слід. ConsentModeDefault ставить denied лише для EEA+UK+CH; аудиторія ~98% Україна (UA → granted).
  6. 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, regCtautils/, typo univeristyuniversity); прибрано 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 “Хід розслідування (інструменти, що спрацювали)”
  1. GA4 BigQuery export (bq CLI, проєкт abitly-447911, dataset analytics_472437676)ground truth. Per-UTC-hour page_view/distinct-users/sessions із event_timestamp; порівняння типів подій (тільки page_view роздутий → over-firing). Без цього діагноз був би хибним.
  2. Windsor.ai MCP (GA4 connector) — денні/погодинні Data API-числа; виявив семплінг (159 vs 984) і київську таймзону погодинних звітів.
  3. gh / GitHub — коміти фронтенду у вікні (RouteTracker.tsx, Scripts.tsx, track.ts, isAnalyticsAllowedHost.ts).
  4. AWS CLI / Terraform cloud-infrastructure — підтвердити авто-тригер V2, час деплою, мапінг task-def↔commit.
  5. Playwright MCP — живі мережеві хіти g/collect (перевірка збору, не звітів).
  • Звіряти аномалії 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)
Pipelineabitly-prod-frontend (CodePipeline V2, тригер PUSH_BRANCH на main)
Зламаний деплой7f84c31c7 (phase 5/5c), exec старт 2026-06-11T10:13Z
Останній здоровий62c71091a = task-def rev 23
ВиправленняPR #4223414e42f → task-def rev 27
Аварійний відкат (instant)aws ecs update-service --cluster abitly-prod-frontend --service abitly-prod-frontend --task-definition abitly-prod-frontend:23
  1. GA4 Reports ≠ GA4 collection. Обвал у звітах може співіснувати з повністю здоровим збором. Перша звірка аномалії — із сирим BigQuery-експортом, не з UI. Інакше ризик хибного «дані втрачено» (тут — дані цілі).
  2. Сигнатура важлива. Пропорційний обвал users↔sessions у звітах виглядав як «мертвий page_view», але сирі дані показали over-firing ×2 при нормальному трафіку. Завжди дивись і на views/user.
  3. Авто-деплой на push + відсутність smoke-тесту аналітики = тихі поломки контракту даних деплоять самі себе. «Сайт працює» маскує поломку, бо в даних немає health-check.
  4. Конфіг поза репо (GTM-контейнер, GA4-тумблери) ламає відкати. Відкат коду не повертає консольних змін — тримай їх версіонованими/задокументованими.
  5. Погодинний GA4 Data API на незавершеній добі — семплиться й у tz property. Для хвилинної атрибуції бери raw event_timestamp (UTC) з BigQuery, не Data API-години.
  6. Звіряй конфіг із живим джерелом правди. Хаб казав «автодеплою немає» — AWS показав протилежне (V2 trigger{}).

Пов’язана документація

Section titled “Пов’язана документація”