1/* ============================================================ 2 privateQuizAnalytics â Tracker privado isolado (v1.0) 3 ============================================================ 4 REGRAS: 5 - NÃO referencia fbq, gtag, dataLayer, NexoFy, Meta CAPI 6 - NÃO envia dados a terceiros 7 - Falha silenciosamente â nunca quebra o quiz 8 - Desativa com PRIVATE_ANALYTICS_ENABLED = false 9 ============================================================ */ 10(function () { 11 'use strict'; 12 13 /* ---- Kill switch ---- */ 14 if (typeof window.PRIVATE_ANALYTICS_ENABLED === 'undefined') { 15 window.PRIVATE_ANALYTICS_ENABLED = true; 16 } 17 18 /* ---- Configuração ---- */ 19 var CFG = { 20 endpoint: '/api/private-analytics/events', 21 flushInterval: 5000, 22 batchSize: 10, 23 maxRetries: 2, 24 retryDelays: [1000, 3000], 25 sessionKey: 'private_quiz_session_id', 26 originKey: 'private_analytics_origin', 27 firedKey: 'private_analytics_fired' 28 }; 29 30 /* Permite override do endpoint antes do carregamento ou fallback para porta 3100 no localhost */ 31 if (window.PRIVATE_ANALYTICS_ENDPOINT) { 32 CFG.endpoint = window.PRIVATE_ANALYTICS_ENDPOINT; 33 } else if (typeof window !== 'undefined' && window.location && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') && window.location.port !== '3100' && window.location.port !== '') { 34 CFG.endpoint = 'http://localhost:3100/api/private-analytics/events'; 35 } 36 37 /* ---- Eventos one-shot (disparam no máximo uma vez por sessão) ---- */ 38 var ONE_SHOT = [ 39 'internal_landing_view', 40 'internal_quiz_start', 41 'internal_quiz_complete', 42 'internal_quiz_passed', 43 'internal_quiz_failed', 44 'internal_price_view', 45 'internal_scroll_25', 46 'internal_scroll_50', 47 'internal_scroll_75', 48 'internal_scroll_90', 49 'internal_scroll_100', 50 'internal_thank_you_page_view' 51 ]; 52 53 /* ============================================================ 54 Helpers â UUID v4 55 ============================================================ */ 56 function uuid() { 57 try { 58 if (typeof crypto !== 'undefined' && crypto.getRandomValues) { 59 var b = new Uint8Array(16); 60 crypto.getRandomValues(b); 61 b[6] = (b[6] & 0x0f) | 0x40; 62 b[8] = (b[8] & 0x3f) | 0x80; 63 var h = '', i; 64 for (i = 0; i < 16; i++) { 65 h += ('0' + b[i].toString(16)).slice(-2); 66 if (i === 3 || i === 5 || i === 7 || i === 9) h += '-'; 67 } 68 return h; 69 } 70 } catch (e) { /* fallback abaixo */ } 71 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 72 var r = Math.random() * 16 | 0; 73 return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); 74 }); 75 } 76 77 /* ============================================================ 78 Helpers â localStorage com fallback para cookie 79 ============================================================ */ 80 function lsGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } } 81 function lsSet(k, v) { try { localStorage.setItem(k, v); } catch (e) { /* silencioso */ } } 82 83 function cookieGet(n) { 84 var m = document.cookie.match('(?:^|; )' + n + '=([^;]*)'); 85 return m ? decodeURIComponent(m[1]) : null; 86 } 87 function cookieSet(n, v, days) { 88 var d = new Date(); 89 d.setTime(d.getTime() + (days || 30) * 86400000); 90 try { 91 document.cookie = n + '=' + encodeURIComponent(v) + 92 ';path=/;expires=' + d.toUTCString() + ';SameSite=Lax'; 93 } catch (e) { /* silencioso */ } 94 } 95 96 /* ============================================================ 97 Session ID â gera UUID, persiste em localStorage + cookie 98 ============================================================ */ 99 var _sid = null; 100 101 function getSessionId() { 102 if (_sid) return _sid; 103 _sid = lsGet(CFG.sessionKey); 104 if (!_sid) _sid = cookieGet(CFG.sessionKey); 105 if (!_sid) { 106 _sid = uuid(); 107 } 108 lsSet(CFG.sessionKey, _sid); 109 cookieSet(CFG.sessionKey, _sid, 30); 110 return _sid; 111 } 112 113 /* ============================================================ 114 Captura de origem (UTMs) â SOMENTE na primeira visita 115 ============================================================ */ 116 function captureOrigin() { 117 var existing = lsGet(CFG.originKey); 118 if (existing) {
119 try { return JSON.parse(existing); } catch (e) { /* recriar */ } 120 } 121 var params, origin = {}; 122 try { params = new URLSearchParams(window.location.search); } catch (e) { return origin; } 123 ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'fbclid'] 124 .forEach(function (k) { var v = params.get(k); if (v) origin[k] = v; }); 125 origin.referrer = document.referrer || ''; 126 origin.landing_page = window.location.pathname + window.location.search; 127 lsSet(CFG.originKey, JSON.stringify(origin)); 128 return origin; 129 } 130 131 var _origin = captureOrigin(); 132 133 /* ============================================================ 134 Device type 135 ============================================================ */ 136 function deviceType() { 137 var ua = navigator.userAgent || ''; 138 if (/Mobi|Android/i.test(ua)) return 'mobile'; 139 if (/Tablet|iPad/i.test(ua)) return 'tablet'; 140 return 'desktop'; 141 } 142 143 /* ============================================================ 144 One-shot protection â armazena eventos já disparados 145 ============================================================ */ 146 function firedList() { 147 try { var d = lsGet(CFG.firedKey); return d ? JSON.parse(d) : []; } catch (e) { return []; } 148 } 149 function markFired(ev) { 150 try { 151 var f = firedList(); 152 if (f.indexOf(ev) === -1) { f.push(ev); lsSet(CFG.firedKey, JSON.stringify(f)); } 153 } catch (e) { /* silencioso */ } 154 } 155 function hasFired(ev) { return firedList().indexOf(ev) !== -1; } 156 157 /* ============================================================ 158 Event queue â acumula eventos e envia em batches 159 ============================================================ */ 160 var _queue = []; 161 var _flushTimer = null; 162 163 function buildPayload(events) { 164 return { 165 session_id: getSessionId(), 166 events: events, 167 session_data: { 168 utm_source: _origin.utm_source || null, 169 utm_medium: _origin.utm_medium || null, 170 utm_campaign: _origin.utm_campaign || null, 171 utm_content: _origin.utm_content || null, 172 utm_term: _origin.utm_term || null, 173 fbclid: _origin.fbclid || null, 174 referrer: _origin.referrer || null, 175 landing_page: _origin.landing_page || null, 176 device_type: deviceType() 177 } 178 }; 179 } 180 181 function doSend(body, attempt) { 182 try { 183 fetch(CFG.endpoint, { 184 method: 'POST', 185 headers: { 'Content-Type': 'application/json' }, 186 body: body, 187 keepalive: true 188 }).then(function (r) { 189 if (!r.ok && attempt < CFG.maxRetries) { 190 setTimeout(function () { doSend(body, attempt + 1); }, 191 CFG.retryDelays[attempt] || 3000); 192 } 193 }).catch(function () { 194 if (attempt < CFG.maxRetries) { 195 setTimeout(function () { doSend(body, attempt + 1); }, 196 CFG.retryDelays[attempt] || 3000); 197 } 198 /* else: falha silenciosa */ 199 }); 200 } catch (e) { /* nunca quebrar o quiz */ } 201 } 202 203 function flush(useBeacon) { 204 try { 205 if (_flushTimer) { clearTimeout(_flushTimer); _flushTimer = null; } 206 if (!_queue.length || !window.PRIVATE_ANALYTICS_ENABLED) return; 207 208 var batch = _queue.splice(0); 209 var body = JSON.stringify(buildPayload(batch)); 210 211 if (useBeacon && navigator.sendBeacon) { 212 try { 213 var blob = new Blob([body], { type: 'application/json' }); 214 navigator.sendBeacon(CFG.endpoint, blob); 215 return; 216 } catch (e) { /* fallback para fetch */ } 217 } 218 doSend(body, 0); 219 } catch (e) { /* silencioso */ } 220 } 221 222 function scheduleFlush() { 223 if (!_flushTimer) { 224 _flushTimer = setTimeout(function () { flush(false); }, CFG.flushInterval); 225 } 226 } 227 228 /* ============================================================ 229 API pública â send() 230 ============================================================ */ 231 function send(eventName, properties) { 232 try { 233 if (!window.PRIVATE_ANALYTICS_ENABLED) return; 234 235 /* One-shot check */ 236 if (ONE_SHOT.indexOf(eventName) !== -1) { 237 if (hasFired(eventName)) return; 238 markFired(eventName); 239 } 240 241 var ev = { 242 event_id: uuid(), 243 event: eventName, 244 timestamp: new Date().toISOString(), 245 page: window.location.pathname 246 }; 247 248 if (properties && typeof properties === 'object') { 249 var k; 250 for (k in properties) { 251 if (properties.hasOwnProperty(k)) ev[k] = properties[k]; 252 } 253 } 254 255 _queue.push(ev); 256 257 if (_queue.length >= CFG.batchSize) { 258 flush(false); 259 } else { 260 scheduleFlush(); 261 } 262 } catch (e) { /* silencioso */ } 263 } 264 265 /* ============================================================ 266 Page lifecycle â flush antes de sair 267 ============================================================ */ 268 try { 269 document.addEventListener('visibilitychange', function () { 270 if (document.visibilityState === 'hidden') flush(true); 271 }); 272 window.addEventListener('pagehide', function () { flush(true); }); 273 } catch (e) { /* silencioso */ } 274 275 /* ============================================================ 276 Error tracking 277 ============================================================ */ 278 try { 279 var _origOnError = window.onerror; 280 window.onerror = function (msg, src, line, col) {
281 try { 282 if (window.PRIVATE_ANALYTICS_ENABLED) { 283 send('internal_javascript_error', { 284 message: String(msg || '').substring(0, 500), 285 source: String(src || '').substring(0, 200), 286 line: line || null, 287 column: col || null 288 }); 289 } 290 } catch (e) { /* silencioso */ } 291 if (typeof _origOnError === 'function') { 292 return _origOnError.apply(this, arguments); 293 } 294 }; 295 } catch (e) { /* silencioso */ } 296 297 /* ============================================================ 298 Timer helpers â mede durações por etapa 299 ============================================================ */ 300 var _timers = {}; 301 function startTimer(name) { _timers[name] = Date.now(); } 302 function getTimer(name) { return _timers[name] ? Date.now() - _timers[name] : null; } 303 304 /* ============================================================ 305 Exposição da API pública 306 ============================================================ */ 307 window.privateQuizAnalytics = { 308 send: send, 309 flush: function () { flush(false); }, 310 flushBeacon: function () { flush(true); }, 311 getSessionId: getSessionId, 312 getOrigin: function () { var o = {}; for (var k in _origin) { if (_origin.hasOwnProperty(k)) o[k] = _origin[k]; } return o; }, 313 startTimer: startTimer, 314 getTimer: getTimer, 315 hasFired: hasFired, 316 disable: function () { window.PRIVATE_ANALYTICS_ENABLED = false; }, 317 enable: function () { window.PRIVATE_ANALYTICS_ENABLED = true; }, 318 _cfg: CFG 319 }; 320 321 /* Inicializa sessão imediatamente */ 322 getSessionId(); 323 324})();
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.