1/** 2 * MW Analytics â first-party tracking beacon. 3 * 4 * Cookieless. Sends one pageview on load (and on SPA route changes), then a 5 * time-on-page value on unload. No personal data is collected client-side. 6 */ 7( function () { 8 'use strict'; 9 var C = window.MWATrack || {}; 10 if ( ! C.collect ) { return; } 11 12 // Respect a global opt-out flag if a consent tool sets one. 13 if ( window.MWA_DO_NOT_TRACK === true ) { return; } 14 15 var enter = Date.now(); 16 var hitId = 0; 17 var lastPath = ''; 18 19 function qp( name ) { 20 try { 21 return new URLSearchParams( window.location.search ).get( name ) || ''; 22 } catch ( e ) { return ''; } 23 } 24 25 function sendPageview() { 26 var path = window.location.pathname || '/'; 27 if ( path === lastPath ) { return; } // Avoid duplicate on same path. 28 lastPath = path; 29 enter = Date.now(); 30 hitId = 0; 31 32 var body = { 33 path: path, 34 hostname: window.location.hostname, 35 referrer: document.referrer || '', 36 utm_source: qp( 'utm_source' ), 37 utm_medium: qp( 'utm_medium' ), 38 utm_campaign: qp( 'utm_campaign' ) 39 }; 40 41 fetch( C.collect, { 42 method: 'POST', 43 headers: { 'Content-Type': 'application/json' }, 44 body: JSON.stringify( body ), 45 credentials: 'omit', 46 keepalive: true 47 } ).then( function ( r ) { return r.json(); } ) 48 .then( function ( d ) { if ( d && d.id ) { hitId = d.id; } } ) 49 .catch( function () {} ); 50 } 51 52 function sendDuration() { 53 if ( ! hitId || ! C.duration ) { return; } 54 var seconds = Math.round( ( Date.now() - enter ) / 1000 ); 55 if ( seconds < 1 ) { return; } 56 var payload = JSON.stringify( { id: hitId, seconds: seconds } ); 57 try { 58 if ( navigator.sendBeacon ) { 59 navigator.sendBeacon( C.duration, new Blob( [ payload ], { type: 'application/json' } ) ); 60 return; 61 } 62 } catch ( e ) {} 63 fetch( C.duration, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload, keepalive: true, credentials: 'omit' } ).catch( function () {} ); 64 } 65 66 // Fire the duration beacon once when the page is being hidden/left. 67 var durationSent = false; 68 function flush() { 69 if ( durationSent ) { return; } 70 durationSent = true; 71 sendDuration(); 72 } 73 document.addEventListener( 'visibilitychange', function () { 74 if ( document.visibilityState === 'hidden' ) { flush(); } 75 } ); 76 window.addEventListener( 'pagehide', flush ); 77 78 // SPA route changes: re-arm on history navigation. 79 [ 'pushState', 'replaceState' ].forEach( function ( m ) { 80 var orig = history[ m ]; 81 if ( typeof orig === 'function' ) { 82 history[ m ] = function () { 83 var r = orig.apply( this, arguments ); 84 flush(); durationSent = false; sendPageview(); 85 return r; 86 }; 87 } 88 } ); 89 window.addEventListener( 'popstate', function () { flush(); durationSent = false; sendPageview(); } ); 90 91 // Initial pageview. 92 if ( document.readyState === 'complete' || document.readyState === 'interactive' ) { 93 sendPageview(); 94 } else { 95 document.addEventListener( 'DOMContentLoaded', sendPageview ); 96 } 97 98 /* ---- Event / CTA tracking ---- */ 99 var EV = C.ev || {}; 100 function sendEvent( event, label, value ) { 101 if ( ! C.event ) { return; } 102 var payload = JSON.stringify( { event: event, label: ( label || '' ).slice( 0, 190 ), path: window.location.pathname || '/', value: value || 0 } ); 103 try { 104 if ( navigator.sendBeacon ) { navigator.sendBeacon( C.event, new Blob( [ payload ], { type: 'application/json' } ) ); return; } 105 } catch ( e ) {} 106 fetch( C.event, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload, keepalive: true, credentials: 'omit' } ).catch( function () {} ); 107 } 108 // Expose a manual API: window.mwaTrack('event_name', 'label', value) 109 window.mwaTrack = function ( event, label, value ) { sendEvent( event, label, value ); }; 110 111 if ( EV.on ) { 112 var selList = ( EV.selectors || '' ).split( /[\n,]+/ ).map( function ( s ) { return s.trim(); } ).filter( Boolean ); 113 function text( el ) { return ( el.getAttribute( 'aria-label' ) || el.textContent || el.value || '' ).replace( /\s+/g, ' ' ).trim().slice( 0, 120 ); } 114 function matchesAny( el ) { for ( var i = 0; i < selList.length; i++ ) { try { if ( el.closest( selList[ i ] ) ) { return el.closest( selList[ i ] ); } } catch ( e ) {} } return null; } 115 116 document.addEventListener( 'click', function ( e ) { 117 var t = e.target; 118 if ( ! t || ! t.closest ) { return; } 119 120 // Explicit opt-in via data attribute.
121 var dataEl = t.closest( '[data-mwa-event]' ); 122 if ( dataEl ) { sendEvent( dataEl.getAttribute( 'data-mwa-event' ) || 'cta', dataEl.getAttribute( 'data-mwa-label' ) || text( dataEl ) ); return; } 123 124 var a = t.closest( 'a[href]' ); 125 if ( a ) { 126 var href = a.getAttribute( 'href' ) || ''; 127 var low = href.toLowerCase(); 128 if ( EV.whatsapp && ( /wa\.me|api\.whatsapp\.com|web\.whatsapp\.com/.test( low ) || low.indexOf( 'whatsapp:' ) === 0 ) ) { sendEvent( 'whatsapp', text( a ) || 'WhatsApp' ); return; } 129 if ( low.indexOf( 'tel:' ) === 0 ) { sendEvent( 'call', href.replace( /^tel:/i, '' ) ); return; } 130 if ( low.indexOf( 'mailto:' ) === 0 ) { sendEvent( 'email', href.replace( /^mailto:/i, '' ).split( '?' )[ 0 ] ); return; } 131 // Configured CTA selectors. 132 var m = matchesAny( a ); 133 if ( m ) { sendEvent( 'cta', text( m ) ); return; } 134 // Outbound links. 135 if ( EV.outbound && /^https?:\/\//i.test( href ) ) { 136 try { if ( new URL( href, location.href ).host !== location.host ) { sendEvent( 'outbound', new URL( href, location.href ).host ); return; } } catch ( e ) {} 137 } 138 return; 139 } 140 // Non-anchor CTA (buttons matched by selector). 141 var cm = matchesAny( t ); 142 if ( cm ) { sendEvent( 'cta', text( cm ) ); } 143 }, true ); 144 145 if ( EV.forms ) { 146 document.addEventListener( 'submit', function ( e ) { 147 var f = e.target; 148 if ( ! f || f.tagName !== 'FORM' ) { return; } 149 if ( f.getAttribute( 'role' ) === 'search' || /\bsearch\b/i.test( f.className || '' ) ) { return; } 150 var label = f.getAttribute( 'name' ) || f.getAttribute( 'id' ) || ( f.className || '' ).split( ' ' )[ 0 ] || 'form'; 151 sendEvent( 'form_submit', label ); 152 }, true ); 153 } 154 } 155 156 /* ---- Heatmaps (click points + scroll depth) ---- */ 157 var HM = C.hm || {}; 158 if ( C.heat && HM.on && ( ( Math.random() * 100 ) <= ( HM.sample || 100 ) ) ) { 159 var clicks = []; 160 var maxDepth = 0; 161 var MAXPTS = 60; 162 163 function docHeight() { 164 var b = document.body, e = document.documentElement; 165 return Math.max( b ? b.scrollHeight : 0, b ? b.offsetHeight : 0, e ? e.clientHeight : 0, e ? e.scrollHeight : 0, e ? e.offsetHeight : 0 ); 166 } 167 function updateDepth() { 168 var dh = docHeight(); 169 if ( dh <= 0 ) { return; } 170 var reached = ( window.pageYOffset || document.documentElement.scrollTop || 0 ) + window.innerHeight; 171 var pm = Math.max( 0, Math.min( 1000, Math.round( reached / dh * 1000 ) ) ); 172 if ( pm > maxDepth ) { maxDepth = pm; } 173 } 174 window.addEventListener( 'scroll', updateDepth, { passive: true } ); 175 window.addEventListener( 'resize', updateDepth, { passive: true } ); 176 updateDepth(); 177 178 function capture( x, y ) { 179 if ( clicks.length >= MAXPTS ) { return; } 180 var w = window.innerWidth || 1; 181 var xpm = Math.max( 0, Math.min( 1000, Math.round( ( x / w ) * 1000 ) ) ); 182 clicks.push( [ xpm, Math.round( y ) ] ); 183 } 184 // Capture on pointerdown (covers mouse + touch reliably); fall back to 185 // click for environments without pointer events. 186 if ( window.PointerEvent ) { 187 document.addEventListener( 'pointerdown', function ( e ) { capture( e.clientX, ( e.pageY != null ? e.pageY : e.clientY + ( window.pageYOffset || 0 ) ) ); }, true ); 188 } else { 189 document.addEventListener( 'click', function ( e ) { capture( e.clientX, ( e.pageY != null ? e.pageY : e.clientY + ( window.pageYOffset || 0 ) ) ); }, true ); 190 document.addEventListener( 'touchstart', function ( e ) { var t = e.touches && e.touches[ 0 ]; if ( t ) { capture( t.clientX, t.clientY + ( window.pageYOffset || 0 ) ); } }, true ); 191 } 192 193 var sentClicks = 0; 194 var scrollSent = false; 195 // Send new clicks (and, on the final flush, the scroll depth). Sending 196 // clicks incrementally means data survives even if the unload beacon is 197 // dropped (common on mobile); scroll is sent once so pageview counts stay 198 // accurate. 199 function sendHeat( isFinal ) { 200 updateDepth(); 201 var batch = clicks.slice( sentClicks ); 202 var sendScroll = isFinal && ! scrollSent; 203 if ( ! batch.length && ! sendScroll ) { return; } 204 var body = { path: window.location.pathname || '/', vw: window.innerWidth, dh: docHeight() }; 205 if ( batch.length ) { body.clicks = batch; } 206 if ( sendScroll ) { body.scroll = maxDepth; scrollSent = true; } 207 sentClicks = clicks.length; 208 var payload = JSON.stringify( body ); 209 try {
210 if ( navigator.sendBeacon ) { navigator.sendBeacon( C.heat, new Blob( [ payload ], { type: 'application/json' } ) ); return; } 211 } catch ( e ) {} 212 fetch( C.heat, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload, keepalive: true, credentials: 'omit' } ).catch( function () {} ); 213 } 214 var heatSent = false; 215 function flushHeat() { if ( heatSent ) { return; } heatSent = true; sendHeat( true ); } 216 // Periodic partial flush so long sessions do not lose clicks. 217 var heatTimer = setInterval( function () { if ( clicks.length > sentClicks ) { sendHeat( false ); } }, 20000 ); 218 document.addEventListener( 'visibilitychange', function () { if ( document.visibilityState === 'hidden' ) { flushHeat(); } } ); 219 window.addEventListener( 'pagehide', function () { clearInterval( heatTimer ); flushHeat(); } ); 220 } 221} )();
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.