← всі звіти · analytics.md

type: subdoc parent: interactive-banners name: Interactive Banners — Analytics slug: analytics created: 2026-04-30 last_updated: 2026-05-05

Interactive Banners — Analytics

Як збираються дані з інтерактивних банерів: pixel-GET → Cloud Function → BigQuery → Grafana. Канонічний документ для будь-якого нового бренду.

Pipeline (high-level)

[Doctor browser / DV360 SafeFrame iframe]
   │  banner JS → new Image().src = ENDPOINT/pixel?event=...
   │  ↓ обходить SafeFrame CSP `connect-src` (img-src ширше)
   ▼
[Cloud Function `track`]
   europe-central2-banner-analytics-fluvir.cloudfunctions.net/track/pixel
   │  parse query string → row insert
   ▼
[BigQuery]
   project=banner-analytics-fluvir, dataset=banner_analytics, table=events, location=europe-central2
   ▼
[Grafana dashboard «Fluvir Banner — Live»]
   http://31.131.26.203/grafana/d/fluvir-banner-live/

Cloud Function track

Endpoint

POST https://europe-central2-banner-analytics-fluvir.cloudfunctions.net/track  — legacy JSON body
GET  https://europe-central2-banner-analytics-fluvir.cloudfunctions.net/track/pixel?...  — головний (pixel-tracking)

Чому pixel-GET

Раніше банер використовував navigator.sendBeacon(Blob, application/json) + fetch(POST json). Обидва падають у SafeFrame/sandbox-iframe з CSP connect-src 'self' на більшості майданчиків. Pixel-GET через <img> src проходить через ширшу img-src CSP без CORS preflight.

Параметри (query string)

Param Required Опис
sid session_id (<brand>-<random>-<timestamp>, генерується клієнтом)
name event_name (impression/view_start/game_start/game_finish/cta_click/exit + кастомні)
product brand SKU code (fluvir/menopace/enzybar-gold тощо)
size recommended banner_size (300x250/300x600/320x480 etc)
ver recommended banner_ver (v1/v2/v3-pixel-pizza тощо — для AB-test різних креативів)
cid optional DV360 %%CAMPAIGN_ID%% (працює лише в URL-полях, не у HTML5)
crid optional DV360 %%CREATIVE_ID%%
site optional publisher domain
device optional mobile/desktop/tablet
step optional для багатоступеневих кейсів (e.g. enzybar score progression)
dur optional duration_ms
payload optional JSON-string з кастомними полями для analytics (передається як 1 string)
_t recommended Date.now() — щоб обходити browser-cache

Response

Deploy команди (для оновлення)

gcloud functions deploy track --gen2 --runtime=nodejs20 \
  --region=europe-central2 --trigger-http --allow-unauthenticated \
  --project=banner-analytics-fluvir --entry-point=track

CF код handles обидва: GET (pixel) і POST (legacy JSON body). Логіка повертає 1×1 GIF на GET, 200 на POST.

Multi-product

Один CF — track у проекті banner-analytics-fluvirобслуговує всі бренди через product параметр. Не треба окремий CF для кожного бренду.

BigQuery schema

banner-analytics-fluvir.banner_analytics.events

Колонка Тип Required Опис
event_date DATE автоматично з event_ts
event_ts TIMESTAMP момент події (UTC, Date.now() з клієнта)
session_id STRING <brand>-<random>-<ms>
event_name STRING impression / view_start / game_start / game_finish / cta_click / exit + кастомні
product STRING fluvir / menopace / enzybar-gold
banner_size STRING 300x250 etc
banner_ver STRING versioning AB-tests
campaign_id STRING DV360 macro
creative_id STRING DV360 macro
site STRING publisher
device STRING mobile/desktop/tablet
step INTEGER для multi-step gameplay
duration_ms INTEGER TTI або gameplay duration
payload JSON custom payload (e.g. nutrient_type, enzyme_match etc)

Подія таксономія (canonical)

Event Коли
impression банер ініціалізовано на сторінці
view_start DOMContentLoaded або gameplay container appended
game_start користувач почав інтерактив (натиснув Start, перший drag etc)
game_finish користувач дограв до кінця (passed all stages)
cta_click клік на CTA-кнопку
exit користувач закрив (pagehide listener)

Кастомні події (per banner)

Кожен банер може додавати власні події у тому ж event_name полі. Приклади:

Кастомні події треба фільтрувати на пайплайні рапортів (canonical funnel = тільки 6 базових).

Engagement classification

engaged    = users who reached game_finish AND cta_click after
interrupt  = users with game_start AND cta_click (без game_finish, або клік до finish)
passive    = users with cta_click без game_start (просто misclick/brand-recognition)

SQL шаблон (Grafana panel використовує):

