1/** 2 * ACTV TRKR â in-page tracker. 3 * 4 * SAFETY CONTRACT (v1.18.2+): 5 * This script is a passive observer. It MUST NOT, under any circumstance: 6 * - call preventDefault() or stopPropagation() on any event 7 * - block, delay, or modify form submissions, checkout, or payment flows 8 * - throw an uncaught error that could break other scripts on the page 9 * - perform synchronous network requests 10 * - depend on jQuery or any third-party global 11 * 12 * Failure mode policy: if anything goes wrong (missing browser API, bad config, 13 * network outage, exception), the tracker silently disables itself. The host 14 * page must always continue to function. 15 * 16 * Transport policy: prefer navigator.sendBeacon (browser-managed, never blocks 17 * unload). Fall back to fetch+keepalive. Never use synchronous XHR. 18 * 19 * QA mode: enabled when ?actv_debug=1 is in the URL OR window.mmConfig.debug 20 * is true. Logs to console with the [ACTV] prefix. Errors are still swallowed. 21 */ 22(function () { 23 'use strict'; 24 25 // ââ OUTER GUARD ââââââââââââââââââââââââââââââââââââââââââââââ 26 // Nothing inside this IIFE may escape. If the entire bootstrap throws, 27 // the host page is unaffected. 28 try { 29 30 if (typeof window === 'undefined' || typeof document === 'undefined') return; 31 if (!window.mmConfig) return; 32 33 var CFG = window.mmConfig; 34 35 // ââ QA / Debug mode ââââââââââââââââââââââââââââââââââââââââ 36 var DEBUG = false; 37 try { 38 if (CFG.debug === true) DEBUG = true; 39 var qs = (window.location && window.location.search) || ''; 40 if (qs.indexOf('actv_debug=1') !== -1) DEBUG = true; 41 } catch (e) {} 42 43 function dbg() { 44 if (!DEBUG) return; 45 try { 46 var args = ['[ACTV]']; 47 for (var i = 0; i < arguments.length; i++) args.push(arguments[i]); 48 if (window.console && window.console.log) window.console.log.apply(window.console, args); 49 } catch (e) {} 50 } 51 52 function dbgErr(label, err) { 53 if (!DEBUG) return; 54 try { 55 if (window.console && window.console.warn) window.console.warn('[ACTV]', label, err); 56 } catch (e) {} 57 } 58 59 // safe(fn) wraps any handler so an internal exception cannot bubble to the host. 60 function safe(fn, label) { 61 return function () { 62 try { return fn.apply(this, arguments); } 63 catch (e) { dbgErr(label || 'handler error', e); } 64 }; 65 } 66 67 // SECURITY (v1.9.17+): use narrow-scope ingest token; fall back to admin 68 // key only for older plugin builds during the upgrade window. 69 var INGEST_CRED = CFG.ingestToken || CFG.apiKey || ''; 70 var USE_INGEST_TOKEN = !!CFG.ingestToken; 71 var COOKIE_VID = 'mm_vid'; 72 var COOKIE_SID = 'mm_sid'; 73 var COOKIE_UTM = 'mm_utm'; 74 var COOKIE_TS = 'mm_ts'; 75 var CONSENT_KEY = 'mm_consent'; 76 var SESSION_TIMEOUT = 30 * 60 * 1000; 77 // Time-on-page heartbeat. Raised from 20s -> 60s (2026-05) to cut 78 // ingest volume 3x. The page-hide / before-unload paths still send a 79 // final sendBeacon so total active_seconds remains accurate. 80 var SIGNAL_INTERVAL = 60000; 81 var WATCHDOG_MULTIPLIER = 2; 82 var MAX_EVENTS_PER_SESSION = 200; 83 var MAX_QUEUE_SIZE = 500; 84 var QUEUE_STORAGE_KEY = 'mm_event_queue'; 85 var FLUSH_INTERVAL = 10000; 86 var MAX_RETRY_DELAY = 300000; 87 var BASE_RETRY_DELAY = 2000; 88 89 function authHeaders(extra) { 90 var h = extra || {}; 91 h['Content-Type'] = 'application/json'; 92 // IMPORTANT: fetch requests must not rely on X-Ingest-Token because that 93 // header triggers a CORS preflight our ingest endpoints do not allow. 94 // Keep credentials in the JSON body via withAuthBody(); only legacy API 95 // key auth may ride in Authorization for backward compatibility. 96 if (!USE_INGEST_TOKEN && INGEST_CRED) { 97 h['Authorization'] = 'Bearer ' + INGEST_CRED; 98 } 99 return h; 100 } 101 102 function withAuthBody(payload) { 103 if (USE_INGEST_TOKEN) { 104 payload.ingest_token = INGEST_CRED; 105 } else if (INGEST_CRED) { 106 payload.api_key = INGEST_CRED; 107 } 108 return payload; 109 } 110 111 // ââ Consent Mode ââââââââââââââââââââââââââââââââââââââââââââââ 112 var consentMode = (CFG.consentMode || 'relaxed').toLowerCase(); 113 // v1.20.9+: Limited Pre-Consent opt-in. When true AND strict AND no consent, 114 // the tracker boots a reduced pipeline (anonymous pageview only â no IDs, 115 // no cookies, no journeys). Off by default; existing sites unaffected. 116 var limitedPreConsent = CFG.limitedPreConsent === true; 117 var consentState = 'no_consent'; 118 var trackerInitialized = false;
119 var limitedModeActive = false; 120 121 function getStoredConsent() { 122 try { return localStorage.getItem(CONSENT_KEY); } catch (e) { return null; } 123 } 124 125 function setStoredConsent(value) { 126 try { localStorage.setItem(CONSENT_KEY, value); } catch (e) {} 127 } 128 129 function clearStoredConsent() { 130 try { localStorage.removeItem(CONSENT_KEY); } catch (e) {} 131 } 132 133 function clearAnalyticsCookies() { 134 try { 135 var cookies = [COOKIE_VID, COOKIE_SID, COOKIE_UTM, COOKIE_TS]; 136 for (var i = 0; i < cookies.length; i++) { 137 document.cookie = cookies[i] + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;SameSite=Lax'; 138 } 139 } catch (e) {} 140 } 141 142 function clearAnalyticsStorage() { 143 clearAnalyticsCookies(); 144 try { localStorage.removeItem(QUEUE_STORAGE_KEY); } catch (e) {} 145 clearStoredConsent(); 146 } 147 148 if (consentMode === 'strict') { 149 var _stored = getStoredConsent(); 150 if (_stored !== 'granted') { 151 clearAnalyticsCookies(); 152 } 153 } 154 155 // ââ Cookie helpers ââââââââââââââââââââââââââââââââââââââââââââââ 156 157 function setCookie(name, value, days) { 158 try { 159 var d = new Date(); 160 d.setTime(d.getTime() + days * 864e5); 161 document.cookie = name + '=' + encodeURIComponent(value) + 162 ';expires=' + d.toUTCString() + 163 ';path=/;SameSite=Lax'; 164 } catch (e) {} 165 } 166 167 function getCookie(name) { 168 try { 169 var v = document.cookie.match('(^|;)\\s*' + name + '=([^;]*)'); 170 return v ? decodeURIComponent(v[2]) : null; 171 } catch (e) { return null; } 172 } 173 174 // ââ UUID v4 âââââââââââââââââââââââââââââââââââââââââââââââââââââ 175 176 function uuid() { 177 try { 178 if (window.crypto && window.crypto.randomUUID) return window.crypto.randomUUID(); 179 } catch (e) {} 180 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 181 var r = (Math.random() * 16) | 0; 182 return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16); 183 }); 184 } 185 186 // ââ UTM extraction ââââââââââââââââââââââââââââââââââââââââââââââ 187 188 function getUtms() { 189 try { 190 if (typeof URLSearchParams === 'undefined') return null; 191 var params = new URLSearchParams(window.location.search); 192 var keys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; 193 var out = {}; 194 var found = false; 195 for (var i = 0; i < keys.length; i++) { 196 var v = params.get(keys[i]); 197 if (v) { out[keys[i]] = v; found = true; } 198 } 199 return found ? out : null; 200 } catch (e) { return null; } 201 } 202 203 function storedUtms() { 204 var raw = getCookie(COOKIE_UTM); 205 if (!raw) return {}; 206 try { return JSON.parse(raw); } catch (e) { return {}; } 207 } 208 209 // ââ Session logic âââââââââââââââââââââââââââââââââââââââââââââââ 210 211 function utmsChanged(newUtms) { 212 if (!newUtms) return false; 213 var old = storedUtms(); 214 return ['utm_source', 'utm_medium', 'utm_campaign'].some(function (k) { 215 return (newUtms[k] || '') !== (old[k] || ''); 216 }); 217 } 218 219 function resolveSession(urlUtms) { 220 var sid = getCookie(COOKIE_SID); 221 var lastTs = parseInt(getCookie(COOKIE_TS) || '0', 10); 222 var now = Date.now(); 223 var expired = !sid || !lastTs || (now - lastTs > SESSION_TIMEOUT); 224 var utmSwitch = urlUtms && utmsChanged(urlUtms); 225 226 if (expired || utmSwitch) { 227 sid = uuid(); 228 } 229 230 setCookie(COOKIE_SID, sid, 1); 231 setCookie(COOKIE_TS, String(now), 1); 232 return sid; 233 } 234 235 // ââ Device hint âââââââââââââââââââââââââââââââââââââââââââââââââ 236 237 function deviceType() {
238 try { 239 var w = window.innerWidth; 240 if (w < 768) return 'mobile'; 241 if (w < 1024) return 'tablet'; 242 return 'desktop'; 243 } catch (e) { return 'desktop'; } 244 } 245 246 // ââ Visitor identity ââââââââââââââââââââââââââââââââââââââââââ 247 248 function buildVisitor(vid) { 249 var v = { visitor_id: vid }; 250 if (CFG.wpUser) { 251 v.wp_user_id = String(CFG.wpUser.id); 252 v.wp_user_role = CFG.wpUser.role; 253 } 254 return v; 255 } 256 257 // ââ Event Queue System âââââââââââââââââââââââââââââââââââââââââ 258 259 var eventQueue = []; 260 261 function loadQueue() { 262 try { 263 var stored = localStorage.getItem(QUEUE_STORAGE_KEY); 264 if (stored) { 265 var parsed = JSON.parse(stored); 266 if (Array.isArray(parsed)) { 267 eventQueue = parsed; 268 } 269 } 270 } catch (e) {} 271 } 272 273 function saveQueue() { 274 try { 275 localStorage.setItem(QUEUE_STORAGE_KEY, JSON.stringify(eventQueue)); 276 } catch (e) {} 277 } 278 279 function clearSavedQueue() { 280 try { localStorage.removeItem(QUEUE_STORAGE_KEY); } catch (e) {} 281 } 282 283 function eventPriority(type) { 284 if (type === 'page_view') return 3; 285 if (type === 'form_submit') return 3; 286 if (type === 'signal' || type === 'time_update') return 0; 287 return 1; 288 } 289 290 function trimQueue() { 291 if (eventQueue.length <= MAX_QUEUE_SIZE) return; 292 eventQueue.sort(function (a, b) { 293 var pa = eventPriority(a.event_type); 294 var pb = eventPriority(b.event_type); 295 if (pa !== pb) return pa - pb; 296 return (new Date(a.timestamp)).getTime() - (new Date(b.timestamp)).getTime(); 297 }); 298 eventQueue = eventQueue.slice(eventQueue.length - MAX_QUEUE_SIZE); 299 saveQueue(); 300 } 301 302 function enqueueEvent(evt) { 303 try { 304 evt.event_uuid = evt.event_uuid || uuid(); 305 evt.timestamp = evt.timestamp || new Date().toISOString(); 306 eventQueue.push(evt); 307 trimQueue(); 308 saveQueue(); 309 } catch (e) { dbgErr('enqueueEvent', e); } 310 } 311 312 // ââ Tracker State ââââââââââââââââââââââââââââââââââââââââââââââ 313 var trackerState = 'active'; 314 var retryCount = 0; 315 var lastSuccessfulSend = Date.now(); 316 var lastSignalAttempt = 0; 317 318 function setTrackerState(newState) { 319 if (trackerState === newState) return; 320 trackerState = newState; 321 dbg('state ->', newState); 322 } 323 324 // ââ Transport ââââââââââââââââââââââââââââââââââââââââââââââââââ 325 // Order of preference for ALL outbound sends: 326 // 1. navigator.sendBeacon â browser-managed, never blocks unload, never throws on us 327 // 2. fetch + keepalive â async, errors caught 328 // 3. silent drop â last resort 329 // We NEVER use synchronous XHR (deprecated, can hang page during unload). 330 331 function getEventEndpoint() { 332 try { return CFG.endpoint.replace(/\/track-pageview$/, '/track-event'); } 333 catch (e) { return CFG.endpoint; } 334 } 335 336 // sendBeacon doesn't support custom headers, so the credential rides 337 // inside the JSON body via withAuthBody(). 338 function tryBeacon(endpoint, payload) { 339 try { 340 if (navigator && typeof navigator.sendBeacon === 'function') { 341 var body = JSON.stringify(payload); 342 var ok = navigator.sendBeacon(endpoint, new Blob([body], { type: 'application/json' })); 343 if (ok) return true; 344 } 345 } catch (e) { dbgErr('sendBeacon failed', e); } 346 return false; 347 } 348 349 function tryFetch(endpoint, payload, onSuccess, onFailure) { 350 try { 351 var body = JSON.stringify(payload); 352 if (typeof fetch !== 'function') { 353 if (onFailure) onFailure(); 354 return; 355 } 356 fetch(endpoint, { 357 method: 'POST', 358 headers: authHeaders(), 359 body: body, 360 keepalive: true, 361 }).then(function (resp) { 362 if (resp && (resp.ok || resp.status === 200)) { 363 if (onSuccess) onSuccess(); 364 } else { 365 if (onFailure) onFailure(); 366 } 367 }).catch(function (err) { 368 dbgErr('fetch rejected', err); 369 if (onFailure) onFailure(); 370 }); 371 } catch (e) { 372 dbgErr('tryFetch threw', e); 373 if (onFailure) onFailure(); 374 } 375 } 376 377 // Fetch-first send with optional retry callbacks. 378 // For normal pageviews/event batches we want an actual HTTP response so 379 // blocked or rejected requests do not look like successful sends. 380 // Beacon remains reserved for unload/final flush paths only.
381 function sendWithRetry(endpoint, payload, onSuccess, onFailure) { 382 tryFetch(endpoint, payload, function () { 383 if (onSuccess) onSuccess(); 384 }, function () { 385 retryCount++; 386 if (onFailure) onFailure(); 387 scheduleRetry(); 388 }); 389 } 390 391 var retryTimer = null; 392 function scheduleRetry() { 393 if (retryTimer) return; 394 var delay = Math.min(BASE_RETRY_DELAY * Math.pow(2, retryCount - 1), MAX_RETRY_DELAY); 395 retryTimer = setTimeout(safe(function () { 396 retryTimer = null; 397 flushQueue(); 398 }, 'retry'), delay); 399 } 400 401 // Fire-and-forget. Used for unload paths. 402 function sendBeaconSafe(endpoint, payload) { 403 if (tryBeacon(endpoint, payload)) return; 404 // Last resort â fetch keepalive. No sync XHR ever. 405 tryFetch(endpoint, payload, null, null); 406 } 407 408 // Standard send (pageviews, time_updates). 409 // Use fetch first so regular tracking is acknowledged by the backend 410 // instead of silently relying on the browser accepting a beacon. 411 function send(endpoint, payload) { 412 tryFetch(endpoint, payload, function () { 413 lastSuccessfulSend = Date.now(); 414 setTrackerState('active'); 415 }, function () { 416 // Last-resort fallback for browsers/environments where fetch+keepalive 417 // is unavailable or unreliable, but never treat a beacon as the normal 418 // success path for interactive tracking. 419 if (tryBeacon(endpoint, payload)) { 420 lastSuccessfulSend = Date.now(); 421 setTrackerState('active'); 422 return; 423 } 424 dbg('send failed, dropping payload'); 425 }); 426 } 427 428 function flushQueue() { 429 if (eventQueue.length === 0) return; 430 if (typeof navigator !== 'undefined' && navigator.onLine === false) { 431 setTrackerState('offline'); 432 return; 433 } 434 435 var pageviewEvents = []; 436 var otherEvents = []; 437 for (var i = 0; i < eventQueue.length; i++) { 438 if (eventQueue[i].event_type === 'page_view') { 439 pageviewEvents.push(eventQueue[i]); 440 } else { 441 otherEvents.push(eventQueue[i]); 442 } 443 } 444 445 if (otherEvents.length > 0) { 446 var batch = otherEvents.splice(0, 50); 447 var vid = getCookie(COOKIE_VID); 448 var sid = getCookie(COOKIE_SID); 449 450 var payload = withAuthBody({ 451 source: { 452 domain: CFG.domain, 453 type: 'wordpress', 454 plugin_version: CFG.pluginVersion, 455 }, 456 events: batch.map(function (e) { 457 return { 458 event_type: e.event_type, 459 event_uuid: e.event_uuid, 460 target_text: e.target_text, 461 page_url: e.page_url || window.location.href, 462 page_path: e.page_path || window.location.pathname, 463 timestamp: e.timestamp, 464 session_id: e.session_id || sid, 465 visitor_id: e.visitor_id || vid, 466 target_label: e.target_label, 467 target_href: e.target_href, 468 meta: e.meta, 469 }; 470 }), 471 }); 472 473 sendWithRetry(getEventEndpoint(), payload, function onSuccess() { 474 var sentUuids = {}; 475 for (var j = 0; j < batch.length; j++) { 476 sentUuids[batch[j].event_uuid] = true; 477 } 478 eventQueue = eventQueue.filter(function (e) { 479 return !sentUuids[e.event_uuid]; 480 }); 481 saveQueue(); 482 retryCount = 0; 483 lastSuccessfulSend = Date.now(); 484 setTrackerState('active'); 485 }, function onFailure() { 486 setTrackerState('retrying'); 487 }); 488 } 489 } 490 491 // ââ Time-on-Page Tracking âââââââââââââââââââââââââââââââââââââ 492 493 var pageTimer = { 494 startedAt: null, 495 activeMs: 0, 496 lastResumeAt: null, 497 isActive: true, 498 eventId: null, 499 signalTimer: null, 500 watchdogTimer: null, 501 502 start: function (eventId) { 503 this.eventId = eventId; 504 this.startedAt = Date.now(); 505 this.lastResumeAt = Date.now(); 506 this.activeMs = 0; 507 this.isActive = true; 508 this.startSignal(); 509 this.startWatchdog(); 510 }, 511 512 pause: function () { 513 if (this.isActive && this.lastResumeAt) { 514 this.activeMs += Date.now() - this.lastResumeAt; 515 this.isActive = false;
516 } 517 }, 518 519 resume: function () { 520 if (!this.isActive) { 521 this.lastResumeAt = Date.now(); 522 this.isActive = true; 523 } 524 }, 525 526 getActiveSeconds: function () { 527 var total = this.activeMs; 528 if (this.isActive && this.lastResumeAt) { 529 total += Date.now() - this.lastResumeAt; 530 } 531 return Math.round(total / 1000); 532 }, 533 534 startSignal: function () { 535 var self = this; 536 if (this.signalTimer) clearInterval(this.signalTimer); 537 this.signalTimer = setInterval(safe(function () { 538 if (self.isActive) { 539 self.sendTimeUpdate(); 540 lastSignalAttempt = Date.now(); 541 } 542 }, 'signal tick'), SIGNAL_INTERVAL); 543 }, 544 545 startWatchdog: function () { 546 var self = this; 547 if (this.watchdogTimer) clearInterval(this.watchdogTimer); 548 lastSignalAttempt = Date.now(); 549 // Min gap (ms) before we consider a missed tick a real "gap" worth 550 // logging. Browser background-tab throttling routinely produces 551 // 1-2 minute pauses that are NOT tracker faults â emitting events 552 // for those was generating ~92% of total ingest volume. 553 var GAP_MIN_MS = 5 * 60 * 1000; 554 this.watchdogTimer = setInterval(safe(function () { 555 var elapsed = Date.now() - lastSignalAttempt; 556 if (elapsed > SIGNAL_INTERVAL * WATCHDOG_MULTIPLIER) { 557 self.startSignal(); 558 setTrackerState('degraded'); 559 // Skip the gap event if: 560 // 1. Tab is hidden (browser-throttled timer is expected behavior). 561 // 2. Gap is shorter than 5 min (normal browser hiccup). 562 // 3. We've already reported a gap for this session 563 // (one diagnostic per session is enough). 564 if (document.visibilityState === 'hidden') return; 565 if (elapsed < GAP_MIN_MS) return; 566 try { 567 if (sessionStorage.getItem('mm_gap_reported') === '1') return; 568 sessionStorage.setItem('mm_gap_reported', '1'); 569 } catch (e) { /* sessionStorage blocked â fall through and emit */ } 570 enqueueEvent({ 571 event_type: 'session_gap_detected', 572 page_url: window.location.href, 573 page_path: window.location.pathname, 574 meta: { gap_ms: elapsed, reason: 'watchdog_restart' }, 575 }); 576 } 577 }, 'watchdog'), SIGNAL_INTERVAL * WATCHDOG_MULTIPLIER + 5000); 578 }, 579 580 sendTimeUpdate: function () { 581 if (!this.eventId) return; 582 var vid = getCookie(COOKIE_VID); 583 var sid = getCookie(COOKIE_SID); 584 send(CFG.endpoint, withAuthBody({ 585 type: 'time_update', 586 source: { domain: CFG.domain, type: 'wordpress', plugin_version: CFG.pluginVersion }, 587 event: { 588 event_id: this.eventId, 589 session_id: sid, 590 active_seconds: this.getActiveSeconds(), 591 }, 592 visitor: buildVisitor(vid), 593 })); 594 }, 595 596 sendFinal: function () { 597 if (!this.eventId) return; 598 clearInterval(this.signalTimer); 599 clearInterval(this.watchdogTimer); 600 var vid = getCookie(COOKIE_VID); 601 var sid = getCookie(COOKIE_SID); 602 sendBeaconSafe(CFG.endpoint, withAuthBody({ 603 type: 'time_update', 604 source: { domain: CFG.domain, type: 'wordpress', plugin_version: CFG.pluginVersion }, 605 event: { 606 event_id: this.eventId, 607 session_id: sid, 608 active_seconds: this.getActiveSeconds(), 609 }, 610 visitor: buildVisitor(vid), 611 })); 612 }, 613 }; 614 615 // ââ Intent-Based Click Tracking âââââââââââââââââââââââââââââââ 616 617 var sessionEventCount = 0; 618 var DOWNLOAD_EXTENSIONS = /\.(pdf|doc|docx|xls|xlsx|ppt|pptx|zip|rar|csv|txt|rtf|mp3|mp4|avi|mov|epub)$/i; 619 var CTA_CLASS_PATTERN = /\b(btn|button|cta|book)\b/i; 620 621 function classifyClick(el) { 622 if (!el) return null; 623 var target = el; 624 for (var i = 0; i < 5 && target; i++) { 625 var tag = (target.tagName || '').toLowerCase(); 626 if (target.getAttribute && target.getAttribute('data-actv') === 'cta') { 627 return { type: 'cta_click', text: getClickText(target), label: getActvLabel(target), el: target }; 628 } 629 if (tag === 'a') { 630 var href = target.getAttribute('href') || ''; 631 if (href.indexOf('tel:') === 0) return { type: 'tel_click', text: href.replace('tel:', ''), label: getActvLabel(target), el: target }; 632 if (href.indexOf('mailto:') === 0) return { type: 'mailto_click', text: href.replace('mailto:', ''), label: getActvLabel(target), el: target }; 633 if (DOWNLOAD_EXTENSIONS.test(href)) return { type: 'download_click', text: getClickText(target) || href.split('/').pop(), label: getActvLabel(target), el: target }; 634 var classes = target.className || ''; 635 var isCta = (typeof classes === 'string' && CTA_CLASS_PATTERN.test(classes)) || target.getAttribute('role') === 'button'; 636 if (isCta) return { type: 'cta_click', text: getClickText(target), label: getActvLabel(target), el: target }; 637 try { 638 var linkHost = new URL(href, window.location.origin).hostname; 639 if (linkHost && linkHost !== window.location.hostname) { 640 return { type: 'outbound_click', text: getClickText(target) || linkHost, label: getActvLabel(target), el: target }; 641 } 642 } catch (e) {} 643 } 644 if (tag === 'button' || (target.getAttribute && target.getAttribute('role') === 'button')) { 645 // CRITICAL: never interfere with submit buttons inside forms. 646 // We look at them only to skip them. 647 var inForm = target.closest && target.closest('form'); 648 var btnType = (target.getAttribute('type') || '').toLowerCase(); 649 if (!inForm || btnType !== 'submit') { 650 return { type: 'cta_click', text: getClickText(target), label: getActvLabel(target), el: target }; 651 } 652 } 653 target = target.parentElement; 654 } 655 return null; 656 } 657 658 function getActvLabel(el) { 659 if (!el || !el.getAttribute) return null; 660 return el.getAttribute('data-actv-label') || null; 661 } 662 663 function getClickText(el) { 664 var label = getActvLabel(el); 665 if (label) return label; 666 var text = (el.innerText || el.textContent || '').trim(); 667 if (text.length > 100) text = text.substring(0, 100); 668 return text || el.getAttribute('aria-label') || el.getAttribute('title') || ''; 669 } 670 671 // CRITICAL: this handler MUST NOT call preventDefault/stopPropagation, 672 // and MUST NOT throw. It's wrapped in safe() at attach time. 673 function trackClick(e) { 674 if (sessionEventCount >= MAX_EVENTS_PER_SESSION) return; 675 var result = classifyClick(e.target); 676 if (!result) return; 677 sessionEventCount++; 678 var vid = getCookie(COOKIE_VID); 679 var sid = getCookie(COOKIE_SID); 680 var evt = { 681 event_type: result.type, 682 target_text: result.text, 683 page_url: window.location.href, 684 page_path: window.location.pathname, 685 session_id: sid, 686 visitor_id: vid, 687 }; 688 if (result.label) evt.target_label = result.label; 689 var href = (result.el && result.el.getAttribute) ? (result.el.getAttribute('href') || '') : ''; 690 if (href) { 691 try { evt.target_href = new URL(href, window.location.origin).href; } catch (err) { evt.target_href = href; } 692 } 693 enqueueEvent(evt); 694 } 695 696 // Form listeners are intentionally disabled. The tracker stays purely 697 // passive â form data is captured server-side by class-forms.php after 698 // the form's own handler has completed. This guarantees we cannot
699 // interfere with submission, validation, payment tokenization, or nonces. 700 function trackFormFocus() { return; } 701 function handleFormSubmit() { return; } 702 703 // ââ Pageview tracking ââââââââââââââââââââââââââââââââââââââââââ 704 705 // 5s debounce: ignore duplicate pageviews for the same URL within a 5-second 706 // window. Prevents double-fire from DOMContentLoaded + immediate track(), 707 // SPA-style rapid history changes, and accidental remounts. Cuts edge-fn 708 // call volume materially without losing real navigations. 709 var _lastTrackUrl = null; 710 var _lastTrackAt = 0; 711 var PAGEVIEW_DEBOUNCE_MS = 5000; 712 713 function track() { 714 var nowMs = Date.now(); 715 var currentUrl = window.location.href; 716 if (_lastTrackUrl === currentUrl && (nowMs - _lastTrackAt) < PAGEVIEW_DEBOUNCE_MS) { 717 return; 718 } 719 _lastTrackUrl = currentUrl; 720 _lastTrackAt = nowMs; 721 722 var vid = getCookie(COOKIE_VID); 723 if (!vid) { 724 vid = uuid(); 725 setCookie(COOKIE_VID, vid, 365); 726 } 727 728 var urlUtms = getUtms(); 729 if (urlUtms) { 730 setCookie(COOKIE_UTM, JSON.stringify(urlUtms), 30); 731 } 732 733 var sid = resolveSession(urlUtms); 734 var attribution = Object.assign({}, storedUtms(), urlUtms || {}); 735 var eventId = uuid(); 736 737 pageTimer.start(eventId); 738 739 send(CFG.endpoint, withAuthBody({ 740 source: { 741 domain: CFG.domain, 742 type: 'wordpress', 743 plugin_version: CFG.pluginVersion, 744 }, 745 event: { 746 event_id: eventId, 747 session_id: sid, 748 page_url: window.location.href, 749 page_path: window.location.pathname, 750 title: document.title, 751 referrer: document.referrer || null, 752 device: deviceType(), 753 occurred_at: new Date().toISOString(), 754 }, 755 attribution: attribution, 756 visitor: buildVisitor(vid), 757 })); 758 } 759 760 // ââ Listeners ââââââââââââââââââââââââââââââââââââââââââââââââ 761 var flushIntervalId = null; 762 var listenersAttached = false; 763 764 function onVisibilityChange() { 765 if (!trackerInitialized) return; 766 if (document.hidden) { 767 pageTimer.pause(); 768 flushQueue(); 769 } else { 770 pageTimer.resume(); 771 var sid = getCookie(COOKIE_SID); 772 var lastTs = parseInt(getCookie(COOKIE_TS) || '0', 10); 773 var now = Date.now(); 774 if (!sid || (now - lastTs > SESSION_TIMEOUT)) { 775 enqueueEvent({ 776 event_type: 'session_resume', 777 page_url: window.location.href, 778 page_path: window.location.pathname, 779 meta: { gap_ms: now - lastTs }, 780 }); 781 resolveSession(null); 782 } 783 setCookie(COOKIE_TS, String(now), 1); 784 pageTimer.startSignal(); 785 flushQueue(); 786 } 787 } 788 789 function onFocus() { 790 if (!trackerInitialized) return; 791 pageTimer.resume(); 792 pageTimer.startSignal(); 793 flushQueue(); 794 } 795 796 function onBlur() { 797 if (!trackerInitialized) return; 798 pageTimer.pause(); 799 } 800 801 function onBeforeUnload() { 802 if (!trackerInitialized) return; 803 pageTimer.sendFinal(); 804 if (eventQueue.length > 0) { 805 var vid = getCookie(COOKIE_VID); 806 var sid = getCookie(COOKIE_SID); 807 var batch = eventQueue.splice(0, 50); 808 sendBeaconSafe(getEventEndpoint(), withAuthBody({ 809 source: { domain: CFG.domain, type: 'wordpress', plugin_version: CFG.pluginVersion }, 810 events: batch.map(function (e) { 811 return { 812 event_type: e.event_type, 813 event_uuid: e.event_uuid, 814 target_text: e.target_text, 815 page_url: e.page_url || window.location.href, 816 page_path: e.page_path || window.location.pathname, 817 timestamp: e.timestamp, 818 session_id: e.session_id || sid, 819 visitor_id: e.visitor_id || vid, 820 target_label: e.target_label, 821 target_href: e.target_href, 822 meta: e.meta, 823 }; 824 }), 825 })); 826 saveQueue(); 827 } 828 } 829 830 function onPageHide() { 831 if (!trackerInitialized) return; 832 pageTimer.sendFinal(); 833 } 834 835 function onOnline() { 836 if (!trackerInitialized) return; 837 setTrackerState('active'); 838 flushQueue(); 839 } 840 841 function onOffline() { 842 if (!trackerInitialized) return; 843 setTrackerState('offline'); 844 } 845 846 // Every listener is wrapped in safe() so a runtime exception here can 847 // never bubble out to the host page or other scripts. 848 var L = { 849 vis: safe(onVisibilityChange, 'visibilitychange'),
850 focus: safe(onFocus, 'focus'), 851 blur: safe(onBlur, 'blur'), 852 beforeUnload: safe(onBeforeUnload, 'beforeunload'), 853 pageHide: safe(onPageHide, 'pagehide'), 854 online: safe(onOnline, 'online'), 855 offline: safe(onOffline, 'offline'), 856 click: safe(trackClick, 'click'), 857 }; 858 859 function attachListeners() { 860 if (listenersAttached) return; 861 listenersAttached = true; 862 document.addEventListener('visibilitychange', L.vis); 863 window.addEventListener('focus', L.focus); 864 window.addEventListener('blur', L.blur); 865 window.addEventListener('beforeunload', L.beforeUnload); 866 window.addEventListener('pagehide', L.pageHide); 867 window.addEventListener('online', L.online); 868 window.addEventListener('offline', L.offline); 869 // Capture-phase listener: passive observation of clicks. We never call 870 // preventDefault or stopPropagation here. trackClick() is wrapped in safe(). 871 document.addEventListener('click', L.click, true); 872 flushIntervalId = setInterval(safe(function () { flushQueue(); }, 'flush tick'), FLUSH_INTERVAL); 873 dbg('listeners attached'); 874 } 875 876 function detachListeners() { 877 if (!listenersAttached) return; 878 listenersAttached = false; 879 document.removeEventListener('visibilitychange', L.vis); 880 window.removeEventListener('focus', L.focus); 881 window.removeEventListener('blur', L.blur); 882 window.removeEventListener('beforeunload', L.beforeUnload); 883 window.removeEventListener('pagehide', L.pageHide); 884 window.removeEventListener('online', L.online); 885 window.removeEventListener('offline', L.offline); 886 document.removeEventListener('click', L.click, true); 887 if (flushIntervalId) { clearInterval(flushIntervalId); flushIntervalId = null; } 888 } 889 890 // ââ Shutdown ââââââââââââââââââââââââââââââââââââââââââââââââââ 891 892 function shutdownTracker() { 893 try { 894 if (pageTimer.signalTimer) clearInterval(pageTimer.signalTimer); 895 if (pageTimer.watchdogTimer) clearInterval(pageTimer.watchdogTimer); 896 detachListeners(); 897 eventQueue = []; 898 trackerInitialized = false; 899 dbg('tracker shut down'); 900 } catch (e) { dbgErr('shutdown', e); } 901 } 902 903 // ââ Boot âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 904 905 function bootTracker() { 906 if (trackerInitialized) return; 907 trackerInitialized = true; 908 909 attachListeners(); 910 loadQueue(); 911 if (eventQueue.length > 0) { 912 setTimeout(safe(flushQueue, 'initial flush'), 1000); 913 } 914 915 if (document.readyState === 'loading') { 916 document.addEventListener('DOMContentLoaded', safe(track, 'pageview')); 917 } else { 918 safe(track, 'pageview')(); 919 } 920 dbg('tracker booted', { region: CFG.consentMode, version: CFG.pluginVersion }); 921 } 922 923 // v1.20.9+: Limited Pre-Consent boot path. 924 // ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 925 // Sends a SINGLE anonymous pageview when consent has not been granted 926 // and the admin has explicitly opted in via Settings â Privacy. 927 // Hard guarantees: 928 // - no visitor_id, no session_id, no wp_user_* 929 // - no cookies are read or written 930 // - no localStorage queue, no journey stitching, no listeners 931 // - no form/lead tracking, no clicks, no time-on-page signals 932 // - flagged with tracking_mode='limited' so the backend strips 933 // anything the client did manage to include 934 // 935 // If consent is later granted, the full tracker boots normally via 936 // mmConsent.grant() â this function does NOT mark tracker initialized, 937 // so the upgrade path is clean. 938 function bootLimitedTracker() { 939 if (limitedModeActive) return; 940 limitedModeActive = true; 941 942 function sendLimitedPageview() { 943 try { 944 var refDomain = null; 945 try { 946 if (document.referrer) refDomain = new URL(document.referrer).hostname; 947 } catch (e) {} 948 949 // Generate a one-shot event_id (required by backend) but DO NOT
950 // persist anywhere. New event_id each call = no stitching possible. 951 var eventId = 'lim_' + uuid(); 952 953 var payload = withAuthBody({ 954 source: { 955 domain: CFG.domain, 956 type: 'wordpress', 957 plugin_version: CFG.pluginVersion, 958 }, 959 event: { 960 event_id: eventId, 961 page_url: window.location.href, 962 page_path: window.location.pathname, 963 referrer: document.referrer || null, 964 device: deviceType(), 965 occurred_at: new Date().toISOString(), 966 tracking_mode: 'limited', 967 }, 968 }); 969 970 send(CFG.endpoint, payload); 971 dbg('limited pre-consent pageview sent'); 972 } catch (e) { dbgErr('limited pageview', e); } 973 } 974 975 if (document.readyState === 'loading') { 976 document.addEventListener('DOMContentLoaded', safe(sendLimitedPageview, 'limited pv')); 977 } else { 978 safe(sendLimitedPageview, 'limited pv')(); 979 } 980 } 981 982 // ââ Public Consent + Diagnostics API ââââââââââââââââââââââââââ 983 984 window.mmConsent = { 985 grant: safe(function () { 986 consentState = 'analytics_consent_granted'; 987 setStoredConsent('granted'); 988 if (!trackerInitialized) { 989 bootTracker(); 990 } 991 }, 'consent.grant'), 992 deny: safe(function () { 993 consentState = 'analytics_consent_denied'; 994 setStoredConsent('denied'); 995 shutdownTracker(); 996 clearAnalyticsCookies(); 997 }, 'consent.deny'), 998 revoke: safe(function () { 999 consentState = 'analytics_consent_denied'; 1000 clearAnalyticsStorage(); 1001 shutdownTracker(); 1002 }, 'consent.revoke'), 1003 getState: function () { return consentState; }, 1004 }; 1005 1006 // QA-mode diagnostics window. Useful for spot-checking install safety 1007 // on a client site without exposing internals to the public. 1008 if (DEBUG) { 1009 window.mmDiag = { 1010 getState: function () { 1011 return { 1012 initialized: trackerInitialized, 1013 consentState: consentState, 1014 consentMode: consentMode, 1015 trackerState: trackerState, 1016 queueLength: eventQueue.length, 1017 sessionEventCount: sessionEventCount, 1018 lastSuccessfulSend: lastSuccessfulSend, 1019 pluginVersion: CFG.pluginVersion, 1020 domain: CFG.domain, 1021 usingIngestToken: USE_INGEST_TOKEN, 1022 }; 1023 }, 1024 flush: safe(flushQueue, 'diag.flush'), 1025 shutdown: safe(shutdownTracker, 'diag.shutdown'), 1026 }; 1027 dbg('QA mode active. window.mmDiag is available.'); 1028 } 1029 1030 // CMP integrations (each handler is wrapped in safe()). 1031 document.addEventListener('cmplz_fire_categories', safe(function (e) { 1032 if (e.detail && e.detail.categories && e.detail.categories.indexOf('statistics') !== -1) { 1033 window.mmConsent.grant(); 1034 } else { 1035 window.mmConsent.deny(); 1036 } 1037 }, 'cmplz handler')); 1038 1039 document.addEventListener('mm_consent_update', safe(function (e) { 1040 if (e.detail && e.detail.analytics === true) { 1041 window.mmConsent.grant(); 1042 } else { 1043 window.mmConsent.deny(); 1044 } 1045 }, 'mm_consent_update handler')); 1046 1047 // ââ Consent-aware initialization ââââââââââââââââââââââââââââââ 1048 // 1049 // v1.22.2+: Always-on tracker. The plugin must produce data the moment 1050 // it is installed. Compliance (consent banner / regional behavior) is 1051 // handled at the admin level â the dashboard already warns operators 1052 // that they must configure a consent banner for EU/UK visitors. We do 1053 // NOT gate visitor data collection on `consentMode` or region anymore. 1054 // 1055 // Runtime opt-out is still fully respected: 1056 // - If the visitor previously denied via mmConsent.deny(), we honor it. 1057 // - If they later opt out via the banner, mmConsent.deny() shuts the 1058 // tracker down and clears analytics cookies in real time. 1059 // 1060 // This restores the "install â see traffic immediately" expectation 1061 // and removes the silent-site failure mode caused by a strict default. 1062 1063 var stored = getStoredConsent(); 1064 if (stored === 'denied') { 1065 consentState = 'analytics_consent_denied'; 1066 // Visitor has explicitly opted out â stay inert. 1067 } else { 1068 consentState = 'analytics_consent_granted'; 1069 bootTracker(); 1070 } 1071 1072 } catch (outerErr) { 1073 // Last line of defense. The host page MUST keep working even if our
1074 // bootstrap throws something unexpected. Surface the error in QA mode 1075 // only; never re-throw. 1076 try { 1077 if (window.mmConfig && (window.mmConfig.debug === true || 1078 (window.location && window.location.search && window.location.search.indexOf('actv_debug=1') !== -1))) { 1079 if (window.console && window.console.warn) { 1080 window.console.warn('[ACTV] tracker bootstrap failed (host page unaffected):', outerErr); 1081 } 1082 } 1083 } catch (_) {} 1084 } 1085})(); 1086
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.