← всі звіти · fix-brief-2026-04-30.md

⚠️ OBSOLETE — див. processed/read-2026-04-30T073547Z-fluvir-banner-tracking-fix.md

Цей brief написано 2026-04-30 ~10:13 БЕЗ перевірки /root/.claude/projects/-/memory/outbox-to-desktop/. Попередня VPS-сесія (07:35Z того ж дня) вже передала Desktop Claude'у точніший handoff з реальною root-причиною (SafeFrame/CSP блокує sendBeacon+fetch, не race у DOMContentLoaded). Цей файл лишається для пост-mortem'у. Source of truth: /root/.claude/projects/-/memory/outbox-to-desktop/processed/read-2026-04-30T073547Z-fluvir-banner-tracking-fix.md


Fluvir Banner v2 — Fix Brief (для Antigravity / Desktop Claude)

Дата: 2026-04-30 Автор: Cloud Claude (VPS) Виконавець: Local Claude (Antigravity) Контекст: обговорення тривало 21.04 (попередня сесія 2026-04-21-voice-proflex-legal-raw), сьогодні Сергій підтвердив «банер треба переробити».


1. Проблема

Банер fluvir 300×600 і 300×250 (v1, generated 2026-04-28) має inline-трекер, який стріляє в Cloud Function https://europe-central2-banner-analytics-fluvir.cloudfunctions.net/track → BigQuery banner-analytics-fluvir.banner_analytics.events.

За 22-30.04 BQ агрегати:

event_name count sessions last_seen
impression 70 70 2026-04-30 07:39
view_start 52 52 2026-04-30 04:07
game_start 26 26 2026-04-30 04:07
game_finish 24 24 2026-04-30 04:07
exit 11 11 2026-04-29 17:15
cta_click 4 4 2026-04-29 18:29

Funnel: impression → view_start 74% → game_start 37% → game_finish 92% (від started) → cta_click 17% (від completed). CTR ≈ 5.7% — це аномально високо, а отже значить що знаменник (impressions) явно занижений.

Підозри (виявлені при code-review):

  1. Race у DOMContentLoaded. Виклик BannerTrack.init() загорнуто в document.addEventListener('DOMContentLoaded', ...). Якщо скрипт виконається ПІСЛЯ DOMContentLoaded (а саме так буде у DV360-friendly бандлі, бо script стоїть в кінці body), listener ніколи не спрацює і impression НЕ відстріляє.
  2. Немає <noscript> fallback-пікселя. При відключеному JS чи блокуванні fetch адблокером — імпресія втрачається без сліду.
  3. Немає try/catch навколо init. Якщо щось кине помилку ДО рядка once('impression') — імпресія втрачена. Сам init не загорнутий.
  4. DV360-макроси %%CAMPAIGN_ID%%, %%CREATIVE_ID%%, %%SITE%% — НЕПРАВИЛЬНІ для DV360. Це синтаксис Campaign Manager (DCM), а не DV360. У BQ всі ці поля = '' або NULL. Як наслідок — неможливо звести events з конкретними lineitem-ами/креативами в DV360 Reports.
  5. exit через pagehide only. На частині мобільних браузерів pagehide не триггериться при background-tab-kill. Краще combo: pagehide + visibilitychange (hidden) + кеш sessionStorage для resume.

2. Файли

3. Що зробити

3.1 Виправити impression-delivery

A. DOMContentLoaded race-fix. Замість:

document.addEventListener('DOMContentLoaded', function() { BannerTrack.init({...}); });

використати:

function bootBanner() {
  try { BannerTrack.init({...}); }
  catch (e) {
    // last-resort fallback pixel
    try { (new Image()).src = ENDPOINT + '?n=impression&err=' + encodeURIComponent(String(e)) + '&size=...&ver=v2&_=' + Date.now(); } catch(_) {}
  }
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootBanner);
else bootBanner();

B. Найперший рядок init — це imp pixel. Зараз once('impression') стоїть після ctx.cid = dv(opts.cid) і кількох інших присвоєнь. Перенести once('impression') НА ПЕРШИЙ рядок init після читання opts.product/size/ver (мінімум для сегментації).

C. <noscript> fallback impression pixel прямо в <body> (до основного <div id="banner">):