WITH sessions AS (
  SELECT
    session_id,
    MAX(IF(event_name='game_start',  event_ts, NULL)) AS gs_ts,
    MAX(IF(event_name='game_finish', event_ts, NULL)) AS gf_ts,
    MAX(IF(event_name='cta_click',   event_ts, NULL)) AS cta_ts
  FROM `banner-analytics-fluvir.banner_analytics.events`
  WHERE $__timeFilter(event_ts) AND product='fluvir'
  GROUP BY session_id
  HAVING cta_ts IS NOT NULL
)
SELECT
  CASE
    WHEN gf_ts IS NOT NULL AND cta_ts >= gf_ts THEN 'engaged'
    WHEN gs_ts IS NOT NULL AND cta_ts >= gs_ts THEN 'interrupt'
    ELSE 'passive'
  END AS class,
  COUNT(*) AS n
FROM sessions
GROUP BY class

Перші замірі Fluvir 300×600 (2026-04-30): 6.9% engaged / 75.5% passive / ~17.6% interrupt.

75% misclicks — стандартна CPC-метрика бреше про ROI. Engaged-частка — справжній сигнал гри.

Grafana dashboards

Fluvir Banner — Live

URL: http://31.131.26.203/grafana/d/fluvir-banner-live/ Логін: sergey / obsidian-memory-2026

Панелі:

  1. Top stats: Impressions / View starts / Game starts / Game finishes / CTA clicks / CTR
  2. Funnel barGauge — щабель за щаблем
  3. Pie engaged / interrupt / passive
  4. Time-series events per hour
  5. Топ-15 сайтів (impression → CTA → CTR)
  6. Pie device + banner_size
  7. Variable filter «Banner size» (All / 300×250 / 300×600)

Auto-refresh 5 хв, time range default 7d.

Дашборд як код

/srv/services/grafana/dashboards/fluvir-banner-live.json — provisioning JSON. Після правки → Grafana auto-reprovision за 30 секунд.

Як додати дашборд для нового бренду

cp /srv/services/grafana/dashboards/fluvir-banner-live.json \
   /srv/services/grafana/dashboards/<brand>-banner-live.json
# replace product='fluvir' → product='<brand>'
# replace title + uid

Auto-provision підхопить.

Як підключатись до даних з коду

Через SA (programmatic)

import json, time, jwt, requests
sa = json.load(open('/srv/passepartout/google/ad-pipeline-worker-sa.json'))
now = int(time.time())
claim = {
  'iss': sa['client_email'],
  'scope': 'https://www.googleapis.com/auth/bigquery.readonly',
  'aud': 'https://oauth2.googleapis.com/token',
  'iat': now, 'exp': now + 3600,
}
token = requests.post('https://oauth2.googleapis.com/token', data={
  'grant_type':'urn:ietf:params:oauth:grant-type:jwt-bearer',
  'assertion': jwt.encode(claim, sa['private_key'], algorithm='RS256'),
}).json()['access_token']

r = requests.post(
  'https://bigquery.googleapis.com/bigquery/v2/projects/banner-analytics-fluvir/queries',
  headers={'Authorization': f'Bearer {token}'},
  json={'query': '...', 'useLegacySql': False, 'location': 'europe-central2'},
)

SA ad-pipeline-worker@ad-analytics-hub.iam.gserviceaccount.com має cross-project IAM grant (bigquery.dataViewer + jobUser) на banner-analytics-fluvir.

Локація

Завжди використовуй location='europe-central2'. Не EU, не US — інакше 404 «Dataset not found».

Як додати новий бренд (кроки)

  1. Banner code: використовуй той же pixel-GET хелпер з product=<новий-slug>, ver='v1-<concept>'. CF приймає одразу, нічого деплоїти не треба.
  2. DV360 bundle: збери .zip ≤200КБ, залий у DV360 Studio (див. dv360-build-guide.md)
  3. Smoke: після першого live impression перевір BQ:
    SELECT COUNT(*) FROM `banner-analytics-fluvir.banner_analytics.events`
    WHERE product='<новий-slug>' AND event_date = CURRENT_DATE()
    
  4. Grafana dashboard: клонуй fluvir-banner-live.json → заміни product → новий uid/title
  5. Health-check: перші 4h + 24h-таймер (як для Fluvir v2 deployment 2026-04-30)

Відомі обмеження / known issues

  1. DV360 macros %%CAMPAIGN_ID%%/%%CREATIVE_ID%%/%%SITE%% НЕ підставляються в HTML5 креативах — тільки у URL-полях (clickTag etc). У всіх 32 v1-рядках Fluvir BQ ці поля пусті. Workaround:
    • SafeFrame $sf.ext.meta() postMessage parser (TODO впровадити)
    • АБО атрибутувати через cid/crid з DV360 Reports (manual JOIN)
  2. pagehide listener для exit event у sandbox-iframe іноді не вистрелює. Не критично — стандартний user-loss.
  3. BannerTrack.init() у DOMContentLoaded у SafeFrame працює, але старий банер робив ga4('banner_impression', ...) синхронно у кінці тіла (~50ms раніше) — рятувало кілька exit-before-load випадків.

Cross-references