DEPLOY: document custom CDN stale-cache gotcha + recovery Findings from 8148 production debug session 2026-05-02: 1. VAST gotcha — _decide() mode логіка: mode = popActive ? (vastActive || skipPattern ? "none" : "vast") : "pop" Тобто VAST loadable ТІЛЬКИ після того як popunder уже спрацював і _popRr cooldown set. На свіжій сесії: mode завжди "pop" → VAST SDK never loaded → користувач думає "VAST зламаний". Force test через ?clearAds=1 → pop fires → refresh → mode=vast. 2. CDN stale-cache — custom CDN кешує `?v=<hex-like>` URLs з s-maxage=31536000 (1 рік). Race у deploy: layout.etlua з новим ?v= може hit CDN раніше ніж static file → CDN кешує OLD bundle під NEW ?v= ключем → застрягає назавжди. Workaround: bump source md5, rebuild, новий ?v= ключ. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 KiB
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:
ad-config.js— config / placement IDs / mirror domainsad-core.js— ядро ad-orchestrationad-mute.js— mute / autoplay controlvast-preroll.js— pre-roll реклама перед videoad-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=1dom_target—#selectorкуди append iframetimeouts—{event_min_timeout, event_max_timeout}msattrs—{height, width, ...}iframe attributes / stylecallback— викликається після 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:
- На стороні ASG користувач прописує код банера (TrafficStars або ExoClick) + кастом-CSS для responsive grid + кастом-JS який postMessage-ить розмір.
- На стороні сервера ASG обертає у iframe (через
/api/spots/<id>endpoint). - Frontend ловить
window.messageevent від iframe → парсить JSON{url, height}→ встановлюєheightattribute на 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 `(?:^ |
| 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
- Користувач клацає play на
pjs_play_btn(PlayerJS player) asgInVideoImpressionevent від ASG SDK →_decideAndApplyловить → запускаєVastPreroll.show()- ASG іnject-ить
.asg-containerз video preroll MutationObserverловить container → pin pointer + z-index 5000, добавляє progress bar (.vast-progressyellow #ffd000)- Coordinated mute: video у container синхронізується з
_vastMutedlocalStorage (per-tab persist) - Після VAST done (video.ended OR
currentTime >= duration - 0.2) → callbackwindow._asgAdDone()→ main video starts viawindow._revealPjs() 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)
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
- Як здійснюється mirror swap — sed-replace у всіх template/JS файлах через Адміна, runtime config, чи інший механізм?
- Які pattern використовується для НЕ-77 сайтів (17 без adspy banner modules) — special-case structure, "no ads" сайти, чи щось інше?
- SDK URLs (
nDNVal3.js,9iO21Eb.js) — чи стабільні чи можуть змінитися при mirror rotation?