Files
vtube/docs/ADS.md
goboss a2f51bac2a ADS: document asgsl frequency-capping gotcha (8148 popunder/vast bug)
Real-browser probe (playwright + Chromium headless) revealed bundle
expects `global_rr:<ts>` and `n:<ts>` fields in asgsl localStorage
to set _popRr / _vastRr cooldowns. New ASG account spot 514208 is
configured with shows_limit:1 without these timestamp fields, so:
- popunder fires every pageload
- _popRr never written → popActive=false always
- mode never transitions to vast → VAST SDK never loads

Resolution requires ASG admin to enable frequency-capping rotation
on the spot (на стороні юзера). Documented detection: check
localStorage.getItem('asgsl') for 'global_rr:' or 'n:' tokens.

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

227 lines
14 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 наповнюємо.
## 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:`. Якщо нема → не налаштовано.
### 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?