Files
vtube/docs/ADS.md

28 KiB
Raw Blame History

tubev — Ads Integration

Реклама через adspyglass.com. Основне джерело монетизації — балансуємо CTR vs UX/PSI.

Status

🟢 Working knowledge — Skeleton наповнюємо.

⚙️ Automated regression test — ad-test

Швидкий smoke перевірити що ad flow не поламано після зміни.

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

Native 1thumb_a..f — single-banner-per-thumb pattern

views/modules/banners/native_allpg_1thumb_{a,b,c,d,e,f}.etlua6 окремих banner модулів, кожен:

  • ОДИН <div id="thmb<N>"> (унікальний DOM ID #thmb1#thmb6)
  • ОДИН spot URL //a5.g--o.info/api/spots/<id>?p=1
  • script tb.load_frame_baner_v2(spotURL, "#thmb<N>", ...) injects iframe всередину

Призначення: заміняти thumbnail у grid-листингах (cat_list_thumb, video.etlua, related_video, recommended_video) на native banner imp що виглядає як thumb.

Render умова: if i == 8 and not isMobile() (desktop) OR i == 5 and isMobile() (mobile) — позиція у grid де banner injects.

⚠️ Унікальність DOM ID per page: один render = один module = один #thmbN. Якщо рендериш 1thumb_a у двох різних модулях (наприклад related_video AND recommended_video) на одній сторінці — отримаєш DUPLICATE #thmb1 (HTML5 violation, ad iframe injects only into first match). Fix: використовуй РІЗНІ модулі (1thumb_a + 1thumb_b) у різних блоках.

Per-site spot inventory

8148 (xn--3dsq7teoyo9d.com — pilot site)

Канонічний reference. Spot IDs у git history; перелік типів — у matrix вище.

8161 (adultmovz.com)

Migrated 2026-05-03 (videojs4 → PlayerJS+ad-bundle). URL pattern /v-arch/.

Spot ID Тип DOM target File
514125 popunder static/js/ad-config.js + cooldown regex у layout.etlua
514120 VAST in-video pjs_play_btn click static/js/ad-config.js
514116 desktop footer A #da_a modules/banners/footer_descktop_adspy.etlua
514117 desktop footer B #da_b same
514118 desktop footer C #da_c same
514119 desktop footer D #da_d same
514123 mobile header #hdm modules/banners/header_mobile_adspy.etlua
514124 mobile footer #ftm modules/banners/footer_mobile_adspy.etlua
514138 mobile middle #mdm modules/banners/middle_mobile_adspy.etlua
514121 desktop sidebar A #sdd_a modules/banners/embed_sidebar_adspy.etlua
514122 desktop sidebar B #sdd_b same
514139 mobile sidebar A #dsd_a modules/banners/embed_mobile_sidebar_adspy.etlua
514140 mobile sidebar B #dsd_b same
514295 native desktop allpg #tbn1 modules/banners/native_allpg_desktop_adspy.etlua
514297 native mobile allpg #tbn2 modules/banners/native_allpg_mobile_adspy.etlua
514296 native embed #tbn3 modules/banners/native_embed_adspy.etlua
514371 native 1thumb A #thmb1 modules/banners/native_allpg_1thumb_a.etlua (use: related_video, cat_list_thumb, video)
514372 native 1thumb B #thmb2 1thumb_b.etlua (use: recommended_video, cat_list_thumb, video)
514373 native 1thumb C #thmb3 1thumb_c.etlua (cat_list_thumb, video)
514374 native 1thumb D #thmb4 1thumb_d.etlua (cat_list_thumb, video)
514375 native 1thumb E #thmb5 1thumb_e.etlua (cat_list_thumb, video)
514376 native 1thumb F #thmb6 1thumb_f.etlua (cat_list_thumb, video)

Migration checklist — legacy (videojs4) → modern (PlayerJS + ad-bundle)

Контрольні чекпоінти при міграції наступного site (template після 8148/8161 досвіду). Кожен ✓ — окремий atomic commit.

Pre-flight

  • Identify URL pattern. /v/ (default), /v-arch/, /video/, etc. Це йде у ad-bootstrap.js path checks (2 occurrences) і у тестових URL-ах.
  • Get spot IDs from ASG admin. Мінімум 16 spots (popunder + VAST + 4 desktop footer + 1 mobile header + 1 mobile footer + 1 mobile middle + 2 desktop sidebar + 2 mobile sidebar + 3 native + опціонально 6 1thumb_a-f). Записати mapping.
  • Verify ASG spot config: frequency capping ENABLED на popunder + VAST (інакше cooldown поломається — див. ## ⚠️ ASG spot config gotcha вище).

Banner modules

  • Replace banner spot IDs у всіх views/modules/banners/*adspy*.etlua + 1thumb_*.etlua (per inventory). Sed-batch friendly.
  • Verify zero stale IDs після replace: grep -r '<old_id>' views/ повинен returnати empty (test/comments OK).

JS bundle (port from 8148)

  • Copy 5 source filesviews/static/js/: ad-config.js, ad-bootstrap.js, ad-core.js, ad-mute.js, vast-preroll.js + build-ad-bundle.sh.
  • Edit ad-config.js: set popunder.spot, vast.spot, _adConfigBuildId (унікальний marker для cache-bust verify).
  • Edit ad-bootstrap.js: додати site URL pattern до обох path checks (HUD interval + _adCtx init), напр. ||location.pathname.indexOf('/v-arch/')===0.
  • Build bundle: bash views/static/js/build-ad-bundle.sh (concat + terser).

layout.etlua

  • Add class="no-js" на <html> + inline script <head> swap до .js (для iOS Safari .js .vi-limiter video::-webkit-media-controls{display:none} selector).
  • Add <link rel="preconnect" href="https://a5.g--o.info" crossorigin>.
  • Add _adSel inline config у mysettings.location_css == "id" block з selector mapping (native classes — vi-limiter у 8161, vdo-blk-lmtr у 8148).
  • Tabunder pre-detect script з popunder spot ID regex (?:^|\|)<popunder_id>= (синхронізовано з ad-config!).
  • <script src="/static/js/ad-bundle.min.js?v=<md5>" defer></script> з cache-bust md5.
  • Inline lazyLoadFunc definitionviews/static/js/lazysizes.min.js content) ПІСЛЯ inline lazysizes core block — інакше timeline-pjs.min.js кине ReferenceError: lazyLoadFunc is not defined (8161 fix 2026-05-04).
  • Body inline scripts (port 8148 lines 184-434): _initPjs(), _revealPjs(), _playAt(), click handler на .<lmtr_class> capture phase, VAST overlay click forwarder.
  • Guard counter calls: tb.start_events_v2(() => { if(typeof c==="function") c(<%-video.video_id%>,0,"click",0); }, ...)c() defined у async-loaded counters.v2.min.js, без guard race-error (c is not defined).

id_index.etlua

  • Replace videojs4 DOM з PlayerJS structure: <div class="<lmtr_class> is-loading"><div class="<vdo_class> js-cleanVideo" id="pjs_container"> + <video> + <div id="pjs_poster"> + <img id="pjs_poster_img"> + <div id="pjs_play_btn"> SVG + <div id="pjs_loader"> SVG.
  • Use site-NATIVE classes (не copy 8148 vdo-blk-lmtr/vdo-blk-vdo). Map до існуючих сайту: 8161 → vi-limiter/vi-player. Foreign classes створюють footprint і CSS дублі.
  • Inline styles target NATIVE classes теж (<style>.vi-limiter ...</style>). Без дзеркал sed.
  • Timeline click handler: <a onClick="_playAt(_SECOND_)"> (НЕ legacy player.setTime).
  • Poster touch handler script: localStorage _pw mark на touchstart/mousedown для tabunder pre-detect.

CSS site-side (адаптивні правки)

  • Tooltip overlap: .<actions>{position:relative;z-index:5001} — підняти actions row над .asg-container{z-index:5000}. Plus .<msg-class>{z-index:10000} як defense.
  • Timeline thumb hover-play: .<thumbs-block> .<item>.<can-play> a:hover .<img-class>:after{display:block} + mobile @media(max-width:768px){.<thumbs> ... :after{transform:translate(-50%,-50%) scale(.6)}}.
  • Title wrap (.heading-2 truncation): .<title-head> .<heading-2>{white-space:normal;overflow-wrap:anywhere;...} — override будь-який single-line ellipsis.
  • .<thumbs-block> mobile scaling — adapt 8148 patterns до native classes site-у.

Verification

  • ad-test "<test_url>" → 6/6 PASS.
  • Empirical Playwright probe: capture pageerror array — має бути empty. Перевірити specifically c is not defined, lazyLoadFunc is not defined, c2.min.js Unexpected identifier 'o'.
  • Validate c2.min.js syntax: node --check views/static/js/c2.min.js — passing. Якщо source c2.js edited без re-minify → broken let declarations у comma-sequence; fix: terser views/static/js/c2.js -c -m -o views/static/js/c2.min.js.
  • Banner DOM uniqueness: await page.evaluate(() => Array.from(document.querySelectorAll('iframe[src*="api/spots"]'))) collect → no duplicate parent IDs (#thmb1 2× = bug).
  • VAST endpoint probe: curl 'https://a5.g--o.info/api/users/<vast_spot>?v2=1&fill=0&url=<page-url>' returns <Ad> (не empty <VAST/> — no-fill).
  • Popunder endpoint probe: curl 'https://a5.g--o.info/api/users/<popunder_spot>?host=<domain>&...' returns window.__NA.renderSpot({...}).
  • t1 CDN serves fresh bundle: curl -sL "https://t1.<domain>/static/js/ad-bundle.min.js" | grep -oE 'spot:"[0-9]+"' — bundle reflects новий config.
  • Real-browser smoke: click play → VAST shows (after popunder cooldown set), click like → tooltip opens above player chrome, hover related thumb → play icon, hover timeline scene → smaller play icon (mobile) / regular (desktop).

Post-deploy

  • Monitor adspyglass dashboard після prod deploy — graph має продовжувати ріст; падіння = регресія.
  • Update site row у SITES.md з generation tag (v3 PlayerJS).
  • Update memory project_player_roadmap.md — додати site до v3 list.

ASG SDK filename patterns (anti-footprint)

a5.g--o.info/<filename>.js server tolerates будь-який filename matching pattern per type — content однаковий. Use unique per site щоб diluteти footprint (adblock fingerprints).

Patterns (verified 2026-05-05, refined post-bug):

  • POPUNDER: 7 chars total, ^[A-Za-z0-9]{6}[13579]$ — last char must be ODD DIGIT only. Even digits return DIFFERENT SDK content (different ad type, will break renderSpot).
  • VAST: 7 chars total, ^[A-Za-z0-9]{6}[abcdABCD]$ — last char must be a-d (8 chars). e-h returns different VAST sub-type (incompatible).
  • Verify content size post-generation: popunder=250844b, vast=323450b. Different size → wrong SDK fetched.
  • BANNER tb_config.banner_source: локальний файл у views/static/js/<name>.js per site, не ASG endpoint — окремий footprint layer (rename файла + update reference)

Workflow: generate per-site filenames → verify via curl -sI → apply mass-replace у ad-config.js (popunder.sdk, vast.sdk) → rebuild bundle → update ?v=<md5> cache-bust у layout.

Detail: memory reference_ad_filename_patterns (15-site mapping applied 2026-05-05).

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> записи. Ключі що використовуються нашим 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 напряму:

curl 'https://a5.g--o.info/api/users/<vast_spot>?v2=1&fill=0&url=<page-url>'

Якщо response:

<?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, тощо:

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?