Цей 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
Дата: 2026-04-30 Автор: Cloud Claude (VPS) Виконавець: Local Claude (Antigravity) Контекст: обговорення тривало 21.04 (попередня сесія
2026-04-21-voice-proflex-legal-raw), сьогодні Сергій підтвердив «банер треба переробити».
Банер 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):
BannerTrack.init() загорнуто в document.addEventListener('DOMContentLoaded', ...). Якщо скрипт виконається ПІСЛЯ DOMContentLoaded (а саме так буде у DV360-friendly бандлі, бо script стоїть в кінці body), listener ніколи не спрацює і impression НЕ відстріляє.<noscript> fallback-пікселя. При відключеному JS чи блокуванні fetch адблокером — імпресія втрачається без сліду.once('impression') — імпресія втрачена. Сам init не загорнутий.%%CAMPAIGN_ID%%, %%CREATIVE_ID%%, %%SITE%% — НЕПРАВИЛЬНІ для DV360. Це синтаксис Campaign Manager (DCM), а не DV360. У BQ всі ці поля = '' або NULL. Як наслідок — неможливо звести events з конкретними lineitem-ами/креативами в DV360 Reports.exit через pagehide only. На частині мобільних браузерів pagehide не триггериться при background-tab-kill. Краще combo: pagehide + visibilitychange (hidden) + кеш sessionStorage для resume.Поточний live (v1, 2026-04-28):
/tmp/fluvir_300x600/index.html (710 рядків)/tmp/fluvir_300x250/index.html/srv/wiki/interactive-banners/samples/v1/ для постійного тримання.Старий референс (v2 з Apps Script-трекером, 2026-04-13):
/srv/wiki/interactive-banners/samples/fluvir_banner_300x250_v2.html — НЕ використовувати, інша архітектура трекінгу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.
DV360 (на відміну від DCM) не підставляє макроси всередині HTML5 ZIP. Стандартні DV360 макроси (${CAMPAIGN_ID}, ${CACHEBUSTER} тощо) працюють тільки в URL-полях creative-trafficking-конфігу, не в файлах банера.
Варіанти:
Варіант A (рекомендую для MVP): хардкодити cid/crid/site при пакуванні ZIP-у. Кожен варіант креативу генерувати з власним bake-in:
cid: 'fluvir-2026q2-display',
crid: 'fluvir-300x600-v2',
site: '%FROM_DV360_LINEITEM%', // якщо потрібно різати по lineitem — пакувати окремий ZIP
Це означає: для кожного нового запуску в DV360 — окремий ZIP з прошитим crid. Не масштабується вічно, але дає атрибуцію зараз.
Варіант B: додати DV360 third-party impression tracker URL у campaign settings, який буде містити справжні ${CAMPAIGN_ID} і пінгати окремий лог-ендпойнт. Звести з нашими подіями по часу + sid (через cookie). Складніше, але дозволяє динамічну атрибуцію.
Варіант C (довгостроково): перейти на Studio Enabler / або DV360 Custom Renderer (якщо взагалі підтримується для звичайних display-кампаній). Окрема ресерч-задача.
Для v2 робимо Варіант A. Документуємо в /srv/wiki/interactive-banners/index.md як «процес пакування creative ZIP».
ver: 'v2'
у BannerTrack.init({...}). Старі v1 events залишаються в BQ для baseline-порівняння, нові події ідуть з ver='v2' і легко відфільтровуються:
SELECT event_name, COUNT(*) FROM ... WHERE banner_ver='v2' GROUP BY 1
exit event: дублювати на visibilitychange коли document.visibilityState === 'hidden'.view_start: якщо IntersectionObserver не запустився за 3 секунди — стріляти forced через setTimeout.localStorage failed-beacon тіла, на наступному impression — батч-flush їх у /track-batch ендпойнт. Це для пост-mortem розслідувань. Можна винести в окремий PR.Локальний preview:
cd /tmp/fluvir_300x600 && python3 -m http.server 8000
# відкрити http://VPS_IP:8000 в браузері, перевірити DevTools Network → POST на /track
Перевірка в 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 на цьому проекті — підтверджено вчора).
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.
DV360 preview: залити test-ZIP як новий creative (v2-test), запустити preview-mode (без витрат бюджету), клікнути → перевірити, що відстрілюються cta_click + опен landing.
Прод-rollout:
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
Покласти оновлені файли:
/srv/wiki/interactive-banners/samples/v2/fluvir_300x600.html/srv/wiki/interactive-banners/samples/v2/fluvir_300x250.htmlZIP-пакування: один шейл-скрипт /srv/wiki/interactive-banners/samples/v2/pack.sh, який бере fluvir_300x600.html → fluvir_300x600.zip (тільки один файл всередині, без папок). Той самий формат, що зараз надсилав Сергій.
Оновити /srv/wiki/interactive-banners/index.md — додати розділ «v2 (2026-04-30)» з описом фіксів.
Скинути готові ZIP-и в Telegram до Сергія (чи покласти в /srv/wiki/interactive-banners/samples/v2/ і пінгнути «готово, шлях»).
clickTag поведінку — window.clickTag мусить лишатися глобальною змінною з default URL.python3 -m http.server показує POST на /track в DevTools з event_name=impressionbanner_ver='v2' після локального тесту<noscript>-пікселя (якщо тестова сторінка завантажена з вимкненим JS)index.md оновлено/srv/wiki/interactive-banners/samples/v2/ готові до завантаження в DV360banner-analytics-fluvir.banner_analytics.eventshttps://europe-central2-banner-analytics-fluvir.cloudfunctions.net/track (POST JSON, треба додати GET-handler для noscript-fallback)banner-analytics-fluvir/srv/passepartout/google/ad-pipeline-worker-sa.json (має bigquery.dataViewer, bigquery.jobUser, roles/logging.viewer)