1// replay-tracker.js â Hardened v1.1.1 2// All tracking routed through server-side WP REST proxy (API key never exposed to browser) 3 4(function() { 5 'use strict'; 6 7 function initReplayTracker() { 8 var config = window.ReplayObj; 9 if (!config || !config.proxyUrl) return; 10 11 var PROXY_URL = config.proxyUrl; 12 13 // Use PostHog session ID if available, otherwise generate local fallback 14 var sessionId = sessionStorage.getItem('replay_session'); 15 var phSessionId = window.posthog && typeof window.posthog.get_session_id === 'function' ? window.posthog.get_session_id() : null; 16 if (phSessionId) { 17 sessionId = phSessionId; 18 sessionStorage.setItem('replay_session', sessionId); 19 } else if (!sessionId) { 20 if (typeof crypto !== 'undefined' && crypto.randomUUID) { 21 sessionId = 'sess_' + crypto.randomUUID(); 22 } else if (typeof crypto !== 'undefined' && crypto.getRandomValues) { 23 var arr = new Uint8Array(16); 24 crypto.getRandomValues(arr); 25 sessionId = 'sess_' + Array.from(arr, function(b) { 26 return b.toString(16).padStart(2, '0'); 27 }).join(''); 28 } else { 29 // Last-resort fallback for very old browsers 30 sessionId = 'sess_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 12); 31 } 32 sessionStorage.setItem('replay_session', sessionId); 33 } 34 35 var visitorInfo = { 36 sessionId: sessionId, 37 url: window.location.href, 38 userAgent: navigator.userAgent, 39 screenWidth: window.innerWidth, 40 screenHeight: window.innerHeight, 41 language: navigator.language, 42 timestamp: new Date().toISOString(), 43 referrer: document.referrer || '' 44 }; 45 46 // Helper: route all requests through the WP REST proxy (no API key in browser) 47 function proxyFetch(endpoint, body) { 48 return fetch(PROXY_URL + '/track/' + endpoint, { 49 method: 'POST', 50 headers: { 'Content-Type': 'application/json' }, 51 body: JSON.stringify(body) 52 }); 53 } 54 55 // 1. Geo lookup + initial visitor ping 56 fetch('https://get.geojs.io/v1/ip/geo.json') 57 .then(function(res) { return res.json(); }) 58 .then(function(geo) { 59 visitorInfo.country = geo.country; 60 visitorInfo.state = geo.region; 61 visitorInfo.city = geo.city; 62 visitorInfo.latitude = geo.latitude ? parseFloat(geo.latitude) : null; 63 visitorInfo.longitude = geo.longitude ? parseFloat(geo.longitude) : null; 64 sendInitialPing(); 65 }) 66 .catch(function() { sendInitialPing(); }); 67 68 function sendInitialPing() { 69 proxyFetch('visitor', visitorInfo) 70 .then(function(res) { return res.json(); }) 71 .then(function() { 72 var currentUrl = window.location.href.toLowerCase(); 73 var eventType = 'page_view'; 74 if (currentUrl.indexOf('/checkout') !== -1) { 75 eventType = 'checkout_view'; 76 } else if (currentUrl.indexOf('/cart') !== -1) { 77 eventType = 'cart_view'; 78 } 79 proxyFetch('event', { 80 sessionId: sessionId, 81 event_type: eventType, 82 url: window.location.href, 83 timestamp: new Date().toISOString() 84 }); 85 }) 86 .catch(function() {}); 87 } 88 89 // 3. WooCommerce add-to-cart click tracking 90 document.addEventListener('click', function(e) { 91 var target = e.target; 92 if (target.classList && target.classList.contains('add_to_cart_button')) { 93 proxyFetch('event', { 94 sessionId: sessionId, 95 event_type: 'client_add_to_cart', 96 timestamp: new Date().toISOString() 97 }); 98 } 99 }); 100 101 // 4. WooCommerce purchase & order tracking 102 function extractWooCommerceTotal() { 103 var selectors = [ 104 '.woocommerce-order-overview__total .woocommerce-Price-amount', 105 '.woocommerce-order-overview__total strong', 106 '.woocommerce-checkout-review-order-table .order-total .woocommerce-Price-amount', 107 '.order_details tfoot .woocommerce-Price-amount', 108 '.woocommerce-Price-amount bdi', 109 '.woocommerce-Price-amount' 110 ]; 111 112 for (var i = 0; i < selectors.length; i++) { 113 var element = document.querySelector(selectors[i]); 114 if (element) { 115 var text = element.innerText || element.textContent || ''; 116 var cleaned = text.replace(/[^\d.,]/g, ''); 117 if (cleaned.indexOf(',') !== -1 && cleaned.indexOf('.') !== -1) { 118 cleaned = cleaned.replace(/,/g, ''); 119 } else if (cleaned.indexOf(',') !== -1 && cleaned.indexOf('.') === -1) { 120 var parts = cleaned.split(','); 121 if (parts[1] && parts[1].length === 2) { 122 cleaned = cleaned.replace(',', '.'); 123 } else { 124 cleaned = cleaned.replace(/,/g, ''); 125 } 126 } 127 var price = parseFloat(cleaned); 128 if (!isNaN(price) && price > 0) { return price; } 129 } 130 } 131 return null; 132 } 133 134 function extractWooCommerceOrderId() { 135 var orderEl = document.querySelector('.woocommerce-order-overview__order strong'); 136 if (orderEl) { 137 var orderId = orderEl.textContent.trim(); 138 if (orderId) return orderId; 139 } 140 var pathMatch = window.location.pathname.match(/\/order-received\/(\d+)/); 141 if (pathMatch && pathMatch[1]) return pathMatch[1]; 142 var params = new URLSearchParams(window.location.search); 143 var orderParam = params.get('order-received'); 144 if (orderParam) return orderParam; 145 return 'unknown_order'; 146 } 147 148 var isOrderReceivedPage = window.location.pathname.indexOf('/order-received') !== -1 || 149 window.location.search.indexOf('order-received') !== -1 || 150 document.querySelector('.woocommerce-order-overview') !== null; 151 152 if (isOrderReceivedPage) { 153 var orderId = extractWooCommerceOrderId(); 154 var trackingKey = 'tracked_order_' + orderId; 155 156 if (!localStorage.getItem(trackingKey)) { 157 var total = extractWooCommerceTotal() || 0; 158 localStorage.setItem(trackingKey, 'true'); 159 160 proxyFetch('event', { 161 sessionId: sessionId, 162 event_type: 'purchase', 163 url: window.location.href, 164 timestamp: new Date().toISOString(), 165 order_id: orderId, 166 total: total, 167 price: total 168 }); 169 } 170 } 171 172 // 5. Heartbeat to keep session alive 173 setInterval(function() { 174 if (document.visibilityState === 'visible') { 175 proxyFetch('visitor', { sessionId: sessionId }).catch(function() {}); 176 } 177 }, 60000); 178 179 // 6. Page leave 180 window.addEventListener('beforeunload', function() { 181 var payload = JSON.stringify({ 182 sessionId: sessionId, 183 event_type: 'page_leave', 184 url: window.location.href, 185 timestamp: new Date().toISOString() 186 }); 187 188 if (navigator.sendBeacon) { 189 var blob = new Blob([payload], { type: 'application/json' }); 190 navigator.sendBeacon(PROXY_URL + '/track/event', blob); 191 } else { 192 fetch(PROXY_URL + '/track/event', { 193 method: 'POST', 194 keepalive: true, 195 headers: { 'Content-Type': 'application/json' }, 196 body: payload 197 }).catch(function() {}); 198 } 199 }); 200 } 201 202 // A robust bootstrapper that waits for PostHog to be fully ready before starting 203 var bootstrapper = function() { 204 var attempts = 0; 205 var tryInit = function() { 206 var hasPostHog = window.posthog && typeof window.posthog.get_session_id === 'function' && window.posthog.get_session_id(); 207 if (window.ReplayObj && hasPostHog) { 208 initReplayTracker(); 209 } else if (attempts < 40) { // Wait up to 10 seconds (40 * 250ms) 210 attempts++; 211 setTimeout(tryInit, 250); 212 } else if (window.ReplayObj) { 213 // Fallback to local session ID generation if PostHog fails to load (e.g. adblocker) 214 initReplayTracker(); 215 } 216 }; 217 tryInit(); 218 }; 219 220 if (document.readyState === 'loading') { 221 document.addEventListener('DOMContentLoaded', bootstrapper); 222 } else { 223 bootstrapper(); 224 } 225})();
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.