PageSourceSearch

https://livesinthebalance.org/wp-content/plugins/actv-trkr/assets/tracker.js?ver=1.22.5

js livesinthebalance.org collected 2026-09-24 09:14:45 UTC 40,803 bytes, 1,086 lines download raw bytes

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.