<noscript>
  <img src="https://europe-central2-banner-analytics-fluvir.cloudfunctions.net/track?evt=impression&fallback=noscript&product=fluvir&size=300x600&ver=v2"
       width="1" height="1" style="position:absolute;visibility:hidden" alt="">
</noscript>

Плюс — у Cloud Function track потрібно додати GET-handler, бо зараз він приймає тільки POST з JSON body. Перевірити в /srv/projects/banner-analytics-fluvir/functions/track/ (якщо існує) або в Console.

3.2 Виправити DV360-макроси

DV360 (на відміну від DCM) не підставляє макроси всередині HTML5 ZIP. Стандартні DV360 макроси (${CAMPAIGN_ID}, ${CACHEBUSTER} тощо) працюють тільки в URL-полях creative-trafficking-конфігу, не в файлах банера.

Варіанти:

Для v2 робимо Варіант A. Документуємо в /srv/wiki/interactive-banners/index.md як «процес пакування creative ZIP».

3.3 Бамп ver

ver: 'v2'

у BannerTrack.init({...}). Старі v1 events залишаються в BQ для baseline-порівняння, нові події ідуть з ver='v2' і легко відфільтровуються:

SELECT event_name, COUNT(*) FROM ... WHERE banner_ver='v2' GROUP BY 1

3.4 (Опційно, не блокує деплой) Reliability tweaks

4. Як тестувати ПЕРЕД uploadом у DV360

  1. Локальний preview:

    cd /tmp/fluvir_300x600 && python3 -m http.server 8000
    # відкрити http://VPS_IP:8000 в браузері, перевірити DevTools Network → POST на /track
    
  2. Перевірка в Cloud Function logs:

    gcloud functions logs read track --region=europe-central2 --project=banner-analytics-fluvir --limit=50
    

    (SA ad-pipeline-worker@ad-analytics-hub.iam.gserviceaccount.com має roles/logging.viewer на цьому проекті — підтверджено вчора).

  3. BQ перевірка через 5 хв:

    SELECT event_name, banner_ver, COUNT(*) AS cnt, MAX(event_ts) AS last
    FROM `banner-analytics-fluvir.banner_analytics.events`
    WHERE banner_ver = 'v2'
    GROUP BY 1, 2 ORDER BY 1
    

    Має бути ≥1 impression, ≥1 view_start.

  4. DV360 preview: залити test-ZIP як новий creative (v2-test), запустити preview-mode (без витрат бюджету), клікнути → перевірити, що відстрілюються cta_click + опен landing.

  5. Прод-rollout:

    • REPLACE creative asset у DV360 (не створювати новий creative, щоб creative_id залишився той самий і метрика історично з'єдналась)
    • Re-approval займає 1-4 год
    • Через 24 год запустити порівняння DV360 reports vs BQ:
      SELECT DATE(event_ts) AS d, banner_size, COUNT(DISTINCT session_id) AS bq_imps
      FROM `banner-analytics-fluvir.banner_analytics.events`
      WHERE event_name='impression' AND banner_ver='v2'
        AND event_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)
      GROUP BY 1, 2 ORDER BY 1, 2
      
    • Цільовий gap (DV360 - BQ) / DV360 < 10%. Якщо все ще >50% — копати глибше: window-нештатний MCC adblocker, iframe sandbox restrictions.

5. Делівері

  1. Покласти оновлені файли:

    • /srv/wiki/interactive-banners/samples/v2/fluvir_300x600.html
    • /srv/wiki/interactive-banners/samples/v2/fluvir_300x250.html
  2. ZIP-пакування: один шейл-скрипт /srv/wiki/interactive-banners/samples/v2/pack.sh, який бере fluvir_300x600.htmlfluvir_300x600.zip (тільки один файл всередині, без папок). Той самий формат, що зараз надсилав Сергій.

  3. Оновити /srv/wiki/interactive-banners/index.md — додати розділ «v2 (2026-04-30)» з описом фіксів.

  4. Скинути готові ZIP-и в Telegram до Сергія (чи покласти в /srv/wiki/interactive-banners/samples/v2/ і пінгнути «готово, шлях»).

6. Не робити

7. Definition of Done

Context appendix — endpoints/credentials