1/** 2 * SurfTank Attribution Tracking 3 * 4 * Captura fingerprint de hardware + UTM params e envia para API de tracking. 5 * O fingerprint é gerado de forma idêntica ao C# do Launcher para match. 6 */ 7(function() { 8 'use strict'; 9 10 const TRACKING_API = '/_cdn/widgets/tracking/tracking.ajax.php'; 11 const COOKIE_NAME = 'surf_tracking'; 12 const COOKIE_DAYS = 3; 13 14 /** 15 * Normaliza nome da GPU para match com C# 16 * Extrai o chip real de wrappers como ANGLE 17 */ 18 function normalizeGpuName(gpu) { 19 if (!gpu) return 'unknown'; 20 21 let normalized = gpu; 22 const upperOriginal = gpu.toUpperCase(); 23 24 // SwiftShader = renderização por software (sem GPU real) 25 if (upperOriginal.includes('SWIFTSHADER')) { 26 return 'SwiftShader'; 27 } 28 29 // Extrai GPU real de dentro do ANGLE wrapper 30 // Formato: ANGLE (Vendor, Renderer Details) 31 // Usa lastIndexOf para pegar o ) final e evitar problemas com parênteses aninhados 32 const angleStart = gpu.indexOf('ANGLE'); 33 if (angleStart !== -1) { 34 const firstComma = gpu.indexOf(',', angleStart); 35 const lastParen = gpu.lastIndexOf(')'); 36 if (firstComma !== -1 && lastParen > firstComma) { 37 normalized = gpu.substring(firstComma + 1, lastParen).trim(); 38 } 39 } 40 41 const upper = normalized.toUpperCase(); 42 43 // NVIDIA: RTX/GTX + modelo 44 const nvidiaMatch = upper.match(/(RTX|GTX)\s*(\d{3,4})(\s*TI)?/); 45 if (nvidiaMatch) { 46 return nvidiaMatch[0].replace(/\s/g, ''); 47 } 48 49 // AMD: RX + modelo (placas discretas) 50 const amdRxMatch = upper.match(/RX\s*(\d{3,4})(\s*XT)?/); 51 if (amdRxMatch) { 52 return amdRxMatch[0].replace(/\s/g, ''); 53 } 54 55 // AMD: Radeon integrado (APU) - sem número RX 56 if (upper.includes('RADEON') && !upper.includes('RX')) { 57 // Tenta extrair modelo Vega se existir 58 const vegaMatch = upper.match(/VEGA\s*(\d+)?/); 59 if (vegaMatch) { 60 return `RadeonVega${vegaMatch[1] || ''}`; 61 } 62 return 'RadeonIntegrated'; 63 } 64 65 // Intel: extrai modelo HD/UHD/Iris 66 const intelMatch = upper.match(/(HD|UHD|IRIS)\s*(GRAPHICS)?\s*(\d+)?/); 67 if (intelMatch && upper.includes('INTEL')) { 68 const type = intelMatch[1]; 69 const model = intelMatch[3] || ''; 70 return `Intel${type}${model}`; 71 } 72 73 // Apple: Apple GPU, Apple M1/M2/M3 74 if (upper.includes('APPLE')) { 75 const appleMatch = upper.match(/APPLE\s*(M\d+)?\s*(PRO|MAX|ULTRA)?/); 76 if (appleMatch && appleMatch[1]) { 77 return `Apple${appleMatch[1]}${appleMatch[2] || ''}`; 78 } 79 return 'AppleGPU'; 80 } 81 82 // Mali (Android) 83 const maliMatch = upper.match(/MALI[- ]?([A-Z]?\d+)/); 84 if (maliMatch) { 85 return `Mali${maliMatch[1]}`; 86 } 87 88 // Adreno (Android) 89 const adrenoMatch = upper.match(/ADRENO.*?(\d+)/); 90 if (adrenoMatch) { 91 return `Adreno${adrenoMatch[1]}`; 92 } 93 94 // PowerVR (iOS older) 95 const powervrMatch = upper.match(/POWERVR\s*([A-Z0-9]+)/); 96 if (powervrMatch) { 97 return `PowerVR${powervrMatch[1]}`; 98 } 99 100 // Vulkan genérico (quando não consegue identificar GPU) 101 if (upper.includes('VULKAN')) { 102 return 'VulkanGeneric'; 103 } 104 105 // Fallback: primeiros 30 chars, remove espaços extras e caracteres problemáticos 106 normalized = normalized.replace(/\s+/g, ' ').replace(/[()]/g, '').trim(); 107 return normalized.length > 30 ? normalized.substring(0, 30) : normalized; 108 } 109 110 /** 111 * Obtém nome da GPU via WebGL 112 */ 113 function getGpuName() { 114 try { 115 const canvas = document.createElement('canvas'); 116 const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); 117 if (!gl) return 'unknown'; 118 119 const debugInfo = gl.getExtension('WEBGL_debug_renderer_info'); 120 if (!debugInfo) return 'unknown'; 121 122 return gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || 'unknown'; 123 } catch (e) { 124 return 'unknown'; 125 } 126 } 127 128 /** 129 * Gera fingerprint de hardware idêntico ao C# 130 * Formato: cpuCores|gpu|screenRes|colorDepth|lang 131 */ 132 function generateFingerprint() { 133 const cpuCores = navigator.hardwareConcurrency || 4; 134 const gpu = normalizeGpuName(getGpuName()); 135 const screenRes = `${screen.width}x${screen.height}`; 136 const colorDepth = screen.colorDepth >= 24 ? 24 : screen.colorDepth; 137 const lang = (navigator.language || 'en').split('-')[0]; 138 139 return `${cpuCores}|${gpu}|${screenRes}|${colorDepth}|${lang}`; 140 } 141 142 var CLICK_ID_KEYS = ['fbclid', 'gclid', 'gbraid', 'wbraid']; 143 var UTM_KEYS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']; 144 145 /** 146 * Sanitiza valor vindo da URL/cookie: remove controle e <>"', limita tamanho. 147 */ 148 function clean(value, max) { 149 if (value === null || value === undefined) return null; 150 var v = String(value).replace(/[\u0000-\u001F\u007F<>"']/g, '').trim(); 151 if (!v) return null; 152 return v.length > max ? v.substring(0, max) : v; 153 } 154 155 /** 156 * Extrai parâmetros UTM e click IDs da URL (click ids <= 256, utm <= 150) 157 */ 158 function getAttributionParams() { 159 var params = new URLSearchParams(window.location.search); 160 var out = {};
161 CLICK_ID_KEYS.forEach(function (k) { out[k] = clean(params.get(k), 256); }); 162 UTM_KEYS.forEach(function (k) { out[k] = clean(params.get(k), 150); }); 163 return out; 164 } 165 166 /** 167 * Obtém cookie por nome 168 */ 169 function getCookie(name) { 170 var value = '; ' + document.cookie; 171 var parts = value.split('; ' + name + '='); 172 if (parts.length === 2) { 173 var raw = parts.pop().split(';').shift(); 174 try { return decodeURIComponent(raw); } catch (e) { return raw; } 175 } 176 return null; 177 } 178 179 /** 180 * Define cookie (Secure em https) 181 */ 182 function setCookie(name, value, days) { 183 var expires = new Date(Date.now() + days * 864e5).toUTCString(); 184 var secure = window.location.protocol === 'https:' ? '; Secure' : ''; 185 document.cookie = name + '=' + encodeURIComponent(value) + '; expires=' + expires + '; path=/; SameSite=Lax' + secure; 186 } 187 188 /** 189 * GA4: client id do cookie _ga (GA1.1.X.Y -> X.Y) 190 */ 191 function getGaClientId() { 192 var ga = getCookie('_ga'); 193 if (!ga) return null; 194 var m = ga.match(/^GA\d\.\d\.(\d+\.\d+)$/); 195 return m ? m[1] : null; 196 } 197 198 /** 199 * GA4: session id do cookie _ga_<container> (GS1.1.<sid>.⦠ou GS2.1.s<sid>$oâ¦) 200 */ 201 function getGaSessionId() { 202 var cookies = document.cookie.split('; '); 203 for (var i = 0; i < cookies.length; i++) { 204 var eq = cookies[i].indexOf('='); 205 if (eq < 0) continue; 206 var name = cookies[i].substring(0, eq); 207 if (name.indexOf('_ga_') !== 0) continue; 208 var val = cookies[i].substring(eq + 1); 209 var m = val.match(/^GS1\.\d\.(\d+)\./) || val.match(/^GS2\.\d\.s(\d+)/); 210 if (m) return m[1];
211 } 212 return null; 213 } 214 215 /** 216 * Sinais de pixel/GA lidos no momento (landing, download). 217 * Sem cookie _fbc e com fbclid na URL, monta fbc = fb.1.{ms}.{fbclid} (formato da Meta). 218 */ 219 function getBrowserSignals(attr) { 220 var fbc = clean(getCookie('_fbc'), 512); 221 var fbcBuilt = false; 222 // So monta fbc com fbclid bem formado 223 if (!fbc && attr && attr.fbclid && /^[A-Za-z0-9_-]{1,256}$/.test(attr.fbclid)) { 224 fbc = 'fb.1.' + Date.now() + '.' + attr.fbclid; 225 fbcBuilt = true; 226 setCookie('_fbc', fbc, 90); 227 } 228 return { 229 fbp: clean(getCookie('_fbp'), 256), 230 fbc: fbc, 231 fbc_built: fbcBuilt ? 1 : 0, 232 ga_client_id: clean(getGaClientId(), 64), 233 ga_session_id: clean(getGaSessionId(), 64) 234 }; 235 } 236 237 function merge(a, b) { 238 var o = {}; 239 Object.keys(a || {}).forEach(function (k) { o[k] = a[k]; }); 240 Object.keys(b || {}).forEach(function (k) { o[k] = b[k]; }); 241 return o; 242 } 243 244 /** 245 * Envia request para API de tracking 246 */ 247 async function sendToApi(action, data) { 248 try { 249 const response = await fetch(TRACKING_API, { 250 method: 'POST', 251 credentials: 'same-origin', 252 keepalive: true, 253 headers: { 254 'Content-Type': 'application/json' 255 }, 256 body: JSON.stringify(merge({ action: action }, data)) 257 }); 258 return await response.json(); 259 } catch (e) { 260 return null; 261 } 262 } 263 264 /** 265 * Inicializa tracking: 266 * - sem sessão: cria em TODA visita (com ou sem parâmetros de campanha); 267 * - com sessão: "touch" â novo click id na URL vira last-click (sessão e click_ts atualizados); 268 * sem clique, só atualiza _fbp/_fbc/_ga se mudaram. 269 */ 270 var initPromise = null; 271 async function initTracking() { 272 var attribution = getAttributionParams(); 273 var signals = getBrowserSignals(attribution); 274 var trackingCode = getCookie(COOKIE_NAME); 275 var landing = window.location.pathname + window.location.search; 276 277 if (trackingCode) { 278 await sendToApi('touch', merge(merge(attribution, signals), { 279 tracking_code: trackingCode, 280 landing_page: landing 281 })); 282 return trackingCode; 283 } 284 285 var result = await sendToApi('session', merge(merge(attribution, signals), { 286 fingerprint: generateFingerprint(), 287 landing_page: landing, 288 referer: document.referrer || null 289 })); 290 291 if (result && result.success && result.tracking_code) { 292 trackingCode = result.tracking_code; 293 setCookie(COOKIE_NAME, trackingCode, COOKIE_DAYS); 294 return trackingCode; 295 } 296 return null; 297 } 298 299 function ensureInit() { 300 if (!initPromise) initPromise = initTracking(); 301 return initPromise; 302 } 303 304 /** 305 * event_id do download: dl_{tracking_code}_{platform} (idêntico ao servidor); null sem sessão. 306 */ 307 function downloadEventId(trackingCode, platform) { 308 return trackingCode ? ('dl_' + trackingCode + '_' + platform) : null; 309 } 310 311 /** 312 * Marca download iniciado 313 */ 314 async function trackDownload(platform) { 315 var resolved = platform || detectPlatform(); 316 var trackingCode = getCookie(COOKIE_NAME); 317 318 // GTM dataLayer: push SÃNCRONO em todo clique (o navegador pode sair da página). 319 // event_id idêntico ao do evento server-side (tracking.ajax.php, action download). 320 window.dataLayer = window.dataLayer || []; 321 window.dataLayer.push({ 322 'event': 'download_initiated', 323 'event_id': downloadEventId(trackingCode, resolved), 324 'platform': resolved, 325 'tracking_code': trackingCode || null 326 }); 327 328 if (!trackingCode) { 329 return false; 330 } 331 332 var result = await sendToApi('download', merge(getBrowserSignals(null), { 333 tracking_code: trackingCode, 334 platform: resolved 335 })); 336 337 return !!(result && result.success); 338 } 339 340 /** 341 * Registra evento customizado 342 */ 343 async function trackEvent(eventName, eventData) { 344 var trackingCode = getCookie(COOKIE_NAME); 345 if (!trackingCode) return false; 346 347 var result = await sendToApi('event', { 348 tracking_code: trackingCode, 349 event_name: eventName, 350 event_data: eventData || {} 351 }); 352 353 return result && result.success; 354 } 355 356 /** 357 * Detecta plataforma do usuário. iPhone/iPad ANTES de mac: o UA do iOS contém "Mac OS X". 358 */ 359 function detectPlatform() { 360 var ua = navigator.userAgent.toLowerCase(); 361 if (ua.indexOf('iphone') !== -1 || ua.indexOf('ipad') !== -1 || ua.indexOf('ipod') !== -1) return 'ios'; 362 // iPadOS 13+ se apresenta como Macintosh com touch 363 if (ua.indexOf('macintosh') !== -1 && navigator.maxTouchPoints > 1) return 'ios'; 364 if (ua.indexOf('android') !== -1) return 'android'; 365 if (ua.indexOf('win') !== -1) return 'windows'; 366 if (ua.indexOf('mac') !== -1) return 'mac'; 367 return 'unknown'; 368 } 369 370 /** 371 * Obtém tracking code atual 372 */ 373 function getTrackingCode() { 374 return getCookie(COOKIE_NAME); 375 } 376 377 /** 378 * Obtém fingerprint (para debug) 379 */ 380 function getFingerprint() { 381 return generateFingerprint(); 382 } 383 384 // Expõe API global 385 window.SurfTracking = { 386 init: ensureInit, 387 trackDownload: trackDownload, 388 trackEvent: trackEvent, 389 getTrackingCode: getTrackingCode, 390 getFingerprint: getFingerprint, 391 detectPlatform: detectPlatform 392 }; 393 394 /** 395 * Bind download tracking via delegação de eventos (funciona com elementos dinâmicos) 396 */ 397 function bindDownloadTracking() { 398 document.addEventListener('click', function(e) { 399 const link = e.target.closest('.surf-download'); 400 if (link) { 401 const platform = link.getAttribute('data-platform') || detectPlatform(); 402 403 // Reescrita SÃNCRONA do href pra Play Store: injeta surf_session no install_referrer 404 // Roda antes do navegador seguir o link, então o referrer chega no app via Google Play 405 if (platform === 'android') { 406 const href = link.getAttribute('href') || ''; 407 if (href.indexOf('play.google.com') !== -1) { 408 const trackingCode = getCookie(COOKIE_NAME); 409 if (trackingCode) { 410 const referrerValue = encodeURIComponent('surf_session=' + trackingCode); 411 let newHref; 412 if (/[?&]referrer=/.test(href)) { 413 newHref = href.replace(/([?&]referrer=)[^&]*/, '$1' + referrerValue); 414 } else if (href.indexOf('?') !== -1) { 415 newHref = href + '&referrer=' + referrerValue; 416 } else { 417 newHref = href + '?referrer=' + referrerValue; 418 } 419 link.setAttribute('href', newHref); 420 } 421 } 422 } 423 424 trackDownload(platform); 425 } 426 }); 427 } 428 429 // Auto-init quando DOM estiver pronto 430 if (document.readyState === 'loading') { 431 document.addEventListener('DOMContentLoaded', ensureInit); 432 } else { 433 ensureInit(); 434 } 435 436 // Bind de eventos sempre (delegação no document) 437 bindDownloadTracking(); 438 439})();
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.