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 /** 143 * Extrai parâmetros UTM e click IDs da URL 144 */ 145 function getAttributionParams() { 146 const params = new URLSearchParams(window.location.search); 147 return { 148 fbclid: params.get('fbclid'), 149 gclid: params.get('gclid'), 150 utm_source: params.get('utm_source'), 151 utm_medium: params.get('utm_medium'), 152 utm_campaign: params.get('utm_campaign'),
153 utm_content: params.get('utm_content'), 154 utm_term: params.get('utm_term') 155 }; 156 } 157 158 /** 159 * Obtém cookie por nome 160 */ 161 function getCookie(name) { 162 const value = `; ${document.cookie}`; 163 const parts = value.split(`; ${name}=`); 164 if (parts.length === 2) return parts.pop().split(';').shift(); 165 return null; 166 } 167 168 /** 169 * Define cookie 170 */ 171 function setCookie(name, value, days) { 172 const expires = new Date(Date.now() + days * 864e5).toUTCString(); 173 document.cookie = `${name}=${value}; expires=${expires}; path=/; SameSite=Lax`; 174 } 175 176 /** 177 * Envia request para API de tracking 178 */ 179 async function sendToApi(action, data) { 180 try { 181 const response = await fetch(TRACKING_API, { 182 method: 'POST', 183 headers: { 184 'Content-Type': 'application/json' 185 }, 186 body: JSON.stringify({ action: action, ...data }) 187 }); 188 return await response.json(); 189 } catch (e) { 190 console.error('[SurfTracking] API error:', e); 191 return null; 192 } 193 } 194 195 /** 196 * Inicializa tracking - cria sessão se necessário 197 */ 198 async function initTracking() { 199 // Se já tem tracking_code, não cria nova sessão 200 let trackingCode = getCookie(COOKIE_NAME); 201 if (trackingCode) { 202 console.log('[SurfTracking] Existing session:', trackingCode.substring(0, 8) + '...'); 203 return trackingCode; 204 } 205 206 // Gera fingerprint e captura attribution 207 const fingerprint = generateFingerprint(); 208 const attribution = getAttributionParams(); 209 210 // Só cria sessão se tiver algum parâmetro de attribution 211 const hasAttribution = Object.values(attribution).some(v => v !== null); 212 213 if (!hasAttribution) { 214 console.log('[SurfTracking] No attribution params, skipping session'); 215 return null; 216 } 217 218 console.log('[SurfTracking] Fingerprint:', fingerprint); 219 console.log('[SurfTracking] Attribution:', attribution); 220 221 // Captura cookies do Meta Pixel (_fbp e _fbc) para matching 222 const fbp = getCookie('_fbp'); 223 const fbc = getCookie('_fbc'); 224 225 // Cria sessão 226 const result = await sendToApi('session', { 227 fingerprint: fingerprint, 228 landing_page: window.location.pathname + window.location.search, 229 referer: document.referrer || null, 230 fbp: fbp || null, 231 fbc: fbc || null, 232 ...attribution 233 }); 234 235 if (result && result.success && result.tracking_code) { 236 trackingCode = result.tracking_code; 237 setCookie(COOKIE_NAME, trackingCode, COOKIE_DAYS); 238 console.log('[SurfTracking] Session created:', trackingCode.substring(0, 8) + '...'); 239 return trackingCode; 240 } 241 242 console.warn('[SurfTracking] Failed to create session'); 243 return null; 244 } 245 246 /** 247 * Marca download iniciado 248 */ 249 async function trackDownload(platform) { 250 const resolved = platform || detectPlatform(); 251 const trackingCode = getCookie(COOKIE_NAME); 252 253 // GTM dataLayer: empurra download_initiated pra TODOS os cliques (anônimos ou com sessão). 254 // O event_id server-side correspondente é `dl_{tracking_code}_{platform}`. 255 window.dataLayer = window.dataLayer || []; 256 window.dataLayer.push({ 257 'event': 'download_initiated', 258 'platform': resolved, 259 'tracking_code': trackingCode || null 260 }); 261 262 if (!trackingCode) { 263 console.warn('[SurfTracking] No tracking session for download'); 264 return false; 265 } 266 267 const result = await sendToApi('download', { 268 tracking_code: trackingCode, 269 platform: resolved 270 }); 271 272 if (result && result.success) { 273 console.log('[SurfTracking] Download tracked:', resolved); 274 return true; 275 } 276 return false;
277 } 278 279 /** 280 * Registra evento customizado 281 */ 282 async function trackEvent(eventName, eventData) { 283 const trackingCode = getCookie(COOKIE_NAME); 284 if (!trackingCode) return false; 285 286 const result = await sendToApi('event', { 287 tracking_code: trackingCode, 288 event_name: eventName, 289 event_data: eventData || {} 290 }); 291 292 return result && result.success; 293 } 294 295 /** 296 * Detecta plataforma do usuário 297 */ 298 function detectPlatform() { 299 const ua = navigator.userAgent.toLowerCase(); 300 if (ua.includes('win')) return 'windows'; 301 if (ua.includes('mac')) return 'mac'; 302 if (ua.includes('iphone') || ua.includes('ipad')) return 'ios'; 303 if (ua.includes('android')) return 'android'; 304 return 'unknown'; 305 } 306 307 /** 308 * Obtém tracking code atual 309 */ 310 function getTrackingCode() { 311 return getCookie(COOKIE_NAME); 312 } 313 314 /** 315 * Obtém fingerprint (para debug) 316 */ 317 function getFingerprint() { 318 return generateFingerprint(); 319 } 320 321 // Expõe API global 322 window.SurfTracking = { 323 init: initTracking, 324 trackDownload: trackDownload, 325 trackEvent: trackEvent, 326 getTrackingCode: getTrackingCode, 327 getFingerprint: getFingerprint, 328 detectPlatform: detectPlatform 329 }; 330 331 /** 332 * Bind download tracking via delegação de eventos (funciona com elementos dinâmicos) 333 */ 334 function bindDownloadTracking() { 335 document.addEventListener('click', function(e) { 336 const link = e.target.closest('.surf-download'); 337 if (link) { 338 const platform = link.getAttribute('data-platform') || detectPlatform(); 339 340 // Reescrita SÃNCRONA do href pra Play Store: injeta surf_session no install_referrer 341 // Roda antes do navegador seguir o link, então o referrer chega no app via Google Play 342 if (platform === 'android') { 343 const href = link.getAttribute('href') || ''; 344 if (href.indexOf('play.google.com') !== -1) { 345 const trackingCode = getCookie(COOKIE_NAME); 346 if (trackingCode) { 347 const referrerValue = encodeURIComponent('surf_session=' + trackingCode); 348 let newHref; 349 if (/[?&]referrer=/.test(href)) { 350 newHref = href.replace(/([?&]referrer=)[^&]*/, '$1' + referrerValue); 351 } else if (href.indexOf('?') !== -1) { 352 newHref = href + '&referrer=' + referrerValue; 353 } else { 354 newHref = href + '?referrer=' + referrerValue; 355 } 356 link.setAttribute('href', newHref); 357 } 358 } 359 } 360 361 trackDownload(platform); 362 } 363 }); 364 } 365 366 // Auto-init quando DOM estiver pronto 367 if (document.readyState === 'loading') { 368 document.addEventListener('DOMContentLoaded', initTracking); 369 } else { 370 initTracking(); 371 } 372 373 // Bind de eventos sempre (delegação no document) 374 bindDownloadTracking(); 375 376})();
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.