| em cache de uma
// versao antiga, servido durante um rollout.
window.PUBMASTER_TRACKING_VERSION =
(window.pubmasterTracking && pubmasterTracking.themeVersion) || 'desconhecida'
var pendingBlurEvent = null
var pendingBlurTimeout = null
var BLUR_DECISION_DELAY = 300 // ms to decide if user left or stayed
var pageUnloading = false
function debugLog(message, data) {
if (window.pubmasterTracking && window.pubmasterTracking.debug) {
console.log('[Pubmaster Tracking] ' + message, data || '')
}
}
/**
* Generate unique event ID
*
* @return {string} Unique event ID
*/
function generateEventID() {
var random
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
var arr = new Uint32Array(1)
crypto.getRandomValues(arr)
random = arr[0].toString(36)
} else {
random = Math.floor(Math.random() * 2147483647).toString(36)
}
return Date.now().toString(36) + '-' + random
}
/**
* Check if URL is same domain
*
* @param {string} url URL to check
* @return {boolean} True if same domain
*/
var _sameDomainAnchor = null
function isSameDomain(url) {
if (!url) return false
if (!_sameDomainAnchor) _sameDomainAnchor = document.createElement('a')
_sameDomainAnchor.href = url
return _sameDomainAnchor.hostname === window.location.hostname
}
/**
* Send event to Conversions API via AJAX
*
* @param {string} eventName Event name
* @param {string} eventID Unique event ID
* @param {string} eventType Event type
*/
function sendToConversionsAPI(eventName, eventID, eventType, useBeacon) {
eventType = eventType || 'viewcontent'
var data = new FormData()
data.append('action', 'Pubmaster_track_conversion')
data.append('eventID', eventID)
data.append('eventName', eventName)
data.append('eventType', eventType)
data.append('pageUrl', window.location.href)
// Use sendBeacon for page-exit events (survives page close)
if (useBeacon && typeof navigator.sendBeacon === 'function') {
var sent = navigator.sendBeacon(pubmasterTracking.ajaxUrl, data)
debugLog('CAPI via sendBeacon', { sent: sent, event: eventName })
return
}
var xhr = new XMLHttpRequest()
xhr.open('POST', pubmasterTracking.ajaxUrl, true)
xhr.timeout = 10000
xhr.onload = function () {
debugLog('Conversions API response', xhr.responseText)
}
xhr.onerror = function () {
debugLog('Conversions API error', xhr.statusText)
}
xhr.ontimeout = function () {
debugLog('Conversions API timeout', eventName)
}
xhr.send(data)
}
/**
* O TikTok esta configurado neste site?
*
* `ttPixelCodes` so existe no config quando ha par gravado (o PHP nem imprime a
* chave). Site sem TikTok: esta funcao devolve false e nenhum bloco tt* roda.
*
* @returns {boolean}
*/
function ttEnabled() {
return !!(
window.pubmasterTracking &&
pubmasterTracking.ttPixelCodes &&
pubmasterTracking.ttPixelCodes.length &&
typeof window.ttq !== 'undefined'
)
}
/**
* O toggle sGTM (Fase 170, SGTM-08/09) esta ligado neste site?
*
* `sgtmAdTracking` so existe no config quando o admin do tema liga o toggle
* (o PHP nem imprime a chave quando desligado). Site sem o toggle: esta
* funcao devolve false e nenhum bloco sGTM roda -- nem o listener GPT, nem
* o ad_unit_path extra no clicouAd.
*
* @returns {boolean}
*/
function sgtmEnabled() {
return !!(window.pubmasterTracking && pubmasterTracking.sgtmAdTracking === true)
}
/**
* Resolve o ad_unit_path do GPT a partir do elemento clicado (iframe do
* anuncio ou ancestral proximo).
*
* Duas estrategias, na ordem:
* 1) O id do iframe segue a convencao do proprio GPT:
* google_ads_iframe__[__container__]. Funciona mesmo
* sem `googletag` disponivel (regex sobre o id, nada de API).
* 2) O id de algum ancestral bate com o slotElementId() de um slot
* registrado no GPT -- cobre o caso de o elemento clicado ser um filho
* do container do slot, nao o iframe do proprio Google.
*
* Sem resolver por nenhuma das duas, devolve '' (nunca lanca) -- o toggle
* sGTM nao pode derrubar o clicouAd por causa disto.
*
* @param {Element} el Elemento clicado (iframe do anuncio ou ancestral).
* @returns {string}
*/
function resolveAdUnitPath(el) {
try {
var node = el
for (var d = 0; node && d < 6; d++) {
var id = node.id || ''
var m = /^google_ads_iframe_(.+?)_\d+(?:__container__)?$/.exec(id)
if (m) return m[1]
if (id && window.googletag && typeof googletag.pubads === 'function') {
var slots = googletag.pubads().getSlots ? googletag.pubads().getSlots() : []
for (var i = 0; i < slots.length; i++) {
if (slots[i].getSlotElementId() === id) return slots[i].getAdUnitPath()
}
}
node = node.parentElement
}
} catch (e) {}
return ''
}
/**
* Registra o listener de impressao visivel do GPT (`impressionViewable`),
* que dispara TAMBEM em refresh do slot -- por isso 1 push de `adImpression`
* por impressao visivel, nao 1 por carregamento de pagina.
*
* Via `googletag.cmd.push`, que funciona independente da ordem de carga: se
* `googletag` ainda nao existe, o comando fica na fila e roda assim que a
* lib carregar; se ja existe, `cmd` e um array pass-through que executa na
* hora (comportamento nativo do proprio GPT).
*
* Os outros dois eventos do ciclo de renderizacao do GPT (creative
* injetado / iframe carregou) foram descartados de proposito: nenhum dos
* dois prova que o slot ficou VISIVEL -- so `impressionViewable` prova
* (criterio IAB, >=50% por >=1s), que e a decisao travada da fase
* (170-CONTEXT.md).
*
* @returns {void}
*/
function setupSgtmAdTracking() {
if (!sgtmEnabled()) return
window.googletag = window.googletag || { cmd: [] }
googletag.cmd = googletag.cmd || []
googletag.cmd.push(function () {
try {
googletag.pubads().addEventListener('impressionViewable', function (event) {
var slot = event && event.slot
if (!slot) return
var elId = slot.getSlotElementId()
pushToDataLayer('adImpression', generateEventID(), {
adType: detectAdType(document.getElementById(elId)) || 'gpt',
ad_unit_path: slot.getAdUnitPath(),
slot_element_id: elId,
})
})
} catch (e) {
debugLog('sGTM: falha ao registrar listener GPT', e)
}
})
}
/**
* Dispara um evento no TikTok Pixel e, em paralelo, na Events API.
*
* Irmao de fbq(...) + sendToConversionsAPI(...), com o MESMO eventID — a chave de
* deduplicacao do TikTok e a trinca [event_source_id, event, event_id], e o espaco
* de dedup e separado do Facebook, entao compartilhar o ID nao causa colisao.
*
* Todo o corpo vive em try/catch: fireAdClickEvent tem seis pontos de chamada e
* varios rodam durante beforeunload/pagehide, onde o usuario nao ve console nenhum.
* Uma excecao aqui mataria o clicouAd do Facebook e, com ele, a monetizacao.
*
* @param {string} eventName Nome do evento (ja sanitizado no servidor quando vem do config)
* @param {string} eventID O MESMO ID usado no fbq e no dataLayer
* @param {object} props Propriedades do pixel (content_name, description)
* @param {boolean} useBeacon Usar sendBeacon (eventos de saida de pagina)
*/
function trackTikTok(eventName, eventID, props, useBeacon) {
if (!ttEnabled() || !eventName) {
return
}
props = props || {}
try {
// O event_id vai no TERCEIRO argumento, em snake_case.
window.ttq.track(eventName, props, { event_id: eventID })
} catch (e) {
debugLog('TikTok pixel error', e)
}
try {
sendToTikTokEventsAPI(eventName, eventID, props, useBeacon)
} catch (e) {
debugLog('TikTok Events API error', e)
}
}
/**
* Envia o evento ao canal server-side do TikTok (Events API) via admin-ajax.
*
* Action PROPRIA, separada da do Facebook: o caminho do Facebook nao muda, e um
* site so-TikTok nao depende de nada do Facebook estar presente.
*
* @param {string} eventName Nome do evento
* @param {string} eventID ID do evento (dedup com o pixel)
* @param {object} props content_name / description
* @param {boolean} useBeacon Usar sendBeacon
*/
function sendToTikTokEventsAPI(eventName, eventID, props, useBeacon) {
props = props || {}
var data = new FormData()
data.append('action', 'Pubmaster_track_tiktok_conversion')
data.append('eventID', eventID)
data.append('eventName', eventName)
data.append('pageUrl', window.location.href)
if (props.content_name) {
data.append('contentName', props.content_name)
}
if (props.description) {
data.append('description', props.description)
}
if (useBeacon && typeof navigator.sendBeacon === 'function') {
var sent = navigator.sendBeacon(pubmasterTracking.ajaxUrl, data)
debugLog('TikTok Events API via sendBeacon', { sent: sent, event: eventName })
return
}
var xhr = new XMLHttpRequest()
xhr.open('POST', pubmasterTracking.ajaxUrl, true)
xhr.timeout = 10000
xhr.onload = function () {
debugLog('TikTok Events API response', xhr.responseText)
}
xhr.onerror = function () {
debugLog('TikTok Events API error', xhr.statusText)
}
xhr.ontimeout = function () {
debugLog('TikTok Events API timeout', eventName)
}
xhr.send(data)
}
/**
* Push event to GTM dataLayer
*
* @param {string} eventName Event name
* @param {string} eventID Event ID
* @param {object} additionalData Additional event data
*/
function pushToDataLayer(eventName, eventID, additionalData) {
window.dataLayer = window.dataLayer || []
var eventData = {
event: eventName,
event_id: eventID,
adType: (additionalData && additionalData.adType) || 'unknown',
}
if (additionalData) {
var keys = Object.keys(additionalData)
for (var i = 0; i < keys.length; i++) {
eventData[keys[i]] = additionalData[keys[i]]
}
}
window.dataLayer.push(eventData)
debugLog('DataLayer push', eventData)
}
/**
* O site TEM pixel configurado?
*
* Diferente de "o fbevents.js carregou". O adblock derruba o script do
* Facebook e `fbq` some — mas o site segue tendo pixel, e e exatamente nesse
* cenario que a CAPI (server-side) existe para salvar o dado. Guardar o envio
* server-side por `typeof fbq` fazia a CAPI morrer junto com o browser-side,
* ou seja: falhava justamente quando era necessaria.
*
* `pixelIds` vem do PHP e ja era o discriminador usado em
* `initializeFacebookPixel()`.
*/
function temPixelConfigurado() {
return !!(
window.pubmasterTracking &&
pubmasterTracking.pixelIds &&
pubmasterTracking.pixelIds.length > 0
)
}
/**
* Initialize Facebook Pixel PageView
*
* fbq('init') is done in by class-facebook-tracking.php.
* This function only fires the PageView event with eventID for CAPI dedup.
*/
function initializeFacebookPixel() {
// Sem pixel configurado nao ha o que enviar por nenhum caminho.
if (!temPixelConfigurado()) {
debugLog('No pixel IDs configured')
return
}
debugLog('Facebook Pixel detected', pubmasterTracking.pixelIds)
if (pubmasterTracking.sendDefaultPageView) {
// The PageView pixel event is fired early in (class-facebook-tracking.php)
// so fast bounces are still counted. Reuse that eventID for CAPI dedup and do
// NOT re-fire the pixel event here. Fallback: mint one if the head ID is absent
// (e.g. an older cached served during a version rollout).
var headEventID = window.__pmPageViewEventID || pubmasterTracking.pageViewEventID
if (headEventID) {
sendToConversionsAPI('PageView', headEventID, 'pageview')
pushToDataLayer('page_view', headEventID)
debugLog('PageView CAPI sent (pixel already fired in head)', headEventID)
} else {
var eventID = generateEventID()
// O pixel so dispara se o fbevents.js estiver de pe; a CAPI vai de todo jeito.
if (typeof fbq !== 'undefined') {
fbq('track', 'PageView', {}, { eventID: eventID })
}
sendToConversionsAPI('PageView', eventID, 'pageview')
pushToDataLayer('page_view', eventID)
debugLog('PageView event sent (fallback)', eventID)
}
}
}
/**
* Check if element is a close/dismiss button
*
* @param {Element} el Element to check
* @return {boolean} True if it's a close button
*/
function isCloseButton(el) {
if (!el) return false
var node = el
for (var i = 0; i < 4 && node; i++) {
if (node.nodeType !== 1) break
var cls = (node.className || '').toString().toLowerCase()
var role = (node.getAttribute('role') || '').toLowerCase()
var label = (node.getAttribute('aria-label') || '').toLowerCase()
var tag = node.tagName.toLowerCase()
var dismiss = (node.getAttribute('data-dismiss') || '').toLowerCase()
if (
cls.indexOf('close') !== -1 ||
cls.indexOf('dismiss') !== -1 ||
cls.indexOf('ai-close') !== -1 ||
cls.indexOf('btn-close') !== -1 ||
cls.indexOf('btn-dismiss') !== -1 ||
cls.indexOf('ad-close') !== -1 ||
cls.indexOf('sticky-close') !== -1
) {
return true
}
if (
(role === 'button' && label.indexOf('close') !== -1) ||
(role === 'button' && label.indexOf('fechar') !== -1) ||
label.indexOf('close') !== -1 ||
label.indexOf('fechar') !== -1 ||
dismiss !== ''
) {
return true
}
if (tag === 'svg' || tag === 'path' || tag === 'line') {
var parent = node.closest('button, [role="button"], .close, .dismiss, .ai-close')
if (parent) return true
}
if ((tag === 'button' || tag === 'span' || tag === 'div') && !node.querySelector('a')) {
var text = (node.textContent || '').trim()
if (
text === '\u00d7' ||
text === '\u2715' ||
text === '\u2716' ||
text === 'X' ||
text === 'x'
) {
return true
}
}
node = node.parentElement
}
return false
}
/**
* Detect ad type from an element's closest ad container
*
* @param {Element} el Element inside an ad container
* @return {string|null} Ad type or null
*/
function detectAdType(el) {
if (!el) return null
// Ad Inserter code-block with number extraction (e.g., .code-block-3 → 'block-3')
var codeBlock = el.closest('[class*="code-block-"]')
if (codeBlock) {
var match = (codeBlock.className || '').match(/code-block-(\d+)/)
return match ? 'block-' + match[1] : 'block'
}
// Ad slot wrappers (e.g., #av_top_wrapper, #av_content_1_wrapper)
var adWrapper = el.closest('[id$="_wrapper"]')
if (adWrapper && adWrapper.querySelector('iframe')) {
var wrapperId = adWrapper.id.replace('_wrapper', '')
return 'slot-' + wrapperId
}
var adContainerMap = {
'.ai-interstitial, .ai-ad-interstitial': 'interstitial',
'.ai-ad-anchor': 'anchor',
'.ai-ad-contents, .ai-inline-ad': 'contents',
'.ai-ad-top, .ai-header-ad': 'top',
'.ai-offerwall, .ai-ad-offerwall': 'offerwall',
'.ad-slot': 'slot',
}
var selectors = Object.keys(adContainerMap)
for (var i = 0; i < selectors.length; i++) {
if (el.closest(selectors[i])) {
return adContainerMap[selectors[i]]
}
}
var container = el.closest('[class*="ai-ad-"], [class*="ai-view-"]')
if (container) return 'generic'
return null
}
/**
* Check if an iframe is an ad iframe by its attributes
*
* @param {Element} iframe The iframe element
* @return {boolean} True if it looks like an ad iframe
*/
function isAdIframe(iframe) {
var id = (iframe.id || '').toLowerCase()
var src = (iframe.src || '').toLowerCase()
return (
id.indexOf('google_ads') !== -1 ||
id.indexOf('aswift_') !== -1 ||
src.indexOf('doubleclick') !== -1 ||
src.indexOf('googlesyndication') !== -1 ||
src.indexOf('adservice.google') !== -1 ||
src.indexOf('tpc.googlesyndication') !== -1 ||
src.indexOf('safeframe') !== -1 ||
src.indexOf('amazon-adsystem') !== -1 ||
src.indexOf('media.net') !== -1 ||
src.indexOf('adnxs.com') !== -1
)
}
/**
* Classify overlay type from iframe context
*
* Phase A: CSS classes from Ad Inserter (high confidence)
* Phase B: Geometric detection for Google auto ads (parents only)
*
* IMPORTANT: Phase B checks ONLY parent elements, never the iframe itself.
* Google Ads inline iframes temporarily get position:fixed during expansion,
* which caused false positives in v2.18.x when checking the iframe directly.
*
* @param {Element} iframe The iframe element
* @return {string|null} 'interstitial' | 'anchor' | 'offerwall' | null
*/
function classifyOverlayType(iframe) {
// Phase A: Ad Inserter CSS classes (high confidence)
if (iframe.closest('.ai-interstitial, .ai-ad-interstitial')) return 'interstitial'
if (iframe.closest('.ai-ad-anchor')) return 'anchor'
if (iframe.closest('.ai-offerwall, .ai-ad-offerwall')) return 'offerwall'
// Phase B: Geometric detection for Google auto ads (parents only)
var vpW = window.innerWidth
var vpH = window.innerHeight
var parent = iframe.parentElement
for (var i = 0; i < 5 && parent && parent !== document.body; i++) {
var pStyle = window.getComputedStyle(parent)
if (pStyle.position === 'fixed') {
var pRect = parent.getBoundingClientRect()
// Interstitial: fixed + covers >50% of viewport in both dimensions
if (pRect.width > vpW * 0.5 && pRect.height > vpH * 0.5) return 'interstitial'
// Anchor: fixed + wide but short strip at top or bottom
if (pRect.width > vpW * 0.5 && pRect.height < vpH * 0.3) {
if (pRect.bottom >= vpH - 30 || pRect.top <= 30) return 'anchor'
}
}
parent = parent.parentElement
}
return null
}
/**
* Classify overlay type by checking the iframe element directly
*
* Unlike classifyOverlayType() which only checks parents, this function
* checks the iframe itself for position:fixed. This is ONLY safe to call
* when we know the user stayed on the page (visibilityState === 'visible'),
* which eliminates false positives from inline ads temporarily getting
* position:fixed during expansion.
*
* @param {Element} iframe The iframe element
* @return {string|null} 'interstitial' | 'anchor' | null
*/
function classifyOverlayDirect(iframe) {
if (!iframe || !iframe.parentElement) return null
try {
var style = window.getComputedStyle(iframe)
if (style.position !== 'fixed') return null
var rect = iframe.getBoundingClientRect()
var vpW = window.innerWidth
var vpH = window.innerHeight
if (rect.width > vpW * 0.5 && rect.height > vpH * 0.5) return 'interstitial'
if (rect.width > vpW * 0.5 && rect.height < vpH * 0.3) {
if (rect.bottom >= vpH - 30 || rect.top <= 30) return 'anchor'
}
} catch (e) {}
return null
}
/**
* Fire the clicouAd event to GTM, Facebook Pixel, and Conversions API
*
* @param {string} adType Type of ad clicked
* @param {boolean} [useBeacon] Survive page close (beforeunload/pagehide).
* @param {Element} [sourceEl] Elemento clicado (iframe do anuncio ou
* ancestral), usado SO para resolver ad_unit_path com o toggle sGTM
* ligado. Ausente onde nao ha elemento em escopo (ex.: teste manual) --
* nesses casos resolveAdUnitPath() nunca roda e ad_unit_path sai ''.
*/
function fireAdClickEvent(adType, useBeacon, sourceEl) {
var eventID = generateEventID()
var siteSlug = (window.pubmasterTracking && pubmasterTracking.siteSlug) || 'sem-nome'
// Google and Facebook need DIFFERENT event names, and each destination is
// independent — a site may run only Facebook, only Google, or both.
//
// Facebook: per-site name. The pixel/Business Manager is shared across sites,
// so the slug is what tells them apart in the Events Manager.
// Google: canonical `clicouAd`. GTM triggers match it by EQUALS, and the
// container is already one per domain — the slug rides as a parameter.
var fbEventName = 'clicouAd-' + siteSlug
debugLog('Ad click detected', { type: adType, eventID: eventID, site: siteSlug })
// --- Google (GTM → GA4 / Ads) — unconditional: never gated on Facebook ---
var clicouAdData = {
adType: adType,
siteSlug: siteSlug,
content_type: 'ad_' + adType,
page_path: window.location.pathname,
page_title: document.title,
}
// sGTM (Fase 170) -- SO acrescenta ad_unit_path com o toggle ligado. Com o
// toggle desligado, clicouAdData fica com as MESMAS chaves de sempre, na
// mesma ordem -- o clicouAd nao muda um byte.
if (sgtmEnabled()) {
clicouAdData.ad_unit_path = resolveAdUnitPath(sourceEl)
}
pushToDataLayer('clicouAd', eventID, clicouAdData)
// --- Facebook (Pixel + CAPI) — only when this site has a pixel ---
if (typeof fbq !== 'undefined') {
fbq(
'trackCustom',
fbEventName,
{
ad_type: adType,
content_type: 'ad_' + adType,
page_path: window.location.pathname,
},
{ eventID: eventID }
)
}
// CAPI must use the SAME event name + eventID for deduplication.
// FORA do `if (fbq)`: com adblock o pixel some e so a CAPI resta.
if (temPixelConfigurado()) {
sendToConversionsAPI(fbEventName, eventID, 'viewcontent', useBeacon)
}
// --- TikTok (Pixel + Events API) — irmao independente do bloco do Facebook ---
//
// Deliberadamente FORA do `if (typeof fbq !== 'undefined')` e por ULTIMO: aninhar
// dentro do guard do fbq faria um site so-TikTok nao rastrear nada, e vir depois
// garante que dataLayer e fbq ja rodaram antes de qualquer risco novo.
//
// O nome vem PRONTO do servidor (ja sanitizado para as regras do TikTok: 50
// caracteres, [A-Za-z0-9_-], comeca com letra). Nao remontar aqui.
//
// `content_type` NAO recebe 'ad_' + adType: no TikTok esse campo e um enum
// restrito a 'product'/'product_group', e um valor fora disso e invalido. O tipo
// do anuncio vai em `description`, que a doc define como campo livre.
trackTikTok(
pubmasterTracking.ttAdClickEventName,
eventID,
{
content_name: adType,
description: 'ad_' + adType,
},
useBeacon
)
debugLog('clicouAd event sent', {
type: adType,
google: 'clicouAd',
facebook: typeof fbq !== 'undefined' ? fbEventName : 'skipped (no pixel)',
tiktok: ttEnabled() ? pubmasterTracking.ttAdClickEventName : 'skipped (no pixel)',
eventID: eventID,
})
}
/**
* Fire Paginacao event for overlay interactions (user stayed on site)
*
* Dispatches to Facebook Pixel + GTM + CAPI, aligned with setupNavigationTracking().
*
* @param {string} overlayType Type of overlay ('interstitial' | 'anchor' | 'offerwall')
*/
function fireOverlayPaginacaoEvent(overlayType) {
var eventID = generateEventID()
debugLog('Overlay navigation event', { type: overlayType, eventID: eventID })
if (typeof fbq !== 'undefined') {
fbq(
'trackCustom',
'Paginacao',
{
content_name: overlayType + ' overlay',
destination_url: window.location.href,
page_path: window.location.pathname,
},
{ eventID: eventID }
)
}
if (temPixelConfigurado()) {
sendToConversionsAPI('Paginacao', eventID, 'viewcontent')
}
pushToDataLayer('internal_navigation', eventID, {
link_url: window.location.href,
link_text: overlayType + ' overlay',
overlay_type: overlayType,
})
// TikTok — 'Paginacao' e nome valido (9 caracteres, so letras, comeca com letra).
// Sem sufixo de slug: so o clicouAd leva slug, no Facebook e no TikTok.
// `destination_url` e `page_path` nao entram: nao sao parametros documentados de
// properties no TikTok, e a URL ja vai em page.url no payload server-side.
trackTikTok('Paginacao', eventID, {
content_name: overlayType + ' overlay',
description: overlayType,
})
debugLog('Paginacao event sent for overlay', { type: overlayType, eventID: eventID })
}
/**
* Setup ad click tracking using mouseenter/mouseleave + window.blur
*
* When a user clicks inside a cross-origin iframe, the parent window loses focus.
* We detect this via the 'blur' event. To identify WHICH iframe was clicked, we
* track mouseenter/mouseleave on all iframes (including dynamically injected ones
* via polling). This works even when the ad opens a new tab and activeElement
* reverts to document.body.
*
* Pattern based on DDMP theme's jQuery iframeTracker, reimplemented without jQuery.
*/
function setupAdClickTracking() {
var hoveredIframe = null
var hoverSource = null // 'interaction' | 'focusin' | null
var vignetteUntil = 0
var trackedIframes = []
var recentAdClicks = {}
var AD_CLICK_COOLDOWN = 1500
var POLL_INTERVAL = 1000
var pendingOverlayConfirmation = null // { pending, timeoutId }
var OVERLAY_CONFIRM_WINDOW = 2000
// Reclaim window focus from cross-origin iframes.
// After focus moves to an iframe, window.blur won't fire for subsequent
// iframe clicks. Moving focus to body ensures the next iframe click
// triggers blur correctly.
function reclaimWindowFocus() {
if (document.visibilityState === 'hidden') return
setTimeout(function () {
if (!document.activeElement || document.activeElement.tagName !== 'IFRAME') return
// Attempt 1: body.focus()
document.body.focus({ preventScroll: true })
// Fallback: if body.focus() failed (e.g. Safari, body not focusable),
// use a temporary span with tabIndex=-1 (programmatically focusable in all browsers)
if (document.activeElement && document.activeElement.tagName === 'IFRAME') {
try {
var el = document.createElement('span')
el.tabIndex = -1
el.setAttribute('aria-hidden', 'true')
el.style.cssText = 'position:fixed;opacity:0;pointer-events:none;width:0;height:0'
document.body.appendChild(el)
el.focus({ preventScroll: true })
el.parentNode.removeChild(el)
} catch (ignore) {}
}
debugLog('focus: reclaimed from iframe', {
success: !document.activeElement || document.activeElement.tagName !== 'IFRAME',
})
}, 50)
}
// Unique key per iframe (src without query params, or id, or position)
function getIframeKey(iframe) {
if (iframe.id) return 'id:' + iframe.id
var src = iframe.src || ''
if (src && src !== 'about:blank') {
return src.split('?')[0]
}
return 'pos:' + Array.prototype.indexOf.call(document.querySelectorAll('iframe'), iframe)
}
// Attach hover and touch listeners to an iframe
function attachHoverListeners(iframe) {
function onEnter() {
hoveredIframe = iframe
hoverSource = 'interaction'
}
function onLeave() {
if (hoveredIframe === iframe) {
hoveredIframe = null
hoverSource = null
}
}
iframe.addEventListener('mouseenter', onEnter)
iframe.addEventListener('mouseleave', onLeave)
iframe.addEventListener('pointerenter', onEnter)
iframe.addEventListener('pointerleave', onLeave)
// Mobile: touch events (passive to not block scroll)
iframe.addEventListener(
'touchstart',
function () {
// Proactive reclaim: if focus is stuck on a different iframe,
// move to body SYNCHRONOUSLY so window.blur fires for THIS iframe.
// reclaimWindowFocus() is async (50ms) and loses the race on mobile.
if (
document.activeElement &&
document.activeElement.tagName === 'IFRAME' &&
document.activeElement !== iframe
) {
document.body.focus({ preventScroll: true })
}
hoveredIframe = iframe
hoverSource = 'interaction'
},
{ passive: true }
)
iframe.addEventListener(
'touchend',
function () {
setTimeout(function () {
if (hoveredIframe === iframe) {
hoveredIframe = null
hoverSource = null
}
}, 500) // Delay for blur/visibilitychange to fire first
},
{ passive: true }
)
}
// Register new iframes and clean removed ones
function scanIframes() {
// Clean iframes removed from DOM
for (var i = trackedIframes.length - 1; i >= 0; i--) {
if (!document.contains(trackedIframes[i])) {
if (hoveredIframe === trackedIframes[i]) {
hoveredIframe = null
hoverSource = null
}
trackedIframes.splice(i, 1)
}
}
// Register new iframes
var iframes = document.querySelectorAll('iframe')
for (var j = 0; j < iframes.length; j++) {
if (trackedIframes.indexOf(iframes[j]) === -1) {
attachHoverListeners(iframes[j])
trackedIframes.push(iframes[j])
}
}
debugLog('scanIframes: ' + trackedIframes.length + ' iframes tracked')
}
// Initial scan for existing iframes
scanIframes()
// MutationObserver: detect iframes added/removed by Ad Inserter, GPT, etc.
if (typeof MutationObserver !== 'undefined') {
var observer = new MutationObserver(function (mutations) {
var needsScan = false
for (var i = 0; i < mutations.length; i++) {
var mutation = mutations[i]
// Check added nodes for iframes or containers that may hold iframes
for (var j = 0; j < mutation.addedNodes.length; j++) {
var node = mutation.addedNodes[j]
if (node.nodeType !== 1) continue
if (node.tagName === 'IFRAME' || (node.querySelector && node.querySelector('iframe'))) {
needsScan = true
break
}
}
// Check removed nodes to clean up tracked iframes
for (var k = 0; k < mutation.removedNodes.length; k++) {
var removed = mutation.removedNodes[k]
if (removed.nodeType !== 1) continue
if (
removed.tagName === 'IFRAME' ||
(removed.querySelector && removed.querySelector('iframe'))
) {
needsScan = true
break
}
}
if (needsScan) break
}
if (needsScan) scanIframes()
})
observer.observe(document.body, { childList: true, subtree: true })
} else {
// Fallback for old browsers: poll every 1s
setInterval(scanIframes, POLL_INTERVAL)
}
// === Deferred blur resolution ===
// Decides event type based on visibilityState after a short delay:
// - hidden → user LEFT → clicouAd
// - visible → user STAYED → Paginacao (if overlay) or nothing
function resolvePendingBlur() {
if (!pendingBlurEvent) return
var pending = pendingBlurEvent
pendingBlurEvent = null
pendingBlurTimeout = null
var key = pending.key
var now = Date.now()
if (document.visibilityState === 'hidden') {
// Page is closing/navigating — NOT an ad click
if (pageUnloading) {
debugLog('deferred blur: suppressed — page unloading, not ad click')
return
}
// User LEFT the site → clicouAd
if (pending.vignetteActive) {
debugLog('deferred blur: clicouAd suppressed during Google Vignette')
return
}
var adType =
pending.overlayType ||
detectAdType(pending.target) ||
(isAdIframe(pending.target) ? 'generic' : 'unknown')
recentAdClicks[key] = now
fireAdClickEvent(adType, false, pending.target)
setTimeout(function () {
delete recentAdClicks[key]
}, AD_CLICK_COOLDOWN + 100)
debugLog('deferred blur: user left, fired clicouAd', adType)
} else {
// User STAYED on site
if (pending.isInteraction) {
if (pending.overlayType) {
// OVERLAY DEFERRED CONFIRMATION:
// Don't decide now. Wait up to OVERLAY_CONFIRM_WINDOW for visibilitychange(hidden).
// If hidden fires → genuine ad click. If timeout → close button / refocus.
if (pendingOverlayConfirmation) {
clearTimeout(pendingOverlayConfirmation.timeoutId)
}
var timeoutId = setTimeout(resolveOverlayAsNotAdClick, OVERLAY_CONFIRM_WINDOW)
pendingOverlayConfirmation = { pending: pending, timeoutId: timeoutId }
debugLog('deferred blur: overlay interaction, deferring to confirmation window', {
overlayType: pending.overlayType,
windowMs: OVERLAY_CONFIRM_WINDOW,
})
return // Skip reclaimWindowFocus — resolve/confirm will call it
} else {
// INLINE AD (no overlayType): unchanged behavior
if (pending.vignetteActive) {
debugLog('deferred blur: clicouAd suppressed during Google Vignette (visible)')
reclaimWindowFocus()
return
}
var adType =
detectAdType(pending.target) || (isAdIframe(pending.target) ? 'generic' : 'unknown')
recentAdClicks[key] = now
fireAdClickEvent(adType, false, pending.target)
setTimeout(function () {
delete recentAdClicks[key]
}, AD_CLICK_COOLDOWN + 100)
debugLog('deferred blur: user stayed but interacted (inline), fired clicouAd', adType)
}
} else {
// Programmatic focus (overlay appeared, focusin) → Paginacao
var overlayType = pending.overlayType
if (!overlayType && pending.target && document.contains(pending.target)) {
overlayType = classifyOverlayDirect(pending.target)
}
if (overlayType) {
recentAdClicks[key] = now
fireOverlayPaginacaoEvent(overlayType)
setTimeout(function () {
delete recentAdClicks[key]
}, AD_CLICK_COOLDOWN + 100)
debugLog('deferred blur: overlay appeared (focusin), fired Paginacao', overlayType)
// Promote hoverSource: next blur from this overlay iframe is a real click
if (hoveredIframe === pending.target && hoverSource === 'focusin') {
hoverSource = 'interaction'
debugLog('deferred blur: promoted hoverSource to interaction for overlay')
}
} else {
debugLog('deferred blur: user stayed, no ad event', key)
}
}
}
// Reclaim focus for next blur detection
reclaimWindowFocus()
}
// Flush pending blur as clicouAd when page is unloading.
// Called from beforeunload/pagehide: if a pending blur exists, the ad click
// caused same-tab navigation (blur fired BEFORE unload). Fire clicouAd immediately.
function flushPendingAsAdClick(source) {
if (!pendingBlurEvent) return
if (pendingBlurTimeout) clearTimeout(pendingBlurTimeout)
var pending = pendingBlurEvent
pendingBlurEvent = null
pendingBlurTimeout = null
if (pending.vignetteActive) return
var adType =
pending.overlayType ||
detectAdType(pending.target) ||
(isAdIframe(pending.target) ? 'generic' : 'unknown')
recentAdClicks[pending.key] = Date.now()
fireAdClickEvent(adType, true, pending.target) // useBeacon: survives page close
debugLog(source + ': ad click caused same-tab navigation, fired clicouAd', adType)
}
// Overlay confirmation timeout: user stayed on site → NOT an ad click.
function resolveOverlayAsNotAdClick() {
if (!pendingOverlayConfirmation) return
var pending = pendingOverlayConfirmation.pending
pendingOverlayConfirmation = null
debugLog(
'overlay-confirm: timeout expired, user stayed, suppressing clicouAd',
pending.overlayType
)
reclaimWindowFocus()
}
// Overlay confirmation: visibilitychange(hidden) confirmed user left → ad click.
function confirmOverlayAsAdClick() {
if (!pendingOverlayConfirmation) return
var oc = pendingOverlayConfirmation
pendingOverlayConfirmation = null
clearTimeout(oc.timeoutId)
var pending = oc.pending
if (pending.vignetteActive || pageUnloading) {
debugLog('overlay-confirm: hidden confirmed but suppressed', {
vignette: !!pending.vignetteActive,
unloading: pageUnloading,
})
return
}
var adType =
pending.overlayType ||
detectAdType(pending.target) ||
(isAdIframe(pending.target) ? 'generic' : 'unknown')
recentAdClicks[pending.key] = Date.now()
fireAdClickEvent(adType, false, pending.target)
setTimeout(function () {
delete recentAdClicks[pending.key]
}, AD_CLICK_COOLDOWN + 100)
debugLog('overlay-confirm: hidden confirmed ad click, fired clicouAd', adType)
}
// === PART 1: Iframe click detection via deferred window.blur ===
window.addEventListener('blur', function () {
var target = hoveredIframe
var isInteraction = hoverSource === 'interaction'
if (!target) {
var active = document.activeElement
if (active && active.tagName === 'IFRAME') {
target = active
// Overlays steal focus programmatically (not interaction).
// Inline ads only receive focus via user click (interaction).
var fallbackOverlay = classifyOverlayType(active) || classifyOverlayDirect(active)
isInteraction = !fallbackOverlay
}
}
if (!target) {
debugLog('blur: no target', {
hoveredIframe: !!hoveredIframe,
activeElement: document.activeElement ? document.activeElement.tagName : 'none',
})
return
}
if (window._adCloseClicked) {
debugLog('blur: close button flag active')
return
}
var key = getIframeKey(target)
var now = Date.now()
if (recentAdClicks[key] && now - recentAdClicks[key] < AD_CLICK_COOLDOWN) {
debugLog('blur: cooldown active for', key)
return
}
// Cancel previous pending
if (pendingBlurTimeout) {
clearTimeout(pendingBlurTimeout)
pendingBlurEvent = null
}
// Defer decision until we know if user left or stayed
pendingBlurEvent = {
target: target,
key: key,
isInteraction: isInteraction,
time: now,
overlayType: classifyOverlayType(target),
vignetteActive: !!(vignetteUntil && now < vignetteUntil),
}
pendingBlurTimeout = setTimeout(resolvePendingBlur, BLUR_DECISION_DELAY)
debugLog('blur: deferred', { key: key, overlayType: pendingBlurEvent.overlayType })
})
// Reset hoveredIframe on window focus ONLY if iframe was removed from DOM
// (e.g., overlay dismissed while tab was in background)
// If iframe still exists, keep reference — mouseenter/mouseleave handle state naturally
window.addEventListener('focus', function () {
if (hoveredIframe && !document.contains(hoveredIframe)) {
debugLog('focus: iframe removed from DOM, resetting')
hoveredIframe = null
hoverSource = null
}
reclaimWindowFocus()
})
// Guard: detect page close/navigate (NOT new tab)
// beforeunload fires when page is leaving; does NOT fire when ad opens new tab.
// If a pending blur exists, the ad click CAUSED this navigation (same-tab) — fire clicouAd.
// If no pending blur, user is closing the tab — set flag to suppress later events.
window.addEventListener('beforeunload', function () {
if (pendingOverlayConfirmation) {
clearTimeout(pendingOverlayConfirmation.timeoutId)
debugLog(
'beforeunload: overlay confirmation cancelled (same-site navigation)',
pendingOverlayConfirmation.pending.overlayType
)
pendingOverlayConfirmation = null
}
flushPendingAsAdClick('beforeunload')
pageUnloading = true
})
// pagehide is more reliable on iOS Safari where beforeunload is spotty
window.addEventListener('pagehide', function () {
if (pendingOverlayConfirmation) {
clearTimeout(pendingOverlayConfirmation.timeoutId)
debugLog(
'pagehide: overlay confirmation cancelled (same-site navigation)',
pendingOverlayConfirmation.pending.overlayType
)
pendingOverlayConfirmation = null
}
flushPendingAsAdClick('pagehide')
pageUnloading = true
})
// Google Vignette detection: suppress blur when #google_vignette hash appears
window.addEventListener('hashchange', function () {
if (window.location.hash.indexOf('google_vignette') !== -1) {
vignetteUntil = Date.now() + 2000
debugLog('Google Vignette detected, suppressing blur for 2s')
}
})
// focusin handler: sets hoveredIframe when iframe receives focus.
// Key fix (v2.20.3): when hoveredIframe is already set and a NEW overlay iframe
// receives focus, the overlay MUST take precedence. Without this, hoveredIframe
// points at the wrong iframe and clicouAd fires for the inline ad instead of
// recognizing the overlay appearance.
// Also creates a synthetic pending when focus moves iframe→iframe, because
// window.blur does NOT fire in that case (window already lost focus).
document.addEventListener('focusin', function (e) {
if (!e.target || e.target.tagName !== 'IFRAME') return
if (!hoveredIframe) {
// First iframe to receive focus (default case)
hoveredIframe = e.target
hoverSource = 'focusin'
} else if (hoveredIframe !== e.target) {
// Different iframe received focus while another was tracked
var newOverlay = classifyOverlayType(e.target) || classifyOverlayDirect(e.target)
if (newOverlay) {
debugLog(
'focusin: overlay ' + newOverlay + ' takes precedence over current hoveredIframe'
)
// Cancel overlay confirmation from previous overlay
if (pendingOverlayConfirmation) {
clearTimeout(pendingOverlayConfirmation.timeoutId)
debugLog('focusin: cancelling overlay confirmation (new overlay appeared)')
pendingOverlayConfirmation = null
}
// Discard any pending from the previous iframe.
// The overlay appearance caused the blur, NOT a user click.
// Even if isInteraction was true (mouse was hovering), the blur
// was triggered by the overlay stealing focus, not by user action.
if (pendingBlurEvent && pendingBlurEvent.target !== e.target) {
if (pendingBlurTimeout) clearTimeout(pendingBlurTimeout)
debugLog('focusin: discarding pending for previous iframe (overlay caused blur)')
pendingBlurEvent = null
pendingBlurTimeout = null
}
hoveredIframe = e.target
hoverSource = 'focusin'
// Create synthetic pending: window.blur does NOT fire when focus
// moves iframe→iframe. Without this pending, the overlay goes unnoticed.
var key = getIframeKey(e.target)
var now = Date.now()
if (!(recentAdClicks[key] && now - recentAdClicks[key] < AD_CLICK_COOLDOWN)) {
pendingBlurEvent = {
target: e.target,
key: key,
isInteraction: false,
time: now,
overlayType: newOverlay,
vignetteActive: !!(vignetteUntil && now < vignetteUntil),
}
pendingBlurTimeout = setTimeout(resolvePendingBlur, BLUR_DECISION_DELAY)
debugLog('focusin: synthetic pending for overlay', {
key: key,
overlayType: newOverlay,
})
}
}
}
debugLog('focusin on iframe', {
id: e.target.id || '',
src: (e.target.src || '').substring(0, 60),
hoverSource: hoverSource,
})
})
// visibilitychange: (a) accelerate pending blur, (b) iOS fallback, (c) reclaim focus
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'hidden') {
// Overlay Deferred Confirmation: hidden confirms ad click
if (pendingOverlayConfirmation) {
confirmOverlayAsAdClick()
}
// Accelerate pending blur: user left the site
if (pendingBlurEvent) {
if (pendingBlurTimeout) clearTimeout(pendingBlurTimeout)
resolvePendingBlur()
return
}
// Fallback iOS Safari: blur didn't fire but tab hid
if (pageUnloading) {
debugLog('visibilitychange: suppressed — page unloading')
return
}
if (!hoveredIframe) return
var target = hoveredIframe
var key = getIframeKey(target)
var now = Date.now()
if (recentAdClicks[key] && now - recentAdClicks[key] < AD_CLICK_COOLDOWN) return
if (vignetteUntil && now < vignetteUntil) {
debugLog('visibilitychange: suppressed during Google Vignette')
return
}
var adType =
classifyOverlayType(target) ||
detectAdType(target) ||
(isAdIframe(target) ? 'generic' : 'unknown')
recentAdClicks[key] = now
fireAdClickEvent(adType, false, target)
setTimeout(function () {
delete recentAdClicks[key]
}, AD_CLICK_COOLDOWN + 100)
debugLog('visibilitychange: user left (iOS fallback), fired clicouAd', adType)
} else if (document.visibilityState === 'visible') {
reclaimWindowFocus()
}
})
// Manual test function for debugging
window.pubmasterTestAdClick = function (adType) {
fireAdClickEvent(adType || 'manual-test')
}
debugLog('Manual test: window.pubmasterTestAdClick("block-1")')
debugLog('Iframe ad click tracking active (MutationObserver + blur)')
// === PART 2: Native click delegation for non-iframe ad elements ===
var adContainerSelector = [
'.ai-ad-contents',
'.ai-inline-ad',
'.ai-ad-top',
'.ai-header-ad',
'.ai-ad-anchor',
'.ai-interstitial',
'.ai-ad-interstitial',
'.ai-offerwall',
'.ai-ad-offerwall',
'.ad-slot',
'.code-block',
'[class*="code-block-"]',
'[class*="ai-ad-"]',
'[class*="ai-view-"]',
'[id$="_wrapper"]',
].join(', ')
document.addEventListener(
'click',
function (e) {
var target = e.target
if (isCloseButton(target)) return
if (target.tagName === 'IFRAME') return
var adContainer = target.closest(adContainerSelector)
if (!adContainer) return
// Overlay containers: only clickable elements (links, buttons) fire clicouAd
// Backdrop clicks and close buttons are filtered out
var overlayContainer = target.closest(
'.ai-interstitial, .ai-ad-interstitial, .ai-ad-anchor, .ai-offerwall, .ai-ad-offerwall'
)
if (overlayContainer) {
var overlayClickable = target.closest('a, button, [role="button"], [onclick]')
if (overlayClickable && !isCloseButton(overlayClickable)) {
fireAdClickEvent(detectAdType(target) || 'overlay', false, target)
}
return
}
var clickable = target.closest('a, button, [role="button"], [onclick]')
if (!clickable) return
if (clickable.tagName === 'BUTTON' && !clickable.getAttribute('href')) return
if (isCloseButton(clickable)) return
var adType = detectAdType(target) || 'native'
fireAdClickEvent(adType, false, target)
},
true
)
debugLog('Native ad click tracking active (click delegation)')
}
/**
* Setup CTA click tracking
*/
function setupCtaTracking() {
document.addEventListener('click', function (e) {
var cta = e.target.closest('.cta-button')
if (!cta) return
var eventID = generateEventID()
var ctaText = cta.textContent.trim()
var ctaUrl = cta.href
debugLog('CTA click detected', { text: ctaText, url: ctaUrl, eventID: eventID })
pushToDataLayer('ctaClick', eventID, {
adType: 'cta',
content_type: 'cta',
cta_text: ctaText,
cta_url: ctaUrl,
page_url: window.location.href,
})
if (pubmasterTracking.sendDefaultViewContent) {
if (typeof fbq !== 'undefined') {
fbq('track', 'ViewContent', { content_name: ctaText }, { eventID: eventID })
}
if (temPixelConfigurado()) {
sendToConversionsAPI('ViewContent', eventID, 'viewcontent')
}
}
// TikTok — gate PROPRIO (`ttSendViewContent`). Nao reusar
// `sendDefaultViewContent`, que e a flag do Facebook e vem false em site sem
// pixel do Facebook: um site so-TikTok nunca emitiria ViewContent.
// 'ViewContent' e evento PADRAO do TikTok — paridade 1:1 de nome.
if (pubmasterTracking.ttSendViewContent) {
trackTikTok('ViewContent', eventID, {
content_name: ctaText,
description: 'cta',
})
}
})
debugLog('CTA tracking enabled')
}
/**
* Setup navigation tracking (internal links)
*/
function setupNavigationTracking() {
document.addEventListener('click', function (e) {
var link = e.target.closest('a')
if (!link || !isSameDomain(link.href)) return
if (
link.href.indexOf('#') !== -1 &&
link.href.split('#')[0] === window.location.href.split('#')[0]
) {
return
}
// Skip links inside ad containers (handled by ad click tracking)
if (
link.closest(
'[class*="code-block-"], .ai-ad-contents, .ai-inline-ad, .ai-ad-top, .ai-header-ad, .ai-interstitial, .ai-ad-interstitial, .ai-ad-anchor, .ai-offerwall, .ai-ad-offerwall, .ad-slot, [id$="_wrapper"]'
)
)
return
// Skip CTA buttons (handled by CTA tracking)
if (link.closest('.cta-button')) return
var eventID = generateEventID()
if (typeof fbq !== 'undefined') {
fbq(
'trackCustom',
'Paginacao',
{
content_name: link.textContent || link.href,
destination_url: link.href,
page_path: window.location.pathname,
},
{ eventID: eventID }
)
}
if (temPixelConfigurado()) {
sendToConversionsAPI('Paginacao', eventID, 'viewcontent')
}
pushToDataLayer('internal_navigation', eventID, {
link_url: link.href,
link_text: link.textContent,
})
// TikTok — SEGUNDO emissor de 'Paginacao' (o outro e o de overlay, em
// fireOverlayPaginacaoEvent). Sao dois caminhos distintos para o mesmo nome de
// evento; implementar so um entrega paridade parcial sem que nada reclame.
trackTikTok('Paginacao', eventID, {
content_name: link.textContent || link.href,
description: 'internal-nav',
})
debugLog('Internal navigation tracked', {
eventID: eventID,
url: link.href,
text: link.textContent,
})
})
debugLog('Navigation tracking enabled')
}
/**
* Setup custom events
*/
function setupCustomEvents() {
var fbList = pubmasterTracking.customEvents || []
var ttList = pubmasterTracking.ttCustomEvents || []
// SĂł desiste quando NENHUMA das duas listas tem nada. Antes o guard olhava sĂł a
// lista do Facebook: num site que usasse apenas TikTok, a funcao global nem era
// definida e os eventos customizados dele nunca disparavam.
if (fbList.length === 0 && ttList.length === 0) {
return
}
debugLog('Custom events configured', { facebook: fbList, tiktok: ttList })
function findByName(list, name) {
for (var i = 0; i < list.length; i++) {
if (list[i].name === name) {
return list[i]
}
}
return null
}
window.pubmasterTrackCustomEvent = function (eventName) {
// As listas sao SEPARADAS: os dois espacos de nome tem regras diferentes (o
// TikTok limita charset e tamanho, o Facebook nao), entao um nome pode existir
// num e nao no outro. Procurar nos dois, e so desistir se nenhum conhecer.
var fbEvent = findByName(fbList, eventName)
var ttEvent = findByName(ttList, eventName)
if (!fbEvent && !ttEvent) {
debugLog('Custom event not found', eventName)
return
}
// Um eventID para os dois destinos — cada plataforma deduplica no proprio espaco.
var eventID = generateEventID()
if (fbEvent) {
if (typeof fbq !== 'undefined') {
fbq('track', fbEvent.name, {}, { eventID: eventID })
}
// Fora do guard do fbq, exatamente como era antes desta mudanca.
sendToConversionsAPI(fbEvent.name, eventID, fbEvent.type)
}
if (ttEvent) {
// Payload vazio, igual ao Facebook. O `type` da lista nao vai ao TikTok: nao
// ha campo equivalente no payload, e o nome do evento ja diz o que e.
trackTikTok(ttEvent.name, eventID, {})
}
pushToDataLayer('custom_event', eventID, {
event_name: eventName,
})
debugLog('Custom event tracked', {
name: eventName,
facebook: fbEvent ? fbEvent.type : 'not configured',
tiktok: ttEvent ? 'sent' : 'not configured',
eventID: eventID,
})
}
}
/**
* Initialize tracking system
*/
window.pubmasterDiagnostics = function () {
var results = {}
results.trackingVersion = window.PUBMASTER_TRACKING_VERSION || 'NOT DEFINED'
results.configPresent = !!window.pubmasterTracking
results.debug = window.pubmasterTracking ? window.pubmasterTracking.debug : false
results.dataLayerPresent = Array.isArray(window.dataLayer)
results.dataLayerLength = (window.dataLayer || []).length
results.gtmPresent = typeof google_tag_manager !== 'undefined'
var iframes = document.querySelectorAll('iframe')
results.totalIframes = iframes.length
var adIframes = 0
for (var i = 0; i < iframes.length; i++) {
var src = (iframes[i].src || '').toLowerCase()
if (
src.indexOf('doubleclick') !== -1 ||
src.indexOf('googlesyndication') !== -1 ||
src.indexOf('google_ads') !== -1
) {
adIframes++
}
}
results.adIframes = adIframes
results.codeBlocks = document.querySelectorAll('[class*="code-block-"]').length
results.aiAdContents = document.querySelectorAll('.ai-ad-contents').length
results.wrappers = document.querySelectorAll('[id$="_wrapper"]').length
results.interstitials = document.querySelectorAll(
'.ai-interstitial, .ai-ad-interstitial'
).length
results.anchors = document.querySelectorAll('.ai-ad-anchor').length
results.inlineScriptPresent = !!document.querySelector('#pubmaster-tracking-inline')
var testId = 'diag-' + Date.now()
window.dataLayer = window.dataLayer || []
window.dataLayer.push({ event: 'pubmaster_diagnostic', event_id: testId })
results.testEventPushed = testId
console.log('=== Pubmaster Tracking Diagnostics ===')
console.table(results)
if (!results.configPresent) {
console.error(
'[Pubmaster] pubmasterTracking NOT found — theme may be outdated or script did not load'
)
}
if (!results.gtmPresent) {
console.error('[Pubmaster] GTM NOT found — check if GTM snippet is on the page')
}
if (results.adIframes === 0) {
console.warn('[Pubmaster] No ad iframes found — ads may not have loaded yet')
}
if (results.codeBlocks === 0) {
console.warn('[Pubmaster] No code-blocks found — Ad Inserter may not be active')
}
if (results.configPresent && results.gtmPresent && results.adIframes > 0) {
console.log('[Pubmaster] All OK — events should be working')
}
// === Web Push Diagnostics ===
var push = {}
// 1. Browser support
push.notificationAPISupported = 'Notification' in window
push.serviceWorkerSupported = 'serviceWorker' in navigator
push.pushManagerSupported = 'PushManager' in window
// 2. Current permission state
push.notificationPermission = push.notificationAPISupported
? Notification.permission
: 'NOT_SUPPORTED'
// 3. Active service workers
if (push.serviceWorkerSupported) {
navigator.serviceWorker
.getRegistrations()
.then(function (regs) {
push.serviceWorkersCount = regs.length
push.serviceWorkerScopes = regs.map(function (r) {
return r.scope
})
console.log('[Pubmaster] Push SW registrations:', regs.length, push.serviceWorkerScopes)
})
.catch(function () {
push.serviceWorkersCount = 'ERROR'
})
}
// 4. Detect common push plugin elements in DOM
var pushSelectors = [
'[id*="onesignal"]',
'[id*="pushengage"]',
'[id*="webpushr"]',
'[id*="pushcrew"]',
'[id*="push"]',
'[class*="onesignal"]',
'[class*="pushengage"]',
'[class*="webpushr"]',
'[class*="web-push"]',
'[class*="push-prompt"]',
'[class*="push-notification"]',
]
push.pluginElementsFound = []
for (var p = 0; p < pushSelectors.length; p++) {
var els = document.querySelectorAll(pushSelectors[p])
if (els.length > 0) {
push.pluginElementsFound.push(pushSelectors[p] + ' (' + els.length + ')')
}
}
// 5. Check if push popup might be hidden behind high z-index elements
push.highZIndexElements = []
var allFixed = document.querySelectorAll('*')
for (var f = 0; f < allFixed.length; f++) {
var cs = window.getComputedStyle(allFixed[f])
if (cs.position === 'fixed' && parseInt(cs.zIndex) > 9999) {
push.highZIndexElements.push({
tag: allFixed[f].tagName,
id: allFixed[f].id || '',
class: (allFixed[f].className || '').toString().substring(0, 60),
zIndex: cs.zIndex,
display: cs.display,
})
}
}
// 6. Check HTTPS (required for push)
push.isHTTPS = location.protocol === 'https:'
console.log('\n=== Web Push Diagnostics ===')
console.table(push)
if (!push.isHTTPS) {
console.error('[Pubmaster] HTTPS required for Web Push — site is using HTTP')
}
if (push.notificationPermission === 'denied') {
console.error(
'[Pubmaster] Notification permission DENIED — user must manually allow in browser settings'
)
}
if (push.notificationPermission === 'default') {
console.log('[Pubmaster] Notification permission not yet asked — popup should appear')
}
if (push.pluginElementsFound.length === 0) {
console.warn(
'[Pubmaster] No push plugin DOM elements found — plugin script may not have loaded'
)
}
if (push.highZIndexElements.length > 0) {
console.warn(
'[Pubmaster] Fixed elements with z-index > 9999 detected — may cover push popup:',
push.highZIndexElements
)
}
results.webPush = push
return results
}
function init() {
if (!window.pubmasterTracking) {
console.error(
'[Pubmaster Tracking] FATAL: window.pubmasterTracking not defined. Check if theme is active and updated.'
)
return
}
debugLog('Tracking system initializing', pubmasterTracking)
initializeFacebookPixel()
setupAdClickTracking()
setupSgtmAdTracking()
setupCtaTracking()
setupNavigationTracking()
setupCustomEvents()
debugLog('Tracking system initialized')
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init)
} else {
init()
}
// Exposto SO em ambiente de teste (jsdom) -- nunca em producao. Sem isso o
// teste nao alcanca resolveAdUnitPath()/fireAdClickEvent(), que vivem
// fechadas no escopo da IIFE.
if (window.__PUBMASTER_TEST__) {
window.__pubmasterSgtm = {
resolveAdUnitPath: resolveAdUnitPath,
fireAdClickEvent: fireAdClickEvent,
}
}
})()
|