1/** 2 * Aggregate-only frontend analytics for Ad Unit performance reports. 3 */ 4(function() { 5 'use strict'; 6 7 var config = window.R89PBAnalytics || {}; 8 var sent = {}; 9 var sentKeys = []; 10 // Sized for long infinite-scroll sessions: every appended article mints 11 // fresh per-render dedup keys, and evicting a key for a slot still in the 12 // DOM would let a later click re-send its creative impression. 13 var MAX_SENT_KEYS = 2000; 14 var FETCH_TIMEOUT_MS = 10000; 15 var timers = {}; 16 var loadWaits = {}; 17 18 function debugEnabled() { 19 // Gate on the localized WP_DEBUG flag only. The r89pb-debug body 20 // class cannot serve as a gate here: isEnabled() suppresses all 21 // tracking (and therefore every debugWarn call site) when that class 22 // is present. 23 return !!config.debug; 24 } 25 26 function debugWarn(message) { 27 if (debugEnabled() && window.console && typeof window.console.warn === 'function') { 28 window.console.warn('[R89PB Analytics] ' + message); 29 } 30 } 31 32 function markSent(key) { 33 if (sent[key]) { 34 return; 35 } 36 37 sent[key] = true; 38 sentKeys.push(key); 39 40 while (sentKeys.length > MAX_SENT_KEYS) { 41 delete sent[sentKeys.shift()]; 42 } 43 } 44 45 function isEnabled() { 46 if (!config.enabled || !config.endpoint) { 47 return false; 48 } 49 50 if (!document.body) { 51 return false; 52 } 53 54 return !document.body.classList.contains('r89pb-debug') &&
55 !document.body.classList.contains('r89pb-visual-builder-active'); 56 } 57 58 function eventKey(eventName, slot) { 59 // The per-render page token is part of the dedup key: infinite-scroll 60 // themes append later articles whose slots reuse the same 61 // unit/placement/instance values but carry a fresh server-signed 62 // token. Without it, appended slots would be deduped against the 63 // initial page and never counted. 64 return [ 65 eventName, 66 slot.getAttribute('data-r89pb-unit') || '', 67 slot.getAttribute('data-r89pb-slot') || '', 68 slot.getAttribute('data-r89pb-instance') || '0', 69 slot.getAttribute('data-r89pb-page-token') || '' 70 ].join('|'); 71 } 72 73 function viewportDevice() { 74 if (window.matchMedia && window.matchMedia('(min-width: 992px)').matches) { 75 return 'desktop'; 76 } 77 78 return 'mobile'; 79 } 80 81 function placementDevice(slot) { 82 return slot.getAttribute('data-r89pb-placement-device') || 'both'; 83 } 84 85 function slotMatchesViewport(slot) { 86 var configured = placementDevice(slot); 87 var viewport = viewportDevice(); 88 89 return configured === 'both' || configured === viewport; 90 } 91 92 function slotIsCssVisible(slot) { 93 if (!window.getComputedStyle) { 94 return true; 95 } 96 97 var style = window.getComputedStyle(slot); 98 return style.display !== 'none' && style.visibility !== 'hidden'; 99 } 100 101 function slotCanTrack(slot) { 102 return slotMatchesViewport(slot) && slotIsCssVisible(slot); 103 } 104 105 function payloadFor(eventName, slot) { 106 return { 107 event: eventName, 108 unit_id: slot.getAttribute('data-r89pb-unit') || '', 109 placement_id: slot.getAttribute('data-r89pb-slot') || '', 110 instance: slot.getAttribute('data-r89pb-instance') || '0', 111 signature: slot.getAttribute('data-r89pb-track-signature') || '', 112 page_token: slot.getAttribute('data-r89pb-page-token') || '', 113 placement_device: placementDevice(slot), 114 viewport_device: viewportDevice() 115 }; 116 } 117 118 function sendEvent(eventName, slot) { 119 if (!isEnabled() || !slot) { 120 return false; 121 } 122 123 var key = eventKey(eventName, slot); 124 if (sent[key]) { 125 return true; 126 } 127 128 var payload = payloadFor(eventName, slot); 129 if (!payload.unit_id || !payload.placement_id || !payload.signature || !payload.page_token) { 130 return false; 131 } 132 133 markSent(key); 134 var body = JSON.stringify(payload); 135 136 if (window.navigator && typeof window.navigator.sendBeacon === 'function') { 137 var blob = new Blob([body], { type: 'application/json' }); 138 if (window.navigator.sendBeacon(config.endpoint, blob)) { 139 return true; 140 } 141 142 debugWarn('sendBeacon rejected the "' + eventName + '" event; falling back to fetch.'); 143 } 144 145 postWithFetch(eventName, body); 146 147 return true; 148 } 149 150 function postWithFetch(eventName, body) { 151 if (typeof window.fetch !== 'function') { 152 debugWarn('Dropped "' + eventName + '" event: fetch is unavailable.'); 153 return; 154 } 155 156 var options = { 157 method: 'POST', 158 credentials: 'same-origin', 159 headers: { 160 'Content-Type': 'application/json' 161 }, 162 body: body, 163 keepalive: true 164 }; 165 166 var timeoutId = null; 167 if (typeof window.AbortController === 'function') { 168 var controller = new AbortController(); 169 options.signal = controller.signal; 170 timeoutId = window.setTimeout(function() { 171 controller.abort(); 172 }, FETCH_TIMEOUT_MS); 173 } 174 175 function clearFetchTimeout() { 176 if (timeoutId !== null) { 177 window.clearTimeout(timeoutId); 178 timeoutId = null; 179 } 180 } 181 182 try { 183 window.fetch(config.endpoint, options).then(function(response) { 184 clearFetchTimeout(); 185 if (!response.ok) { 186 debugWarn('Tracking request for "' + eventName + '" failed with status ' + response.status + '.'); 187 } 188 }).catch(function(error) { 189 clearFetchTimeout(); 190 debugWarn('Tracking request for "' + eventName + '" failed: ' + (error && error.message ? error.message : 'network error') + '.'); 191 }); 192 } catch (error) { 193 clearFetchTimeout(); 194 debugWarn('Tracking request for "' + eventName + '" could not start: ' + (error && error.message ? error.message : 'unknown error') + '.'); 195 } 196 } 197 198 function creativeElement(slot) { 199 return slot.querySelector('[data-r89pb-creative="1"]'); 200 } 201 202 function creativeLoaded(slot) { 203 var img = creativeElement(slot);
204 return !!(img && img.complete && img.naturalWidth > 0); 205 } 206 207 function sendCreativeImpression(slot) { 208 if (!slotCanTrack(slot) || !creativeLoaded(slot)) { 209 return false; 210 } 211 212 return sendEvent('creative_impression', slot); 213 } 214 215 function waitForCreativeLoad(slot, callback) { 216 var img = creativeElement(slot); 217 var key = eventKey('creative_impression', slot); 218 219 if (!img || loadWaits[key]) { 220 return; 221 } 222 223 loadWaits[key] = true; 224 img.addEventListener('load', function() { 225 delete loadWaits[key]; 226 callback(); 227 }, { once: true }); 228 } 229 230 function slotMeetsVisibilityThreshold(slot, minimumRatio) { 231 if (!slotCanTrack(slot) || !slot.getBoundingClientRect) { 232 return false; 233 } 234 235 var rect = slot.getBoundingClientRect(); 236 if (rect.width <= 0 || rect.height <= 0) { 237 return false; 238 } 239 240 var viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0; 241 var viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0; 242 var visibleWidth = Math.max(0, Math.min(rect.right, viewportWidth) - Math.max(rect.left, 0)); 243 var visibleHeight = Math.max(0, Math.min(rect.bottom, viewportHeight) - Math.max(rect.top, 0)); 244 var visibleArea = visibleWidth * visibleHeight; 245 var totalArea = rect.width * rect.height; 246 247 return totalArea > 0 && (visibleArea / totalArea) >= minimumRatio; 248 } 249 250 function initCreativeImpressions(slots) { 251 var creativeSlots = slots.filter(function(slot) { 252 return creativeElement(slot) && slotCanTrack(slot); 253 }); 254 255 if (!creativeSlots.length) { 256 return; 257 } 258 259 if (typeof window.IntersectionObserver !== 'function') { 260 creativeSlots.forEach(function(slot) { 261 if (!sendCreativeImpression(slot)) { 262 waitForCreativeLoad(slot, function() { 263 sendCreativeImpression(slot); 264 }); 265 } 266 }); 267 return; 268 } 269 270 var observer = new IntersectionObserver(function(entries) { 271 entries.forEach(function(entry) { 272 var slot = entry.target; 273 var key = eventKey('creative_impression', slot); 274 275 if (sent[key]) { 276 observer.unobserve(slot); 277 return; 278 } 279 280 if (entry.isIntersecting && entry.intersectionRatio >= 0.5 && slotCanTrack(slot)) { 281 if (!timers[key]) { 282 timers[key] = window.setTimeout(function() { 283 delete timers[key]; 284 if (sendCreativeImpression(slot)) { 285 observer.unobserve(slot); 286 return; 287 } 288 289 waitForCreativeLoad(slot, function() { 290 if (slotMeetsVisibilityThreshold(slot, 0.5) && sendCreativeImpression(slot)) { 291 observer.unobserve(slot); 292 } 293 }); 294 }, 1000); 295 } 296 } else if (timers[key]) { 297 window.clearTimeout(timers[key]); 298 delete timers[key]; 299 } 300 }); 301 }, { threshold: [0, 0.5, 1] }); 302 303 creativeSlots.forEach(function(slot) { 304 observer.observe(slot); 305 }); 306 } 307 308 function closestCreativeLink(target) { 309 while (target && target !== document) { 310 if (target.matches && target.matches('.r89pb-creative-link')) { 311 return target; 312 } 313 314 target = target.parentNode; 315 } 316 317 return null; 318 } 319 320 function addViewportDeviceToClick(link) { 321 if (!window.URL || !link.href) { 322 return; 323 } 324 325 try { 326 var url = new URL(link.href, window.location.href); 327 url.searchParams.set('vd', viewportDevice()); 328 link.href = url.toString(); 329 } catch (error) {} 330 } 331 332 function handleCreativeClick(event) { 333 if (!isEnabled()) { 334 return; 335 } 336 337 var link = closestCreativeLink(event.target); 338 if (!link) { 339 return; 340 } 341 342 var slot = link.closest ? link.closest('.r89pb-ad-slot') : null; 343 if (slot) { 344 sendCreativeImpression(slot); 345 } 346 347 addViewportDeviceToClick(link); 348 } 349 350 var TRACKABLE_SLOT_SELECTOR = '.r89pb-ad-slot[data-r89pb-unit][data-r89pb-track-signature]'; 351 var BOUND_ATTR = 'data-r89pb-analytics-bound'; 352 var rescanTimer = null; 353 354 function bindNewSlots() { 355 if (!isEnabled()) { 356 return; 357 } 358 359 var slots = Array.prototype.slice.call(document.querySelectorAll(TRACKABLE_SLOT_SELECTOR)).filter(function(slot) { 360 return !slot.hasAttribute(BOUND_ATTR); 361 }); 362 363 if (!slots.length) { 364 return; 365 } 366 367 slots.forEach(function(slot) { 368 slot.setAttribute(BOUND_ATTR, '1'); 369 if (slotCanTrack(slot)) { 370 sendEvent('wrapper_render', slot); 371 } 372 }); 373 374 initCreativeImpressions(slots); 375 } 376 377 function nodeBringsSlots(node) { 378 if (node.nodeType !== 1) { 379 return false;
380 } 381 382 if (node.matches && node.matches(TRACKABLE_SLOT_SELECTOR)) { 383 return true; 384 } 385 386 return !!(node.querySelector && node.querySelector(TRACKABLE_SLOT_SELECTOR)); 387 } 388 389 function scheduleRescan() { 390 if (rescanTimer !== null) { 391 return; 392 } 393 394 rescanTimer = window.setTimeout(function() { 395 rescanTimer = null; 396 bindNewSlots(); 397 }, 50); 398 } 399 400 function watchForAppendedSlots() { 401 var root = document.documentElement || document.body; 402 if (typeof window.MutationObserver !== 'function' || !root) { 403 return; 404 } 405 406 var observer = new MutationObserver(function(mutations) { 407 for (var i = 0; i < mutations.length; i++) { 408 var added = mutations[i].addedNodes; 409 for (var j = 0; j < added.length; j++) { 410 if (nodeBringsSlots(added[j])) { 411 scheduleRescan(); 412 return; 413 } 414 } 415 } 416 }); 417 418 observer.observe(root, { childList: true, subtree: true }); 419 } 420 421 function init() { 422 if (!isEnabled()) { 423 return; 424 } 425 426 bindNewSlots(); 427 428 // Infinite-scroll themes (JNews auto-load-next-post, Jetpack infinite 429 // scroll, Ajax Load More) append later articles after DOMContentLoaded; 430 // keep binding their slots so impressions keep counting. 431 watchForAppendedSlots(); 432 } 433 434 if (document.readyState === 'loading') { 435 document.addEventListener('DOMContentLoaded', init); 436 } else { 437 init(); 438 } 439 440 document.addEventListener('click', handleCreativeClick, true); 441})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.