1/* =========================================================================== 2 VMP Pass â Universal auto-capture engine (GA4 + Meta Pixel) 3 --------------------------------------------------------------------------- 4 Captures EVERY button and EVERY user action site-wide with no per-element 5 wiring. It binds to `document`, not to markup, so it covers every theme 6 template automatically â including ones added later. 7 8 Four rules make this HIGH-QUALITY data rather than merely high-volume: 9 10 1. FEW EVENT NAMES, RICH PARAMS. GA4 caps a property at 500 distinct event 11 names; exceeding it silently drops data forever. We emit ~25 names and 12 put the detail in parameters. 13 2. EVERY event carries the same context block (page_type, template, 14 section, component, element_*, lang), so any event slices the same way 15 in GA4 Explorations and Meta breakdowns. 16 3. GA4 limits are enforced here (param name <=40 chars, value <=100 chars, 17 <=25 params) so events are never silently rejected server-side. 18 4. Consent-aware: GA rides Consent Mode; Meta is gated on `marketing`. 19 20 Transport is vmp-tracking.js (window.vmpTrack). That file is deferred, so if 21 it has not executed yet events buffer here and flush on arrival. 22 =========================================================================== */ 23(function (w, d) { 24 'use strict'; 25 if (w.__vmpAutoTrackLoaded) { return; } 26 w.__vmpAutoTrackLoaded = true; 27 28 var CFG = w.vmpAnalyticsConfig || {}; 29 var DEBUG = !!CFG.debug || /[?&]vmpdebug=1/.test(w.location.search); 30 var NOW = function () { return Date.now ? Date.now() : new Date().getTime(); }; 31 32 /* ââ 0. utilities ââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 33 function log() { 34 if (!DEBUG || !w.console) { return; } 35 try { console.log.apply(console, ['%c[vmp]', 'color:#5b8def;font-weight:700'].concat([].slice.call(arguments))); } catch (e) {} 36 } 37 38 function clamp(s, n) { 39 s = (s == null ? '' : String(s)); 40 s = s.replace(/\s+/g, ' ').trim(); // "Get\n started" -> "Get started" 41 return s.length > n ? s.slice(0, n) : s; 42 } 43 44 /** GA4 param names must be [a-z0-9_] and <= 40 chars. */ 45 function safeKey(k) { 46 return String(k).toLowerCase().replace(/[^a-z0-9_]/g, '_').slice(0, 40); 47 } 48 49 function throttle(fn, ms) { 50 var last = 0, timer = null, lastArgs; 51 return function () { 52 lastArgs = arguments; 53 var now = NOW(); 54 if (now - last >= ms) { last = now; fn.apply(null, lastArgs); } 55 else if (!timer) { 56 timer = setTimeout(function () { timer = null; last = NOW(); fn.apply(null, lastArgs); }, ms - (now - last)); 57 } 58 }; 59 } 60 61 function closest(el, sel) { 62 if (!el || el.nodeType !== 1) { return null; } 63 if (el.closest) { try { return el.closest(sel); } catch (e) { return null; } } 64 while (el && el.nodeType === 1) { 65 if (el.matches && el.matches(sel)) { return el; } 66 el = el.parentElement; 67 } 68 return null; 69 } 70 71 function attr(el, name) { 72 return (el && el.getAttribute) ? (el.getAttribute(name) || '') : ''; 73 } 74 75 /* ââ 1. consent â GA rides Consent Mode; Meta gated on `marketing` âââââ */ 76 function consent() { 77 try { 78 var m = d.cookie.match(/(?:^|;\s*)vmp_cookie_consent=([^;]*)/); 79 if (!m) { return null; } // undecided 80 return JSON.parse(decodeURIComponent(m[1])); 81 } catch (e) { return null; } 82 } 83 function marketingAllowed() { var c = consent(); return !!(c && c.marketing); } 84 85 /* ââ 2. session identity âââââââââââââââââââââââââââââââââââââââââââââââ */ 86 var SESSION_KEY = 'vmpSess'; 87 var SESSION_TTL = 30 * 60 * 1000; // GA4-style 30 min inactivity window 88 89 function uuid() { 90 // Correlation id for GA4 <-> Meta CAPI matching. Not security-sensitive.
91 try { 92 if (w.crypto && w.crypto.randomUUID) { return w.crypto.randomUUID(); } 93 if (w.crypto && w.crypto.getRandomValues) { 94 var b = new Uint8Array(16); w.crypto.getRandomValues(b); 95 b[6] = (b[6] & 0x0f) | 0x40; b[8] = (b[8] & 0x3f) | 0x80; 96 var h = []; for (var i = 0; i < 16; i++) { h.push((b[i] + 0x100).toString(16).slice(1)); } 97 return h.slice(0, 4).join('') + '-' + h.slice(4, 6).join('') + '-' + h.slice(6, 8).join('') + '-' + h.slice(8, 10).join('') + '-' + h.slice(10, 16).join(''); 98 } 99 } catch (e) {} 100 return 'x' + NOW().toString(36) + Math.random().toString(36).slice(2, 10); 101 } 102 103 function session() { 104 var s = null; 105 try { s = JSON.parse(w.sessionStorage.getItem(SESSION_KEY) || 'null'); } catch (e) {} 106 var now = NOW(); 107 if (!s || (now - s.last) > SESSION_TTL) { s = { id: uuid(), start: now, pages: 0, events: 0 }; } 108 s.last = now; 109 try { w.sessionStorage.setItem(SESSION_KEY, JSON.stringify(s)); } catch (e) {} 110 return s; 111 } 112 var SESS = session(); 113 114 function deviceCtx() { 115 var nav = w.navigator || {}; 116 var conn = nav.connection || nav.mozConnection || nav.webkitConnection || {}; 117 var vw = w.innerWidth || 0; 118 return { 119 viewport_w: vw, 120 viewport_h: w.innerHeight || 0, 121 device_type: vw < 768 ? 'mobile' : (vw < 1024 ? 'tablet' : 'desktop'), 122 touch: ('ontouchstart' in w) ? 1 : 0, 123 connection: clamp(conn.effectiveType || '', 20) 124 }; 125 } 126 127 /* ââ 3. element context â the heart of the data quality ââââââââââââââââ */ 128 129 /** Best human label for an element, in descending order of intent. */ 130 function labelOf(el) { 131 if (!el) { return ''; } 132 var v = attr(el, 'data-vmp-label') 133 || attr(el, 'aria-label') 134 || clamp(el.textContent, 100) 135 || attr(el, 'title') 136 || attr(el, 'value') 137 || attr(el, 'name') 138 || attr(el, 'placeholder'); 139 if (!v && el.querySelector) { 140 var img = el.querySelector('img[alt]'); 141 if (img) { v = attr(img, 'alt'); } 142 } 143 if (!v && el.querySelector) { 144 // icon-only buttons still deserve an identity 145 var icon = el.querySelector('[class*="bi-"],[class*="fa-"]'); 146 if (icon) { 147 var cls = (icon.className || '').toString().split(/\s+/).filter(function (c) { return /^(bi|fa)-/.test(c); })[0]; 148 if (cls) { v = 'icon:' + cls; } 149 } 150 } 151 return clamp(v, 100); 152 } 153 154 /** What KIND of control was pressed. */ 155 function elementKind(el) { 156 if (attr(el, 'data-vmp-kind')) { return clamp(attr(el, 'data-vmp-kind'), 40); } 157 var tag = (el.tagName || '').toLowerCase(); 158 var type = (attr(el, 'type') || '').toLowerCase(); 159 var bs = attr(el, 'data-bs-toggle'); 160 if (bs) { return 'bs_' + clamp(bs, 24); } 161 if (tag === 'a') { 162 var href = attr(el, 'href'); 163 if (/^mailto:/i.test(href)) { return 'mailto'; } 164 if (/^tel:/i.test(href)) { return 'tel'; } 165 if (/^#/.test(href)) { return 'anchor'; } 166 return 'link'; 167 } 168 if (tag === 'button') { return type === 'submit' ? 'submit' : 'button'; } 169 if (tag === 'input') { return type || 'input'; } 170 if (tag === 'summary') { return 'disclosure'; } 171 if (tag === 'label') { return 'label'; } 172 if (tag === 'select') { return 'select'; } 173 var role = attr(el, 'role'); 174 if (role) { return 'role_' + clamp(role, 24); } 175 return tag || 'element'; 176 } 177 178 /** Nearest named region â explicit data-vmp-section wins, then landmarks. */ 179 function sectionOf(el) { 180 var s = closest(el, '[data-vmp-section]'); 181 if (s) { return clamp(attr(s, 'data-vmp-section'), 60); } 182 var node = closest(el, 'section[id],[data-section],header,footer,nav,main,aside,dialog,form[id]'); 183 if (!node) { return 'unclassified'; } 184 var tag = (node.tagName || '').toLowerCase(); 185 if (tag === 'header') { return 'site_header'; } 186 if (tag === 'footer') { return 'site_footer'; } 187 if (tag === 'nav') { return 'navigation'; } 188 if (tag === 'aside') { return 'sidebar'; } 189 if (tag === 'main') { return 'main'; } 190 return clamp(attr(node, 'data-section') || node.id || tag, 60); 191 } 192 193 /** Nearest UI component wrapper. */ 194 function componentOf(el) { 195 var c = closest(el, '[data-vmp-component]'); 196 if (c) { return clamp(attr(c, 'data-vmp-component'), 40); } 197 var known = closest(el, '.modal,.offcanvas,.accordion,.tab-pane,.carousel,.dropdown-menu,.card,table,.navbar,.pagination,form'); 198 if (!known) { return 'page'; } 199 var cl = (known.className || '').toString(); 200 if (/\bmodal\b/.test(cl)) { return 'modal'; } 201 if (/\boffcanvas\b/.test(cl)) { return 'offcanvas'; } 202 if (/\baccordion\b/.test(cl)) { return 'accordion'; } 203 if (/\btab-pane\b/.test(cl)) { return 'tabs'; } 204 if (/\bcarousel\b/.test(cl)) { return 'carousel'; } 205 if (/\bdropdown-menu\b/.test(cl)) { return 'dropdown'; } 206 if (/\bnavbar\b/.test(cl)) { return 'navbar'; } 207 if (/\bpagination\b/.test(cl)) { return 'pagination'; } 208 if (/\bcard\b/.test(cl)) { return 'card'; } 209 var tag = (known.tagName || '').toLowerCase(); 210 return tag === 'form' ? 'form' : (tag === 'table' ? 'table' : 'page'); 211 } 212 213 /** Short CSS-ish path so you can find the exact control in the DOM later. */ 214 function pathOf(el) { 215 var parts = [], node = el, hops = 0; 216 while (node && node.nodeType === 1 && hops < 4) { 217 var seg = (node.tagName || '').toLowerCase(); 218 if (node.id) { parts.unshift(seg + '#' + node.id); break; } 219 var cl = (node.className || '').toString().split(/\s+/).filter(
219Boolean)[0]; 220 if (cl) { seg += '.' + cl; } 221 parts.unshift(seg); 222 node = node.parentElement; hops++; 223 } 224 return clamp(parts.join('>'), 100); 225 } 226 227 /** Full context block attached to every element-scoped event. */ 228 function ctxOf(el) { 229 var o = { 230 element_text: labelOf(el), 231 element_kind: elementKind(el), 232 element_id: clamp(el.id || '', 60), 233 element_classes: clamp((el.className || '').toString().split(/\s+/).filter(Boolean).slice(0, 3).join(' '), 100), 234 element_path: pathOf(el), 235 section: sectionOf(el), 236 component: componentOf(el) 237 }; 238 var href = attr(el, 'href'); 239 if (href) { 240 o.link_url = clamp(href, 100); 241 try { o.link_domain = clamp(new URL(href, w.location.href).hostname, 60); } catch (e) {} 242 } 243 var groupEl = closest(el, '[data-vmp-group]'); 244 if (groupEl) { o.item_group = clamp(attr(groupEl, 'data-vmp-group'), 60); } 245 return o; 246 } 247 248 /* ââ 4. emit: normalise, enforce GA4 limits, hand to vmp-tracking.js âââ */ 249 var PAGE = { 250 page_type: clamp(CFG.pageType || 'unknown', 40), 251 template: clamp(CFG.template || '', 60), 252 lang: clamp(CFG.lang || d.documentElement.getAttribute('lang') || '', 12), 253 logged_in: CFG.loggedIn ? 1 : 0, 254 page_path: clamp(w.location.pathname, 100) 255 }; 256 257 var buffer = [], flushTimer = null; 258 259 function deliver(name, params) { 260 if (typeof w.vmpTrack !== 'function') { return false; } 261 try { w.vmpTrack(name, params); } catch (e) { log('deliver failed', e); } 262 return true; 263 } 264 265 function flushBuffer() { 266 if (!buffer.length) { return; } 267 if (typeof w.vmpTrack !== 'function') { 268 if (!flushTimer) { flushTimer = setTimeout(function () { flushTimer = null; flushBuffer(); }, 400); } 269 return; 270 } 271 var pending = buffer; buffer = []; 272 for (var i = 0; i < pending.length; i++) { deliver(pending[i][0], pending[i][1]); } 273 } 274 275 /** track(name, params) â name from the fixed vocabulary, detail in params. */ 276 function track(name, params) { 277 params = params || {}; 278 var out = {}; 279 // context first so it can never be truncated away by the 25-param cap 280 out.page_type = PAGE.page_type; 281 out.template = PAGE.template; 282 out.lang = PAGE.lang; 283 out.logged_in = PAGE.logged_in; 284 out.page_path = PAGE.page_path; 285 out.session_id = SESS.id; 286 out.event_id = uuid(); // pixel <-> Conversions API de-duplication 287 288 var keys = Object.keys(params), n = 0; 289 for (var i = 0; i < keys.length && n < 18; i++) { // 7 context + 18 = GA4's 25 290 var v = params[keys[i]]; 291 if (v === undefined || v === null || v === '') { continue; } 292 out[safeKey(keys[i])] = (typeof v === 'number' || typeof v === 'boolean') ? v : clamp(v, 100); 293 n++; 294 } 295 296 SESS.events++;
297 try { w.sessionStorage.setItem(SESSION_KEY, JSON.stringify(SESS)); } catch (e) {} 298 299 log(name, out); 300 if (!deliver(name, out)) { buffer.push([name, out]); flushBuffer(); } 301 return out.event_id; 302 } 303 304 w.vmpAutoTrack = { 305 track: track, ctxOf: ctxOf, 306 session: function () { return SESS; }, 307 debug: function () { DEBUG = true; log('debug logging on'); } 308 }; 309 310 /* ââ 5. page view (enhanced) âââââââââââââââââââââââââââââââââââââââââââ */ 311 (function pageView() { 312 var q = {}; 313 try { 314 var sp = new URLSearchParams(w.location.search); 315 ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'ref', 'gclid', 'fbclid'].forEach(function (k) { 316 var v = sp.get(k); if (v) { q[k] = clamp(v, 100); } 317 }); 318 } catch (e) {} 319 var dev = deviceCtx(); 320 track('page_view_enhanced', { 321 page_title: clamp(d.title, 100), 322 page_referrer: clamp(d.referrer, 100), 323 device_type: dev.device_type, 324 viewport_w: dev.viewport_w, 325 viewport_h: dev.viewport_h, 326 connection: dev.connection, 327 touch: dev.touch, 328 utm_source: q.utm_source, 329 utm_medium: q.utm_medium, 330 utm_campaign: q.utm_campaign, 331 ref: q.ref, 332 is_new_session: SESS.pages === 0 ? 1 : 0 333 }); 334 SESS.pages++; 335 })(); 336 337 /* ââ 6. CLICKS â every button and link on every template âââââââââââââââ */ 338 var INTERACTIVE = 'a,button,input,select,textarea,summary,label,[role="button"],[role="link"],[role="tab"],[role="menuitem"],[role="switch"],[role="checkbox"],[role="radio"],[onclick],[data-bs-toggle],[data-vmp-event],.btn,.nav-link,.dropdown-item,.page-link'; 339 340 /** Clicks that hit nothing interactive â a strong UX-friction signal. */ 341 var deadClick = throttle(function (target) { 342 try { 343 if (w.getSelection && String(w.getSelection()).length > 0) { return; } // was selecting text 344 // only report clicks on things that LOOK clickable 345 if (!closest(target, 'img,svg,i,.card,[class*="icon"],[class*="btn"],[class*="tile"]')) { return; } 346 track('dead_click', ctxOf(target)); 347 } catch (e) {} 348 }, 2000); 349 350 var lastClick = { key: '', t: 0, n: 0 }; 351 352 d.addEventListener('click', function (ev) { 353 var target = ev.target; 354 if (!target || target.nodeType !== 1) { return; } 355 var el = closest(target, INTERACTIVE); 356 if (!el) { deadClick(target); return; } 357 358 var c = ctxOf(el); 359 360 // rage-click: same control 3+ times inside 1s => friction 361 var key = c.element_path + '|' + c.element_text, now = NOW(); 362 if (key === lastClick.key && (now - lastClick.t) < 1000) { 363 if (++lastClick.n === 3) { track('rage_click', c); } 364 } else { lastClick.n = 1; } 365 lastClick.key = key; lastClick.t = now; 366 367 track('element_click', c); 368 369 // link-intent sub-events, kept separate so each can be a GA4 key event 370 var href = attr(el, 'href'); 371 if (href) { 372 if (/^mailto:/i.test(href)) { track('mailto_click', c); } 373 else if (/^tel:/i.test(href)) { track('tel_click', c); } 374 else if (/\.(pdf|zip|dmg|exe|msi|pkg|apk|csv|xlsx?|docx?|pptx?|txt|rar|7z)(\?|#|$)/i.test(href)) { 375 track('file_download', { 376 file_name: clamp(href.split('/').pop(), 100), 377 file_extension: clamp((href.split('.').pop() || '').split(/[?#]/)[0], 10), 378 link_url: c.link_url, section: c.section, element_text: c.element_text 379 }); 380 } 381 else if (/^https?:/i.test(href) && c.link_domain && c.link_domain !== w.location.hostname) { 382 track('outbound_click', c); 383 } 384 else if (/^#./.test(href)) { track('anchor_click', c); } 385 } 386 387 // explicit business intent declared in the template 388 var semantic = attr(el, 'data-vmp-event'); 389 if (semantic) { 390 var p = { 391 element_text: c.element_text, section: c.section, component: c.component, 392 location: attr(el, 'data-vmp-label') || c.element_text 393 }; 394 var val = attr(el, 'data-vmp-value'); 395 if (val) { p.value = parseFloat(val) || 0; p.currency = attr(el, 'data-vmp-currency') || 'USD'; } 396 var item = attr(el, 'data-vmp-item'); if (item) { p.item_name = clamp(item, 100); } 397 track(semantic, p); 398 } 399 }, true); 400 401 /* Keyboard activation â accessibility users must not be invisible. */ 402 d.addEventListener('keydown', function (ev) { 403 if (ev.key !== 'Enter' && ev.key !== ' ') { return; } 404 var el = closest(ev.target, '[role="button"],[role="tab"],[role="switch"],summary'); 405 if (!el) { return; } 406 var c = ctxOf(el); c.input_method = 'keyboard'; 407 track('element_click', c); 408 }, true); 409 410 /* ââ 7. FORMS â start, field completion, errors, submit, abandonment ââ */ 411 var formStates = []; 412 413 /** 414 * The group a field belongs to. A real <form> wins; otherwise fall back to the 415 * nearest named region so form-less widgets (password generator, username 416 * generator, pricing filters, calculators) still report start/completion. 417 */ 418 function fieldGroup(el) { 419 return closest(el, 'form,[data-vmp-form]') || 420 closest(el, '[data-vmp-section],section[id],main') || d.body; 421 } 422 423 function formState(f) { 424 for (var i = 0; i < formStates.length; i++) { if (formStates[i].el === f) { return formStates[i]; } } 425 var isForm = (f.tagName || '').toLowerCase() === 'form'; 426 var id = attr(f, 'data-vmp-form') || f.id || attr(f, 'name') || 427 (isForm ? attr(f, 'action') : attr(f, 'data-vmp-section')) || 428 (isForm ? 'form' : (f.tagName || 'group').toLowerCase()); 429 var s = { 430 el: f, 431 isForm: isForm, 432 id: clamp((isForm ? '' : 'group_') + id, 60), 433 started: false, submitted: false, fields: {}, fieldCount: 0, 434 firstTouch: 0, section: sectionOf(f) 435 }; 436 formStates.push(s); 437 return s; 438 } 439 function fieldName(el) { 440 return clamp(attr(el, 'name') || el.id || attr(el, 'placeholder') || attr(el, 'aria-label') || (el.tagName || '').toLowerCase(), 60); 441 } 442 /** Never send the VALUE of anything that could hold personal or secret data. */ 443 function isSensitive(el) { 444 var t = (attr(el, 'type') || '').toLowerCase(); 445 var n = (attr(el, 'name') + ' ' + (el.id || '') + ' ' + attr(el, 'autocomplete')).toLowerCase(); 446 return t === 'password' || t === 'email' || t === 'tel' || 447 /pass|pwd|secret|token|card|cvv|ssn|iban|email|phone|otp|code|address|name/.test(n); 448 } 449 function isField(el) { return el && /^(input|select|textarea)$/i.test(el.tagName || ''); } 450 451 d.addEventListener('focusin', function (ev) { 452 if (!isField(ev.target)) { return; } 453 var f = fieldGroup(ev.target); if (!f) { return; } 454 var s = formState(f); 455 if (s.started) { return; } 456 s.started = true;
456 s.firstTouch = NOW(); 457 track('form_start', { form_id: s.id, section: s.section, first_field: fieldName(ev.target) }); 458 }, true); 459 460 d.addEventListener('focusout', function (ev) { 461 var el = ev.target; 462 if (!isField(el)) { return; } 463 var f = fieldGroup(el); if (!f) { return; } 464 var s = formState(f), name = fieldName(el); 465 if (el.value && String(el.value).length && !s.fields[name]) { 466 s.fields[name] = 1; s.fieldCount++; 467 track('form_field_complete', { 468 form_id: s.id, field_name: name, 469 field_type: clamp(attr(el, 'type') || (el.tagName || '').toLowerCase(), 30), 470 section: s.section, field_index: s.fieldCount, 471 field_value: isSensitive(el) ? undefined : clamp(String(el.value), 60) 472 }); 473 } 474 if (el.checkValidity && !el.checkValidity()) { 475 track('form_field_error', { 476 form_id: s.id, field_name: name, 477 error_type: clamp(el.validationMessage || 'invalid', 100), section: s.section 478 }); 479 } 480 }, true); 481 482 d.addEventListener('submit', function (ev) { 483 var f = ev.target; 484 if (!f || (f.tagName || '').toLowerCase() !== 'form') { return; } 485 var s = formState(f); 486 s.submitted = true; 487 track('form_submit', { 488 form_id: s.id, section: s.section, 489 field_count: f.querySelectorAll('input,select,textarea').length, 490 filled_count: s.fieldCount, 491 time_to_submit: s.firstTouch ? Math.round((NOW() - s.firstTouch) / 1000) : 0 492 }); 493 }, true); 494 495 /* Contact Form 7 outcomes â the theme's main lead capture. */ 496 ['wpcf7mailsent', 'wpcf7invalid', 'wpcf7spam', 'wpcf7mailfailed'].forEach(function (evt) { 497 d.addEventListener(evt, function (e) { 498 var okay = (evt === 'wpcf7mailsent'); 499 var id = (e.detail && (e.detail.contactFormId || e.detail.id)) || ''; 500 track(okay ? 'form_submit_success' : 'form_submit_error', { 501 form_id: clamp('cf7_' + id, 60), status: evt.replace('wpcf7', ''), section: 'contact_form' 502 }); 503 if (okay) { track('lead', { location: 'cf7_' + id }); } 504 }, false); 505 }); 506 507 /* Choice controls. */ 508 d.addEventListener('change', function (ev) { 509 var el = ev.target; 510 if (!isField(el)) { return; } 511 var type = (attr(el, 'type') || '').toLowerCase(); 512 var c = ctxOf(el); 513 var tag = (el.tagName || '').toLowerCase(); 514 if (tag === 'select') { 515 var opt = el.options && el.options[el.selectedIndex]; 516 track('option_select', { control: fieldName(el), option_value: clamp(opt ? opt.text : el.value, 100), section: c.section, component: c.component }); 517 } else if (type === 'checkbox' || type === 'radio') { 518 track('option_select', { control: fieldName(el), option_value: clamp(labelOf(closest(el, 'label')) || el.value, 100), checked: el.checked ? 1 : 0, section: c.section, component: c.component }); 519 } else if (type === 'range') { 520 track('option_select', { control: fieldName(el), option_value: clamp(el.value, 20), section: c.section, component: c.component }); 521 } else if (type === 'file') { 522 track('file_select', { control: fieldName(el), file_count: (el.files && el.files.length) || 0, section: c.section }); 523 } 524 }, true); 525 526 /* ââ 8. Bootstrap 5 components âââââââââââââââââââââââââââââââââââââââââ */ 527 function bs(evt, fn) { d.addEventListener(evt, fn, true); } 528 bs('shown.bs.modal', function (e) { track('modal_open', { modal_id: clamp(e.target.id || labelOf(e.target), 60), section: sectionOf(e.target) }); }); 529 bs('hidden.bs.modal', function (e) { track('modal_close', { modal_id: clamp(e.target.id || labelOf(e.target), 60), section: sectionOf(e.target) }); }); 530 bs('shown.bs.offcanvas', function (e) { track('offcanvas_open', { panel_id: clamp(e.target.id || '', 60) }); }); 531 bs('hidden.bs.offcanvas', function (e) { track('offcanvas_close', { panel_id: clamp(e.target.id || '', 60) }); }); 532 bs('shown.bs.tab', function (e) { track('tab_change', { tab_name: labelOf(e.target), section: sectionOf(e.target), component: 'tabs' }); }); 533 bs('shown.bs.collapse', function (e) { track('accordion_open', { panel_id: clamp(e.target.id || labelOf(e.target), 60), section: sectionOf(e.target) }); }); 534 bs('hidden.bs.collapse', function (e) { track('accordion_close', { panel_id: clamp(e.target.id || labelOf(e.target), 60), section: sectionOf(e.target) }); }); 535 bs('shown.bs.dropdown', function (e) { track('dropdown_open', { menu_name: labelOf(e.target), section: sectionOf(e.target) }); });
536 bs('slid.bs.carousel', function (e) { track('carousel_slide', { carousel_id: clamp(e.target.id || '', 60), slide_index: (e.to != null ? e.to : -1), section: sectionOf(e.target) }); }); 537 538 /* Native <details> disclosure (FAQ blocks that aren't Bootstrap). */ 539 d.addEventListener('toggle', function (ev) { 540 var el = ev.target; 541 if (!el || (el.tagName || '').toLowerCase() !== 'details') { return; } 542 track(el.open ? 'accordion_open' : 'accordion_close', { 543 panel_id: clamp(labelOf(el.querySelector('summary')) || el.id, 60), section: sectionOf(el) 544 }); 545 }, true); 546 547 /* ââ 9. engagement time (active-only) ââââââââââââââââââââââââââââââââââ */ 548 var activeStart = NOW(), activeTotal = 0, isActive = !d.hidden, interactions = 0, maxDepth = 0; 549 function activeMs() { return activeTotal + (isActive ? (NOW() - activeStart) : 0); } 550 function setActive(on) { 551 if (on === isActive) { return; } 552 if (isActive) { activeTotal += NOW() - activeStart; } else { activeStart = NOW(); } 553 isActive = on; 554 } 555 w.addEventListener('blur', function () { setActive(false); }); 556 w.addEventListener('focus', function () { setActive(true); }); 557 ['click', 'keydown', 'scroll', 'touchstart'].forEach(function (e) { 558 d.addEventListener(e, function () { interactions++; }, { passive: true, capture: true }); 559 }); 560 561 /* ââ 10. scroll depth + section visibility + CTA impressions âââââââââââ */ 562 var depthMarks = [10, 25, 50, 75, 90, 100], depthHit = {}; 563 w.addEventListener('scroll', throttle(function () { 564 var docH = Math.max(d.body.scrollHeight, d.documentElement.scrollHeight) - w.innerHeight; 565 if (docH <= 0) { return; } 566 var pct = Math.min(100, Math.round((w.pageYOffset / docH) * 100)); 567 if (pct > maxDepth) { maxDepth = pct; } 568 for (var i = 0; i < depthMarks.length; i++) { 569 var m = depthMarks[i]; 570 if (pct >= m && !depthHit[m]) { 571 depthHit[m] = 1; 572 track('scroll_depth', { percent_scrolled: m, seconds_elapsed: Math.round(activeMs() / 1000) }); 573 } 574 } 575 }, 350), { passive: true }); 576 577 if (w.IntersectionObserver) { 578 var seen = {}; 579 var secObs = new w.IntersectionObserver(function (entries) { 580 entries.forEach(function (en) { 581 if (!en.isIntersecting) { return; } 582 var el = en.target, name = attr(el, 'data-vmp-section') || el.id || pathOf(el); 583 secObs.unobserve(el); 584 if (seen[name]) { return; } 585 seen[name] = 1; 586 track('section_view', { section: clamp(name, 60), seconds_elapsed: Math.round(activeMs() / 1000), percent_scrolled: maxDepth }); 587 }); 588 }, { threshold: 0.4 }); 589 590 var ctaObs = new w.IntersectionObserver(function (entries) { 591 entries.forEach(function (en) { 592 if (!en.isIntersecting) { return; } 593 var el = en.target, c = ctxOf(el); 594 ctaObs.unobserve(el); 595 track('cta_view', { 596 element_text: c.element_text, section: c.section, component: c.component, 597 link_url: c.link_url, promotion_name: attr(el, 'data-vmp-label') || c.element_text 598 }); 599 }); 600 }, { threshold: 0.6 }); 601 602 var startObservers = function () { 603 try { 604 var secs = d.querySelectorAll('[data-vmp-section],section[id],main section'); 605 for (var i = 0; i < secs.length && i < 120; i++) { secObs.observe(secs[i]); } 606 var ctas = d.querySelectorAll('[data-vmp-event],[data-vmp-impression]'); 607 for (var j = 0; j < ctas.length && j < 120; j++) { ctaObs.observe(ctas[j]); } 608 } catch (e) {} 609 }; 610 if (d.readyState === 'loading') { d.addEventListener('DOMContentLoaded', startObservers); } else { startObservers(); } 611 } 612 613 /* ââ 11. time milestones + engaged session âââââââââââââââââââââââââââââ */ 614 var timeMarks = [10, 30, 60, 120, 300], timeHit = {}, engagedSent = false;
615 setInterval(function () { 616 var secs = Math.round(activeMs() / 1000); 617 for (var i = 0; i < timeMarks.length; i++) { 618 var m = timeMarks[i]; 619 if (secs >= m && !timeHit[m]) { 620 timeHit[m] = 1; 621 track('time_on_page', { seconds_elapsed: m, percent_scrolled: maxDepth, interactions: interactions }); 622 } 623 } 624 if (!engagedSent && (secs >= 10 || interactions >= 2 || maxDepth >= 50)) { 625 engagedSent = true; 626 track('engaged_session', { seconds_elapsed: secs, percent_scrolled: maxDepth, interactions: interactions }); 627 } 628 }, 5000); 629 630 /* ââ 12. media âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 631 function bindMedia(v) { 632 if (v.__vmpBound) { return; } 633 v.__vmpBound = true; 634 var name = clamp(attr(v, 'data-vmp-label') || attr(v, 'title') || (v.currentSrc || '').split('/').pop(), 100); 635 var marks = {}; 636 v.addEventListener('play', function () { track('video_start', { video_title: name, video_duration: Math.round(v.duration || 0), section: sectionOf(v) }); }); 637 v.addEventListener('ended', function () { track('video_complete', { video_title: name, video_duration: Math.round(v.duration || 0), section: sectionOf(v) }); }); 638 v.addEventListener('timeupdate', throttle(function () { 639 if (!v.duration) { return; } 640 var pct = Math.round((v.currentTime / v.duration) * 100); 641 [25, 50, 75].forEach(function (m) { 642 if (pct >= m && !marks[m]) { marks[m] = 1; track('video_progress', { video_title: name, video_percent: m, section: sectionOf(v) }); } 643 }); 644 }, 1000)); 645 } 646 function scanMedia() { 647 var v = d.querySelectorAll('video,audio'); 648 for (var i = 0; i < v.length; i++) { bindMedia(v[i]); } 649 } 650 if (d.readyState === 'loading') { d.addEventListener('DOMContentLoaded', scanMedia); } else { scanMedia(); } 651 652 /* ââ 13. content interactions ââââââââââââââââââââââââââââââââââââââââââ */ 653 d.addEventListener('copy', function () { 654 var sel = ''; 655 try { sel = String(w.getSelection() || ''); } catch (e) {} 656 track('copy_text', { characters: sel.length, content_sample: clamp(sel, 60), section: sectionOf(d.activeElement || d.body) }); 657 }, true); 658 659 d.addEventListener('mouseup', throttle(function () { 660 var sel = ''; 661 try { sel = String(w.getSelection() || ''); } catch (e) {} 662 if (sel.length >= 25) { track('text_selection', { characters: sel.length, content_sample: clamp(sel, 60) }); } 663 }, 3000), true); 664 665 w.addEventListener('beforeprint', function () { track('print_page', {}); }); 666 667 var exitSent = false; 668 d.addEventListener('mouseout', function (ev) { 669 if (exitSent || ev.clientY > 8 || ev.relatedTarget) { return; } 670 exitSent = true; 671 track('exit_intent', { seconds_elapsed: Math.round(activeMs() / 1000), percent_scrolled: maxDepth, interactions: interactions }); 672 }, true); 673 674 /* ââ 14. errors â broken UI is the costliest thing to not measure ââââââ */ 675 w.addEventListener('error', function (ev) { 676 if (ev && ev.target && ev.target !== w && ev.target.nodeType === 1) { 677 var src = attr(ev.target, 'src') || attr(ev.target, 'href'); 678 if (src) { track('resource_error', { resource_url: clamp(src, 100), resource_type: clamp((ev.target.tagName || '').toLowerCase(), 20) }); } 679 return; 680 } 681 track('js_error', { 682 error_message: clamp((ev && ev.message) || 'error', 100), 683 error_source: clamp(String((ev && ev.filename) || '').split('/').pop(), 60), 684 error_line: (ev && ev.lineno) || 0 685 }); 686 }, true); 687 688 w.addEventListener('unhandledrejection', function (ev) { 689 var r = ev && ev.reason; 690 track('js_error', { error_message: clamp((r && (r.message || r)) || 'rejection', 100), error_source: 'promise' }); 691 }); 692 693 /* ââ 15. page leave â the summary that makes every other event usable ââ */ 694 var leaveSent = false;
695 function pageLeave() { 696 if (leaveSent) { return; } 697 leaveSent = true; 698 setActive(false); 699 for (var i = 0; i < formStates.length; i++) { 700 var s = formStates[i]; 701 if (s.isForm && s.started && !s.submitted) { 702 track('form_abandon', { form_id: s.id, section: s.section, filled_count: s.fieldCount, seconds_elapsed: Math.round((NOW() - s.firstTouch) / 1000) }); 703 } 704 } 705 track('page_leave', { 706 engagement_time_msec: Math.round(activeMs()), 707 seconds_elapsed: Math.round(activeMs() / 1000), 708 percent_scrolled: maxDepth, interactions: interactions, events_in_session: SESS.events 709 }); 710 } 711 w.addEventListener('pagehide', pageLeave); 712 d.addEventListener('visibilitychange', function () { 713 setActive(!d.hidden); 714 if (d.hidden) { pageLeave(); leaveSent = false; } 715 }); 716 717 /* ââ 16. history-API navigation ââââââââââââââââââââââââââââââââââââââââ */ 718 (function () { 719 var push = w.history && w.history.pushState; 720 if (!push) { return; } 721 w.history.pushState = function () { 722 var r = push.apply(this, arguments); 723 setTimeout(function () { 724 PAGE.page_path = clamp(w.location.pathname, 100); 725 depthHit = {}; maxDepth = 0; leaveSent = false; 726 track('page_view_enhanced', { page_title: clamp(d.title, 100), navigation_type: 'spa' }); 727 }, 30); 728 return r; 729 }; 730 w.addEventListener('popstate', function () { 731 PAGE.page_path = clamp(w.location.pathname, 100); 732 track('page_view_enhanced', { page_title: clamp(d.title, 100), navigation_type: 'popstate' }); 733 }); 734 })(); 735 736 flushBuffer(); 737 log('auto-capture armed', PAGE, 'marketing consent:', marketingAllowed()); 738})(window, document);
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.