Files
vtube/docs/ADS.md
goboss c9890a32fe Add ad-test regression tool (Playwright headless ad-flow validator)
After 8148 ASG account recreation + spot rotation debug session,
formalize tooling for automated ad-pipeline regression checks.

- /home/w4/bin/ad-test — wrapper, available globally
- /home/w4/playwright-tests/ — Playwright + Chromium runtime
- scripts/ad-regression.mjs — versioned copy у репо

Per-URL checks (6):
  1. ad-bundle loaded (window._adConfig set)
  2. popunder SDK fetches
  3. /api/users/<vast_spot> returns <Ad> (catches no-fill)
  4. mode = "vast" after popunder cooldown active
  5. .asg-container injects on play click (right data-spot-id)
  6. console clean (no [ASGB LOADER] errors)

Headless limitation documented: network+DOM checks cover 95% of
regressions without need for full video playback validation.
Use `ad-test --full` or real browser w/ ?debug=1 for visual.

Memory: reference_ad_regression_tool.md added — auto-trigger rule
to run ad-test after spot ID changes / mirror swaps / bundle rebuild.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 22:45:31 +00:00

295 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# tubev — Ads Integration
Реклама через **adspyglass.com**. Основне джерело монетизації — балансуємо CTR vs UX/PSI.
## Status
🟢 **Working knowledge** — Skeleton наповнюємо.
## ⚙️ Automated regression test — `ad-test`
Швидкий smoke перевірити що ad flow не поламано після зміни.
```bash
# default — 3 URLs з 8148 (швидкий smoke)
ad-test
# конкретні URLs
ad-test "https://www.xn--3dsq7teoyo9d.com/v/box/123/foo" \
"https://t1.atube.sex/v/box/456/bar"
# з screenshots + JSON report
ad-test --screenshot --json "https://..." "https://..."
```
**Що перевіряє per URL:**
1. ✓ ad-bundle loaded (`window._adConfig` set, spot IDs читаються)
2. ✓ popunder SDK loads
3.`/api/users/<vast_spot>` повертає `<Ad>` (не empty no-fill)
4. ✓ mode = "vast" після popunder cooldown
5.`.asg-container` injects на click `pjs_play_btn` (правильний `data-spot-id`)
6. ✓ console clean (no `[ASGB LOADER]` errors, no `pageerror`)
**Exit code:** 0 = всі pass, 1 = ≥1 fail. Можна chain у CI / git hook.
**Реалізація:**
- Wrapper: `/home/w4/bin/ad-test`
- Script: `/home/w4/playwright-tests/ad-regression.mjs` (Playwright + Chromium headless)
- Repo copy (versioned): `scripts/ad-regression.mjs` + `scripts/ad-test.sh`
**Обмеження headless:**
- Real video playback не verifies (autoplay restrictions, VPAID не fully supported у chromium-headless-shell)
- Network-level VAST chain validation (через `<Ad>/<InLine>/<Wrapper>` детекцію) cover 95% regressions без візуальної перевірки
- Якщо треба видеоплей — `ad-test --full` (full chromium, але VPAID все ще incomplete)
- Альтернатива для full visual: запусти у real browser з `?debug=1` + дивись HUD
## Architecture
### Mirrors (зеркала)
Реклама вантажиться через **зеркала-домени**, не прямо `adspyglass.com`:
- `a5.g--o.info` — ad orchestration (preconnect target у modern setup)
- `surstrom.com` — inline ad-spot loader (legacy pattern)
- `agego.com` — verifycdn (`verifycdn.agego.com/v1/verify.js`)
- `g--o.info` — base domain
- (інші додаються на потребу)
**Чому зеркала:** anti-block — adblockers баняють відомі ad-домени. Зеркала маскують та ротуються.
**Mirror lifecycle:**
- Зеркала **періодично баняться** (adblock list updates)
- Заміна — **mass-operation через Адміна** (одразу на всі сайти)
- Ми не керуємо вибором зеркал — це адмінська функція
### Three architecture patterns coexist (audit 2026-05-02)
Site-level audit показав **3 patterns** — не uniform:
| Pattern | Sites | Як виглядає | Примітка |
|---------|-------|-------------|----------|
| **A) Modern bundle** | 1 (тільки 8148) | `<script src="/static/js/ad-bundle.min.js?v=<md5>" defer>` + `views/modules/banners/*adspy*.etlua` partials | Canonical — є build pipeline, source JS у `views/static/js/` |
| **B) Modern partials** | ~23 | `preconnect``a5.g--o.info` + adspy `.etlua` partials, але **без** ad-bundle.min.js | Modules orchestrate, no central bundle |
| **C) Legacy inline** | 31 (8112-style) | Inline `<script async src="//surstrom.com/<random>.js">` + iframes до `surstrom.com/api/spots/<id>` + `tb.load_frame_baner_v2()` API | Pre-modular epoch |
**77 із 94 сайтів** мають adspy banner modules у `views/modules/banners/*adspy*.etlua`. Решта (17) — legacy без banner partials або special structure.
### Build pipeline (тільки 8148)
`/home/nosfortube/frontend_8148/views/static/js/build-ad-bundle.sh` концатує+minify-їть 5 source JS-файлів у `ad-bundle.min.js`:
1. `ad-config.js` — config / placement IDs / mirror domains
2. `ad-core.js` — ядро ad-orchestration
3. `ad-mute.js` — mute / autoplay control
4. `vast-preroll.js` — pre-roll реклама перед video
5. `ad-bootstrap.js` — entry-point / init
**Source files:** усі у `/home/nosfortube/frontend_8148/views/static/js/` (унікально для 8148).
Tool: **terser** (concat + minify). Triggered by `~/git-save-all.sh` коли source новіший за bundle.
Cache-bust: md5 → `?v=...` у `layout.etlua` (детально у [DEPLOY.md](DEPLOY.md#cache-bust-механізм)).
### Banner partial loader — `tb.load_frame_baner_v2`
Source: `views/static/js/lib/common/js/tbanner.etlua` (shared lib з кешуючою етлуа-обгорткою) + `tbanner_min.etlua` (minified inline). Підключається у layout.etlua через `<% render("static.js.lib.common.js.tbanner_min") %>`.
**Сигнатура:** `tb.load_frame_baner_v2(spot_url, dom_target, timeouts, attrs [, callback])`
- `spot_url``//a5.g--o.info/api/spots/<spot_id>?p=1`
- `dom_target``#selector` куди append iframe
- `timeouts``{event_min_timeout, event_max_timeout}` ms
- `attrs``{height, width, ...}` iframe attributes / style
- `callback` — викликається після append (для native — інсталяція postMessage listener-у)
**Lazy-load logic:**
- Чекає user event (`scroll`, `mousemove`, `touchstart`, `resize`, `mouseenter`, `click`) АБО `event_min_timeout` (раніше)
- Якщо tab hidden → відкладає до `visibilitychange`
- `event_max_timeout` — fallback hard limit
- Counter per banner — окрема черга для кожного instance
- iframe attrs default: `sandbox="allow-scripts allow-popups allow-forms allow-same-origin"`, `loading="lazy"`, `class="na"`
### Native banners — auto-height через postMessage
Native (`tbn1`, `tbn2`, `tbn3`) — adaptive ads з невідомою висотою. Pattern:
1. **На стороні ASG** користувач прописує код банера (TrafficStars або ExoClick) + кастом-CSS для responsive grid + кастом-JS який postMessage-ить розмір.
2. **На стороні сервера** ASG обертає у iframe (через `/api/spots/<id>` endpoint).
3. **Frontend** ловить `window.message` event від iframe → парсить JSON `{url, height}` → встановлює `height` attribute на iframe з `transition: all .3s`.
**Network providers за ASG (приклад інтеграції на adспy admin side):**
- **TrafficStars** — `cdn.tsyndicate.com/sdk/v1/n.js` + `NativeAd({element_id, spot, type: "label-under", cols:4, rows:1, ...})`
- **ExoClick** — `a.magsrv.com/ad-provider.js` + `<ins class="eas..." data-zoneid="..." data-keywords="%KW%" data-sub="%SUB1%"></ins>` + `(AdProvider).push({"serve":{}})`
Обидва вкладають кастомний `<style>` з 4-col responsive grid (≥739px: 4 cols, 450-738: 2, <450: 1). Плюс postMessage скрипт що шле parent розмір на load/resize/mutation.
ASG керує цими SDK runtime-no — ми бачимо тільки iframe.
### Spot ID location matrix (8148 — 16 spots)
| Тип | Файл | Spot ID location |
|-----|------|-----------------|
| popunder | `views/static/js/ad-config.js` | `popunder.spot` |
| VAST/in-video | `views/static/js/ad-config.js` | `vast.spot` |
| popunder cooldown matcher | `views/layout.etlua` | inline regex `(?:^|\|)<id>=` (must sync з ad-config) |
| desktop footer ×4 | `views/modules/banners/footer_descktop_adspy.etlua` | hardcoded URL `spots/<id>?p=1` |
| mobile footer/header/middle | `views/modules/banners/{footer,header,middle}_mobile_adspy.etlua` | same |
| desktop sidebar ×2 | `views/modules/banners/embed_sidebar_adspy.etlua` | same |
| mobile sidebar ×2 | `views/modules/banners/embed_mobile_sidebar_adspy.etlua` | same |
| native (3 partials) | `views/modules/banners/native_*adspy.etlua` | same |
**При зміні spot IDs (rotation / re-creation):** оновити ВСІ перелічені місця, потім `bash /home/w4/git-save-all.sh "msg"` — auto-rebuild bundle + cache-bust.
⚠️ **Не забути `layout.etlua` cooldown regex** — окрема hardcoded reference на popunder spot ID (для localStorage `asgsl` cookie matching).
## Monitoring
### adspyglass dashboard — best diagnostic signal
`https://app.adspyglass.com/dashboard`
**Чому це best signal:**
- Live графік показу реклами per site
- **Падіння графіка** = реклама не показується = ймовірно поломка (frontend/template/JS error / banned mirror / CDN cache stale)
- **Ріст** = трафік підріс або UX покращився
Юзер каже: "більший показник саме для мене це лайв графік реклами від АСГ" — реклама падає миттєво коли щось не так, ще до того як прийдуть алерти.
### Інші alerts
- **Telegram + email** — критичні (downtime, errors)
- Billing dashboard `https://billing.g--o.info/cs/oursites/dashboard/` — статистика (зазвичай не дивимось)
### Template patterns
**Pattern A (8148 modern):**
```etlua
<link rel="preconnect" href="https://a5.g--o.info" crossorigin>
<script src="/static/js/ad-bundle.min.js?v=<%= ad_bundle_v %>" defer></script>
...
<% render("views.modules.banners.header_mobile_adspy") %>
<% render("views.modules.banners.footer_mobile_adspy") %>
<% render("views.modules.banners.footer_descktop_adspy") %>
```
**Pattern C (8112 legacy inline):**
```etlua
<link rel="dns-prefetch" href="//surstrom.com">
...
<script async src="//surstrom.com/qDap9.js"></script>
<iframe class="na" src="//surstrom.com/api/spots/72437?p=1" sandbox="..." loading="lazy"></iframe>
...
tb.load_frame_baner_v2("//surstrom.com/api/spots/72437?p=1","#tb0",{...},{...});
```
## Що треба з'ясувати + задокументувати
### Architecture migration
- [ ] Чи планується мігрувати pattern C → B/A (legacy → modern)?
- [ ] 8148 — pilot для bundle-architecture, чи поширюватиметься?
### CSP / Security
- [ ] CSP exceptions для зеркал — як підтримуються?
- [ ] Mirror swap — чи треба update CSP при заміні?
### Performance impact
- [ ] PSI вплив реклами — % LCP / TBT (per pattern)?
- [ ] Lazy-load ads below-fold — реалізовано (видно `loading="lazy"` на iframes)?
## VAST flow (8148 detailed)
### User experience
1. Користувач клацає play на `pjs_play_btn` (PlayerJS player)
2. `asgInVideoImpression` event від ASG SDK → `_decideAndApply` ловить → запускає `VastPreroll.show()`
3. ASG іnject-ить `.asg-container` з video preroll
4. `MutationObserver` ловить container → pin pointer + z-index 5000, добавляє progress bar (`.vast-progress` yellow #ffd000)
5. Coordinated mute: video у container синхронізується з `_vastMuted` localStorage (per-tab persist)
6. Після VAST done (video.ended OR `currentTime >= duration - 0.2`) → callback `window._asgAdDone()` → main video starts via `window._revealPjs()`
7. `enableMediaPlayHijack: true` блокує race у HTMLMediaElement.play() якщо `_adsLocked` — попереджає 1-frame audio artifact коли VAST стартує над main video
### Cooldown management
- ASG локальне сховище: `localStorage.asgsl` — pipe-separated `<spot_id>=<key>:<value>` записи. Ключі що використовуються нашим bundle:
- `global_rr:<timestamp>``_readCooldowns` parse-ить це як cooldown end time
- `n:<timestamp>` — impression handler копіює це у `_popRr` / `_vastRr` cookies+LS
- Дублюючі cookies: `_popRr`, `_vastRr`. Реплікуються з asgsl при impression event.
- Декрипт пам'яті: дві ключові події з ASG — `asgPopunderImpression`, `asgInVideoImpression` — тригерять запис cooldown.
### ⚠️ ASG spot config gotcha — frequency-capping mandatory
**Bug 2026-05-02 (8148, after account recreation):** popunder spot 514208 у новому ASG account-і був налаштований з `shows_limit:1` без frequency-capping rotation. Симптоми:
- popunder СПрацьовує (видно `asgPopunderImpression`)
- Але `asgsl` локально вигляд: `<spot>=keep_looping:false,tabunder:false,uuid:...,noloop:true,shows_limit:1`
- **Нема `n:<timestamp>`** → impression handler виходить без write `_popRr`
- **Нема `global_rr:<timestamp>`** → `_readCooldowns` повертає `popActive=false`
- → mode завжди "pop" → popunder fires кожен pageload → VAST SDK ніколи не loadиться
**Fix у ASG admin** (на стороні юзера, не у нас):
- Відкрити spot config → знайти "Global rotation" / "Frequency capping" / "Cooldown" / "Time-based capping" / "Show frequency"
- Встановити, наприклад, "once per 2 hours per user" — це додасть `n:` / `global_rr:` поля в asgsl
- **Те саме для VAST spot** — інакше після популярного pop firing, VAST mode triggered але cooldown поломаний
**Як перевірити:** після popunder fire відкрий DevTools → `localStorage.getItem('asgsl')` → шукай `global_rr:` або `n:`. Якщо нема → не налаштовано.
### ⚠️ ASG no-fill diagnostic — VAST returns empty `<VAST/>`
**Bug 2026-05-02 (8148, VAST never visible):** Mode перемикався у "vast", VAST SDK loaded, але реклама не з'являлась візуально.
Перевірка endpoint напряму:
```bash
curl 'https://a5.g--o.info/api/users/<vast_spot>?v2=1&fill=0&url=<page-url>'
```
Якщо response:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<VAST version="3.0">
</VAST>
```
— це **VAST no-fill**. ASG не має inventory для цього spot.
**Порівняй з popunder endpoint** який повертає повний JSON-like config з `creative_id`, `behavior`, тощо:
```js
window.__NA.renderSpot({ spot_id: ..., creative_id: 83660, config: {...} });
```
**Можливі причини no-fill (по пріоритету):**
1. Spot **type ≠ "In-video VAST"** — створено як Banner/Popunder/etc → VAST endpoint не має чого serve
2. Spot **Status pending review** — створено, чекає approval
3. **Creative не attached** — VAST потребує специфічний ад чи bidder pool
4. **Bidder / network не linked** — VAST джерело мережі (ExoClick VAST / TS / etc) не призначено
5. Budget / cap exhausted
**Як остаточно verify:** через playwright headless + manual inject `_popRr` форсити VAST mode → click play → дивитися чи з'являється `.asg-container`. Якщо ні + curl показує empty VAST → no-fill.
### Mode decision (`AdCore._decide`)
```
mode = popActive ? (vastActive || skipPattern ? "none" : "vast") : "pop"
```
| popunder | vast | mode |
|----------|------|------|
| available (cooldown OFF) | — | **pop** (priority) |
| у cooldown (recent fire) | available + не skip-pattern | **vast** |
| у cooldown | у cooldown OR skip-pattern | none |
**Implicit dependency:** VAST показується **тільки після того як popunder уже спрацював** і поставив cooldown. На свіжій сесії (нема `_popRr` у localStorage / cookies) → mode завжди "pop", VAST SDK навіть не завантажується.
⚠️ Це **gotcha при тестуванні**: якщо щойно фікснули popunder spot — VAST не з'явиться поки popunder не спрацює хоча б раз. Юзер має думати що "VAST зламаний", а він просто заблокований mode logic.
**Force VAST testing**: `?clearAds=1` URL param очищає `_popRr/_vastRr/_vastPatternIdx/_pw/asgsl/_pjsLog` — фрешний state. Тоді: page load → pop fires → cooldown set → refresh → mode=vast → VAST SDK loaded.
`vastPolicy "show-1-skip-1"` додатково: idx=0 → SHOW, idx=1 → SKIP (incremented per VAST impression). На свіжій сесії idx=0 → перша VAST показується, друга skip-неться.
### Policy choice
`pop-priority` policy + `show-1-skip-1` для VAST — **навмисний баланс ревеню vs UX**:
- Popunder показується відносно рідко (cap від ASG високий)
- VAST спрацьовує часто, тому штучно скачуємо до 50% (`show-1-skip-1`) — менше тиску на юзера, не вбиває engagement
- Якщо колись треба максимізувати ревеню → `vastPolicy: 'show-always'`
## Open questions for developer
1. Як здійснюється mirror swap — sed-replace у всіх template/JS файлах через Адміна, runtime config, чи інший механізм?
2. Які pattern використовується для НЕ-77 сайтів (17 без adspy banner modules) — special-case structure, "no ads" сайти, чи щось інше?
3. SDK URLs (`nDNVal3.js`, `9iO21Eb.js`) — чи стабільні чи можуть змінитися при mirror rotation?