Files
vtube/docs/ADS.md
goboss a2d637c483 ADS.md: add tb.load_frame_baner_v2 details, native postMessage pattern,
spot ID location matrix, VAST flow walkthrough, policy rationale

Findings from 8148 spot-ID rotation audit (2026-05-02):
- Banner partial loader source: views/static/js/lib/common/js/tbanner.etlua
  (shared lib, lazy-load via user events / timeout, hidden-tab handling)
- Native banners use postMessage from iframe→parent for auto-height
  (ASG admin wraps TrafficStars/ExoClick code which postMessages dimensions)
- Spot IDs живуть у 4 місцях: ad-config.js + 9 banner partials + layout.etlua
  cooldown regex (popunder ID hardcoded в asgsl matcher)
- VAST flow detailed: pjs_play_btn → asgInVideoImpression → VastPreroll.show()
  → .asg-container → _asgAdDone callback → _revealPjs()
- pop-priority + show-1-skip-1 = intentional rev/UX balance (popunder rare,
  VAST throttled to 50%)

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

12 KiB
Raw Blame History

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 preconnecta5.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).

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):

  • TrafficStarscdn.tsyndicate.com/sdk/v1/n.js + NativeAd({element_id, spot, type: "label-under", cols:4, rows:1, ...})
  • ExoClicka.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 `(?:^
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):

<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):

<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> записи. Ключ global_rr = "remaining ratio" (timestamp до якого spot у cooldown).
  • Дублюючі cookies: _popRr, _vastRr. Реплікуються з asgsl при impression event.
  • Декрипт пам'яті: дві ключові події з ASG — asgPopunderImpression, asgInVideoImpression — тригерять запис cooldown.

Mode decision (AdCore._decide)

  • popActive = true && vastActive = truenone (поки активна recoverFromNone)
  • popActive = falsepop (priority)
  • popActive = true && vastActive = falsevast
  • AND vastPolicy "show-1-skip-1" skip pattern на половину VAST imps

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?