PageSourceSearch

https://redesurftank.com.br/themes/wc_surf/wc_js/surf-tracking.js?v=1790699681

js redesurftank.com.br collected 2026-10-02 01:18:38 UTC 15,786 bytes, 439 lines download raw bytes

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.