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>
227 lines
14 KiB
Markdown
227 lines
14 KiB
Markdown
# 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?
|