/** * Flow Tracking - Tracker Unificado v2.0.0 * Versão estável com versionamento * Combina scroll tracking especializado com tracking geral */ (function() { 'use strict'; // Estado global do tracker const trackerState = { initialized: false, appId: null, userId: null, sessionId: null, /** true quando session_id veio da URL (funil Bridge → VSL) */ journeyContinuedFromUrl: false, deviceInfo: null, geoData: null, maxScrollDepth: 0, pageStartTime: Date.now(), scrollTracker: null, debug: false, /** Evita múltiplos UE page_exit no mesmo descarregamento (pagehide + beforeunload). */ pageExitSent: false }; // Configuração padrão const DEFAULT_CONFIG = { appId: null, // Será definido pelo trackId do usuário trackerName: 'flowtrk', platform: 'web', debug: false, scrollConfig: { milestones: [10, 25, 50, 75, 90, 100], debounceMs: 100, throttleMs: 16, debug: false, precision: 1, minScrollDistance: 10, maxIdleTime: 30000 } }; // ======================================== // SCROLL TRACKER COM SCROLLAMA // ======================================== class ScrollTracker { constructor(config = {}) { this.config = { ...DEFAULT_CONFIG.scrollConfig, ...config }; this.state = { currentScroll: 0, maxScroll: 0, trackedMilestones: new Set(), isScrolling: false, lastScrollTime: 0, scrollDirection: 'down', velocity: 0, acceleration: 0, idleTimer: null, isIdle: false }; this.listeners = []; this.scroller = null; this.lastScrollY = 0; this.lastVelocity = 0; this.lastTime = 0; this.init(); } init() { this.log('ScrollTracker com Scrollama inicializado'); this.loadScrollama(); } async loadScrollama() { try { // Carregar Scrollama do CDN if (typeof window.scrollama === 'undefined') { this.log('Carregando Scrollama do CDN...'); const script = document.createElement('script'); script.src = 'https://unpkg.com/scrollama@3.2.0/build/scrollama.js'; script.async = true; script.onload = () => { this.log('Scrollama carregado com sucesso!'); // Aguardar um pouco para garantir que o script foi processado setTimeout(() => { if (typeof window.scrollama !== 'undefined') { this.log('Scrollama confirmado disponível após carregamento'); this.setupScrollama(); } else { this.log('Scrollama não disponível após carregamento, usando fallback'); this.fallbackScrollTracking(); } }, 200); }; script.onerror = (error) => { this.log('Erro ao carregar Scrollama:', error); this.fallbackScrollTracking(); }; document.head.appendChild(script); } else { this.log('Scrollama já disponível'); this.setupScrollama(); } } catch (error) { this.log('Erro ao carregar Scrollama:', error); this.fallbackScrollTracking(); } } setupScrollama() { this.log('Configurando Scrollama...'); // Verificar se Scrollama está disponível if (typeof window.scrollama === 'undefined') { this.log('Scrollama não está disponível, usando fallback'); this.fallbackScrollTracking(); return; } // Criar elementos de milestone se não existirem this.createMilestoneElements(); try { // Inicializar Scrollama this.scroller = window.scrollama(); this.log('Scrollama instanciado com sucesso'); this.scroller .setup({ step: '.scroll-milestone', offset: 0.5, debug: this.config.debug }) .onStepEnter((response) => { const { element, index, direction } = response; const milestone = parseInt(element.dataset.milestone); this.log(`Scrollama: Marco ${milestone}% enter (${direction})`); this.handleMilestone(milestone, 'enter', direction); }) .onStepExit((response) => { const { element, index, direction } = response; const milestone = parseInt(element.dataset.milestone); this.log(`Scrollama: Marco ${milestone}% exit (${direction})`); this.handleMilestone(milestone, 'exit', direction); }); // Ajustar em caso de redimensionamento window.addEventListener('resize', () => { if (this.scroller) { this.scroller.resize(); } }); // Fallback para scroll tradicional (sempre ativo como backup) this.bindEvents(); this.startIdleTimer(); this.updateScrollData(); this.log('Scrollama configurado com sucesso'); } catch (error) { this.log('Erro ao configurar Scrollama:', error); this.fallbackScrollTracking(); } } createMilestoneElements() { // Criar elementos invisíveis para cada milestone this.config.milestones.forEach(milestone => { const element = document.createElement('div'); element.className = 'scroll-milestone'; element.dataset.milestone = milestone; element.style.cssText = ` position: absolute; top: ${milestone}%; left: 0; width: 1px; height: 1px; pointer-events: none; opacity: 0; `; document.body.appendChild(element); }); } handleMilestone(milestone, action, direction) { this.log(`🎯 Marco ${milestone}% ${action} (${direction})`); if (action === 'enter' && !this.state.trackedMilestones.has(milestone)) { this.state.trackedMilestones.add(milestone); this.state.maxScroll = Math.max(this.state.maxScroll, milestone); // Atualizar maxScrollDepth no trackerState global if (typeof trackerState !== 'undefined' && trackerState.scrollTracker) { if (milestone > trackerState.maxScrollDepth) { trackerState.maxScrollDepth = milestone; } } // Notificar listeners this.notifyListeners('milestone', { milestone, action, direction, scrollData: this.getScrollData() }); } } fallbackScrollTracking() { this.log('Usando scroll tracking tradicional como fallback'); this.bindEvents(); this.startIdleTimer(); this.updateScrollData(); } bindEvents() { // Scroll com throttling para performance let scrollTimeout; const throttledScroll = this.throttle(() => { this.handleScroll(); }, this.config.throttleMs); window.addEventListener('scroll', () => { if (scrollTimeout) { clearTimeout(scrollTimeout); } scrollTimeout = setTimeout(throttledScroll, this.config.debounceMs); }, { passive: true }); // Resize para recalcular dimensões window.addEventListener('resize', this.debounce(() => { this.updateScrollData(); }, 250), { passive: true }); // Visibility change para detectar quando usuário volta à aba document.addEventListener('visibilitychange', () => { if (!document.hidden) { this.updateScrollData(); this.startIdleTimer(); } }); // Beforeunload para capturar scroll final window.addEventListener('beforeunload', () => { this.handleScroll(); }); } handleScroll() { const now = Date.now(); const scrollY = this.getScrollY(); const deltaY = scrollY - this.lastScrollY; const deltaTime = now - this.lastTime; // Debug: log do scroll if (this.config.debug) { this.log('🔄 Scroll detectado:', { scrollY: scrollY, deltaY: deltaY, maxScroll: this.state.maxScroll }); } // Calcular velocidade e aceleração if (deltaTime > 0) { this.state.velocity = deltaY / deltaTime; this.state.acceleration = (this.state.velocity - this.lastVelocity) / deltaTime; } // Atualizar direção this.state.scrollDirection = deltaY > 0 ? 'down' : 'up'; // Atualizar estado this.state.currentScroll = scrollY; this.state.lastScrollTime = now; this.state.isScrolling = true; // Atualizar máximo scroll if (scrollY > this.state.maxScroll) { this.state.maxScroll = scrollY; } // Atualizar maxScrollDepth no trackerState global if (typeof trackerState !== 'undefined' && trackerState.scrollTracker) { const scrollData = this.getScrollData(); if (scrollData.maxScrollPercent > trackerState.maxScrollDepth) { trackerState.maxScrollDepth = scrollData.maxScrollPercent; } } // Verificar marcos de scroll this.checkMilestones(); // Notificar listeners this.notifyListeners('scroll', this.getScrollData()); // Reiniciar timer de idle this.startIdleTimer(); // Atualizar valores para próxima iteração this.lastScrollY = scrollY; this.lastVelocity = this.state.velocity; this.lastTime = now; } getScrollY() { return Math.max( window.pageYOffset || 0, document.documentElement.scrollTop || 0, document.body.scrollTop || 0 ); } getScrollData() { const scrollY = this.state.currentScroll; const windowHeight = window.innerHeight; const documentHeight = Math.max( document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight ); const scrollableHeight = Math.max(0, documentHeight - windowHeight); // Calcular percentual com precisão const scrollPercent = scrollableHeight > 0 ? Math.min(100, Math.max(0, Math.round((scrollY / scrollableHeight) * 100))) : 0; return { scrollY: scrollY, scrollPercent: scrollPercent, maxScroll: this.state.maxScroll, maxScrollPercent: scrollableHeight > 0 ? Math.min(100, Math.max(0, Math.round((this.state.maxScroll / scrollableHeight) * 100))) : 0, windowHeight: windowHeight, documentHeight: documentHeight, scrollableHeight: scrollableHeight, direction: this.state.scrollDirection, velocity: this.state.velocity, acceleration: this.state.acceleration, isScrolling: this.state.isScrolling, isIdle: this.state.isIdle, trackedMilestones: Array.from(this.state.trackedMilestones).sort((a, b) => a - b), timestamp: Date.now() }; } checkMilestones() { const scrollData = this.getScrollData(); const currentPercent = scrollData.scrollPercent; this.config.milestones.forEach(milestone => { if (currentPercent >= milestone && !this.state.trackedMilestones.has(milestone)) { this.state.trackedMilestones.add(milestone); this.log(`🎯 Marco de scroll atingido: ${milestone}%`); // Notificar listeners this.notifyListeners('milestone', { milestone, scrollData }); } }); } updateScrollData() { const scrollData = this.getScrollData(); this.state.currentScroll = scrollData.scrollY; // Atualizar maxScroll se necessário if (scrollData.scrollY > this.state.maxScroll) { this.state.maxScroll = scrollData.scrollY; } // Atualizar maxScrollDepth no trackerState global if (typeof trackerState !== 'undefined' && trackerState.scrollTracker) { if (scrollData.maxScrollPercent > trackerState.maxScrollDepth) { trackerState.maxScrollDepth = scrollData.maxScrollPercent; } } this.log('📊 Dados de scroll atualizados:', scrollData); } startIdleTimer() { if (this.state.idleTimer) { clearTimeout(this.state.idleTimer); } this.state.idleTimer = setTimeout(() => { this.state.isIdle = true; this.state.isScrolling = false; this.log('😴 Usuário inativo - scroll pausado'); this.notifyListeners('idle', this.getScrollData()); }, this.config.maxIdleTime); } // Utilitários throttle(func, limit) { let inThrottle; return function() { const args = arguments; const context = this; if (!inThrottle) { func.apply(context, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } debounce(func, wait) { let timeout; return function() { const context = this; const args = arguments; clearTimeout(timeout); timeout = setTimeout(() => func.apply(context, args), wait); }; } // API pública getCurrentScroll() { return this.getScrollData(); } getMaxScroll() { return this.state.maxScroll; } getTrackedMilestones() { return Array.from(this.state.trackedMilestones).sort((a, b) => a - b); } reset() { this.state.trackedMilestones.clear(); this.state.maxScroll = 0; this.state.currentScroll = 0; this.log('🔄 Scroll tracker resetado'); } // Sistema de listeners on(event, callback) { if (!this.listeners[event]) { this.listeners[event] = []; } this.listeners[event].push(callback); } off(event, callback) { if (this.listeners[event]) { this.listeners[event] = this.listeners[event].filter(cb => cb !== callback); } } notifyListeners(event, data) { if (this.listeners[event]) { this.listeners[event].forEach(callback => { try { callback(data); } catch (error) { console.error('Erro no listener de scroll:', error); } }); } } log(...args) { if (this.config.debug) { console.log('[ScrollTracker]', ...args); } } destroy() { // Remover event listeners window.removeEventListener('scroll', this.handleScroll); window.removeEventListener('resize', this.updateScrollData); document.removeEventListener('visibilitychange', this.handleVisibilityChange); // Limpar timers if (this.state.idleTimer) { clearTimeout(this.state.idleTimer); } if (this.rafId) { cancelAnimationFrame(this.rafId); } this.log('🗑️ ScrollTracker destruído'); } } // ======================================== // FLOW TRACKING PRINCIPAL // ======================================== // Função principal de inicialização function initializeFlowTracking(config = {}) { if (trackerState.initialized) { console.log('Flow Tracking já inicializado'); return; } const finalConfig = { ...DEFAULT_CONFIG, ...config }; trackerState.debug = finalConfig.debug; trackerState.appId = finalConfig.appId; log('🚀 Inicializando Flow Tracking v1.0.0'); log('📱 App ID configurado:', trackerState.appId); // 1. Configurar dados básicos setupBasicData(); // 2. Configurar informações do dispositivo setupDeviceInfo(); // 3. Configurar geolocalização // setupGeolocation(); // COMENTADO: Desabilitado para não pedir localização trackerState.geoData = { source: 'ip' }; // 4. Inicializar scroll tracker especializado initializeScrollTracker(finalConfig.scrollConfig); // 5. Configurar tracking de tempo setupTimeTracking(); // 6. Configurar tracking de engajamento setupEngagementTracking(); // 7. Configurar tracking de página setupPageTracking(); // 8. Propagação de jornada em funil (Bridge → VSL) setupFunnelLinkPropagation(); trackerState.initialized = true; log('✅ Flow Tracking inicializado com sucesso'); } /** Mesmo padrão do loader v2: ftsession_{uuid} (backend + checkout leem este formato). */ const FT_SESSION_PREFIX = 'ftsession_'; const FLOW_SESSION_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; function parseFlowWindowName(raw) { if (!raw) return null; const n = String(raw).trim(); if (n.indexOf(FT_SESSION_PREFIX) !== 0) return null; const id = n.slice(FT_SESSION_PREFIX.length); return FLOW_SESSION_UUID_RE.test(id) ? id : null; } function syncFlowWindowName(sessionId) { const sid = sessionId != null ? String(sessionId).trim() : ''; if (!sid || !FLOW_SESSION_UUID_RE.test(sid)) return null; const built = FT_SESSION_PREFIX + sid; try { window.name = built; } catch (_e) {} return built; } // Configurar dados básicos function setupBasicData() { trackerState.userId = getOrCreateUserId(); trackerState.sessionId = getOrCreateSessionId(); trackerState.pageStartTime = Date.now(); const flowWindowName = syncFlowWindowName(trackerState.sessionId); log('👤 Dados básicos configurados:', { userId: trackerState.userId, sessionId: trackerState.sessionId, journeyContinuedFromUrl: trackerState.journeyContinuedFromUrl, windowName: flowWindowName }); } /** * Propagação de jornada em funil (Bridge/Pre-sell → VSL → Checkout). * Adiciona ft_sid em todos os links (mesmo origin ou cross-origin) para o tracking * tratar o fluxo como uma única sessão ao salvar eventos. */ function setupFunnelLinkPropagation() { document.addEventListener('click', function(e) { const a = e.target && e.target.closest ? e.target.closest('a') : null; if (!a || !a.href) return; try { const targetUrl = new URL(a.href); if (targetUrl.protocol !== 'http:' && targetUrl.protocol !== 'https:') return; if (targetUrl.searchParams.get('ft_sid') || targetUrl.searchParams.get('ft_journey')) return; if (!trackerState.sessionId) return; targetUrl.searchParams.set('ft_sid', trackerState.sessionId); a.href = targetUrl.toString(); log('🔗 Funil: ft_sid adicionado ao link (mesma origem ou cross-origin)', targetUrl.href); } catch (err) { log('🔗 Funil: erro ao propagar ft_sid:', err); } }, true); log('✅ Propagação de jornada em funil (ft_sid em todos os links) configurada'); } // Configurar informações do dispositivo function setupDeviceInfo() { const userAgent = navigator.userAgent; const platform = navigator.platform; const language = navigator.language; const cookieEnabled = navigator.cookieEnabled; const onLine = navigator.onLine; // Detectar tipo de dispositivo const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent); const isTablet = /iPad|Android(?!.*Mobile)/i.test(userAgent); const deviceType = isMobile ? 'Mobile' : isTablet ? 'Tablet' : 'Desktop'; // Detectar sistema operacional let os = 'Unknown'; if (userAgent.indexOf('Windows') !== -1) os = 'Windows'; else if (userAgent.indexOf('Mac') !== -1) os = 'macOS'; else if (userAgent.indexOf('Linux') !== -1) os = 'Linux'; else if (userAgent.indexOf('Android') !== -1) os = 'Android'; else if (userAgent.indexOf('iOS') !== -1) os = 'iOS'; // Detectar navegador let browser = 'Unknown'; if (userAgent.indexOf('Chrome') !== -1) browser = 'Chrome'; else if (userAgent.indexOf('Firefox') !== -1) browser = 'Firefox'; else if (userAgent.indexOf('Safari') !== -1) browser = 'Safari'; else if (userAgent.indexOf('Edge') !== -1) browser = 'Edge'; trackerState.deviceInfo = { userAgent: userAgent, platform: platform, language: language, cookieEnabled: cookieEnabled, onLine: onLine, deviceType: deviceType, os: os, browser: browser, screen: { width: screen.width, height: screen.height, availWidth: screen.availWidth, availHeight: screen.availHeight, colorDepth: screen.colorDepth, pixelDepth: screen.pixelDepth }, viewport: { width: window.innerWidth, height: window.innerHeight } }; log('📱 Informações do dispositivo:', trackerState.deviceInfo); } // Configurar geolocalização // COMENTADO: Desabilitado para não pedir localização do usuário function setupGeolocation() { // if (navigator.geolocation) { // navigator.geolocation.getCurrentPosition( // function(position) { // trackerState.geoData = { // latitude: position.coords.latitude, // longitude: position.coords.longitude, // accuracy: position.coords.accuracy, // source: 'browser' // }; // log('📍 Geolocalização obtida:', trackerState.geoData); // }, // function(error) { // log('📍 Geolocalização não disponível:', error.message); // trackerState.geoData = { source: 'ip' }; // } // ); // } else { // trackerState.geoData = { source: 'ip' }; // } trackerState.geoData = { source: 'ip' }; } // Inicializar scroll tracker especializado function initializeScrollTracker(scrollConfig) { const config = { ...scrollConfig, debug: trackerState.debug }; trackerState.scrollTracker = new ScrollTracker(config); // Configurar callbacks após criação trackerState.scrollTracker.on('scroll', handleScrollEvent); trackerState.scrollTracker.on('milestone', handleScrollMilestone); trackerState.scrollTracker.on('idle', handleScrollIdle); log('📊 Scroll Tracker especializado inicializado'); // Debug: verificar dados iniciais if (trackerState.debug) { setTimeout(() => { const scrollData = trackerState.scrollTracker.getCurrentScroll(); log('📊 Dados iniciais do scroll:', scrollData); }, 1000); } } // Handler para eventos de scroll function handleScrollEvent(scrollData) { if (trackerState.debug) { log('📊 Scroll event:', { percent: scrollData.scrollPercent, maxPercent: scrollData.maxScrollPercent, direction: scrollData.direction, velocity: scrollData.velocity }); } // Atualizar maxScrollDepth no estado if (scrollData.scrollPercent > trackerState.maxScrollDepth) { trackerState.maxScrollDepth = scrollData.scrollPercent; } } // Handler para marcos de scroll function handleScrollMilestone(milestoneData) { // Extrair dados corretamente let milestone, scrollData; if (typeof milestoneData === 'object' && milestoneData !== null) { milestone = milestoneData.milestone || milestoneData.percentage; scrollData = milestoneData.scrollData || milestoneData; } else { milestone = milestoneData; scrollData = trackerState.scrollTracker ? trackerState.scrollTracker.getScrollData() : {}; } log(`🎯 Marco de scroll atingido: ${milestone}%`); // Enviar evento de scroll depth trackScrollDepthEvent(milestone, scrollData); } // Handler para scroll idle function handleScrollIdle(scrollData) { log('😴 Usuário inativo - scroll pausado'); // Enviar evento de engajamento trackEngagementEvent('scroll_idle', scrollData); } // Configurar tracking de tempo function setupTimeTracking() { // Enviar evento de tempo a cada 30 segundos setInterval(() => { const timeOnPage = Date.now() - trackerState.pageStartTime; const scrollData = trackerState.scrollTracker ? trackerState.scrollTracker.getCurrentScroll() : null; trackTimeUpdateEvent(timeOnPage, scrollData); }, 30000); } // Configurar tracking de engajamento function setupEngagementTracking() { log('🎯 Configurando tracking de engajamento...'); let lastClickSignature = null; let lastClickAt = 0; const CLICK_DEDUP_WINDOW_MS = 700; /** Ignora query/hash na dedup — subids injetados entre pointerdown e click não geram segundo evento. */ function hrefForDedup(href) { if (!href) return ''; try { const u = new URL(href, window.location.href); return u.origin + u.pathname; } catch { return String(href).split('?')[0].split('#')[0]; } } /** Elemento usado no evento + se veio de um alvo interativo (link/botão), alinhado ao agregado `button_clicks`. */ function getClickTargetInfo(event) { if (!event || !event.target) return null; const sel = 'a,button,[role="button"],input[type="button"],input[type="submit"]'; let el = null; let isInteractive = false; if (typeof event.target.closest === 'function') { el = event.target.closest(sel); } if (el) { isInteractive = true; } else { el = event.target; } return { element: el, isInteractive: isInteractive }; } function trackClick(event, source) { const info = getClickTargetInfo(event); if (!info || !info.element) return; const element = info.element; const tagName = (element.tagName || '').toUpperCase(); const className = element.className || ''; const id = element.id || ''; const href = element.href || null; const clickSignature = `${tagName}|${id}|${className}|${hrefForDedup(href)}`; const now = Date.now(); if (lastClickSignature === clickSignature && (now - lastClickAt) <= CLICK_DEDUP_WINDOW_MS) { return; } lastClickSignature = clickSignature; lastClickAt = now; log('🖱️ Clique detectado:', { target: tagName, className, id, source, href, isInteractive: info.isInteractive }); // keepalive aumenta a chance do evento sobreviver em cliques com redirecionamento. trackEngagementEvent('click', { target: tagName, className: className, id: id, href: href, source: source, isInteractive: info.isInteractive }, { keepalive: true }); } // Captura no pointerdown envia o evento antes da navegação acontecer. document.addEventListener('pointerdown', (event) => { trackClick(event, 'pointerdown'); }, true); // Mantém listener de click para casos sem pointer events/teclado. document.addEventListener('click', (event) => { trackClick(event, 'click'); }, true); // Detectar teclas pressionadas document.addEventListener('keydown', (event) => { log('⌨️ Tecla pressionada:', { key: event.key, code: event.code }); trackEngagementEvent('keydown', { key: event.key, code: event.code }); }); log('✅ Tracking de engajamento configurado'); } // Configurar tracking de página function setupPageTracking() { log('📄 Configurando tracking de página...'); // Enviar page_view imediatamente log('📄 Enviando page_view event...'); trackPageViewEvent(); // Detectar mudanças de rota (SPA) let currentPath = window.location.pathname; const pathObserver = new MutationObserver(() => { if (window.location.pathname !== currentPath) { currentPath = window.location.pathname; trackPathViewEvent(); } }); // Observar mudanças no DOM que podem indicar mudança de rota pathObserver.observe(document.body, { childList: true, subtree: true }); // Saída por navegação / fecho do separador window.addEventListener('pagehide', () => { trackPageExitEvent(); }); window.addEventListener('beforeunload', () => { trackPageExitEvent(); }); // page_exit quando a página deixa de estar visível (outra aba, minimizar, etc.) document.addEventListener('visibilitychange', () => { if (document.hidden) { trackPageExitEvent(); } else { trackerState.pageExitSent = false; } }); log('✅ Tracking de página configurado'); } // Função para trackear evento de scroll depth function trackScrollDepthEvent(percentage, scrollData) { const event = { e: 'ue', eid: generateUUID(), aid: trackerState.appId || 'flow-tracking-cdn', tna: 'flowtrk', uid: trackerState.userId, sid: trackerState.sessionId, url: window.location.href, dtm: Date.now(), tv: 'js-1.0.0', p: 'web', ue_pr: btoa(unescape(encodeURIComponent(JSON.stringify({ schema: 'iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0', data: { schema: 'iglu:com.flowtracking/scroll_depth/jsonschema/1-0-0', data: { percentage: percentage, maxScrollDepth: trackerState.maxScrollDepth, timeOnPage: Date.now() - trackerState.pageStartTime, url: window.location.href, deviceInfo: trackerState.deviceInfo, scrollData: scrollData, geoData: trackerState.geoData } } })))) }; sendEvent(event); log(`📤 Scroll depth event enviado: ${percentage}%`); } // Função para trackear evento de engajamento function trackEngagementEvent(type, data, options = {}) { log(`📤 Preparando evento de engajamento: ${type}`, data); // Garantir que maxScrollDepth está atualizado if (trackerState.scrollTracker) { const scrollData = trackerState.scrollTracker.getScrollData(); if (scrollData.maxScrollPercent > trackerState.maxScrollDepth) { trackerState.maxScrollDepth = scrollData.maxScrollPercent; } } const event = { e: 'ue', eid: generateUUID(), aid: trackerState.appId || 'flow-tracking-cdn', tna: 'flowtrk', uid: trackerState.userId, sid: trackerState.sessionId, url: window.location.href, dtm: Date.now(), tv: 'js-1.0.0', p: 'web', ue_pr: btoa(JSON.stringify({ schema: 'iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0', data: { schema: 'iglu:com.flowtracking/engagement/jsonschema/1-0-0', data: { type: type, timeOnPage: Date.now() - trackerState.pageStartTime, maxScrollDepth: trackerState.maxScrollDepth, url: window.location.href, deviceInfo: trackerState.deviceInfo, data: data, geoData: trackerState.geoData } } })) }; sendEvent(event, options); log(`📤 Engagement event enviado: ${type}`, { maxScrollDepth: trackerState.maxScrollDepth }); } // Função para trackear atualização de tempo function trackTimeUpdateEvent(timeOnPage, scrollData) { const event = { e: 'ue', eid: generateUUID(), aid: trackerState.appId || 'flow-tracking-cdn', tna: 'flowtrk', uid: trackerState.userId, sid: trackerState.sessionId, url: window.location.href, dtm: Date.now(), tv: 'js-1.0.0', p: 'web', ue_pr: btoa(unescape(encodeURIComponent(JSON.stringify({ schema: 'iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0', data: { schema: 'iglu:com.flowtracking/time_update/jsonschema/1-0-0', data: { timeOnPage: timeOnPage, maxScrollDepth: trackerState.maxScrollDepth, currentScrollPercent: scrollData ? scrollData.scrollPercent : 0, url: window.location.href, deviceInfo: trackerState.deviceInfo, geoData: trackerState.geoData } } })))) }; sendEvent(event); log(`📤 Time update event enviado: ${timeOnPage}ms`); } // Função para trackear evento de page view function trackPageViewEvent() { trackerState.pageExitSent = false; const event = { e: 'ue', eid: generateUUID(), aid: trackerState.appId || 'flow-tracking-cdn', tna: 'flowtrk', uid: trackerState.userId, sid: trackerState.sessionId, url: window.location.href, dtm: Date.now(), tv: 'js-1.0.0', p: 'web', ue_pr: btoa(unescape(encodeURIComponent(JSON.stringify({ schema: 'iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0', data: { schema: 'iglu:com.flowtracking/page_view/jsonschema/1-0-0', data: { pageTitle: document.title, referrer: document.referrer, url: window.location.href, path: window.location.pathname, search: window.location.search, hash: window.location.hash, deviceInfo: trackerState.deviceInfo, geoData: trackerState.geoData } } })))) }; if (trackerState.journeyContinuedFromUrl) { event.ft_journey_continued = '1'; } sendEvent(event); log('📤 Page view event enviado'); console.log('🔍 PAGE VIEW EVENT ENVIADO:', event); } // Função para trackear evento de path view function trackPathViewEvent() { trackerState.pageExitSent = false; const event = { e: 'ue', eid: generateUUID(), aid: trackerState.appId || 'flow-tracking-cdn', tna: 'flowtrk', uid: trackerState.userId, sid: trackerState.sessionId, url: window.location.href, dtm: Date.now(), tv: 'js-1.0.0', p: 'web', ue_pr: btoa(unescape(encodeURIComponent(JSON.stringify({ schema: 'iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0', data: { schema: 'iglu:com.flowtracking/path_view/jsonschema/1-0-0', data: { pageTitle: document.title, referrer: document.referrer, url: window.location.href, path: window.location.pathname, search: window.location.search, hash: window.location.hash, timeOnPage: Date.now() - trackerState.pageStartTime, maxScrollDepth: trackerState.maxScrollDepth, deviceInfo: trackerState.deviceInfo, geoData: trackerState.geoData } } })))) }; sendEvent(event); log('📤 Path view event enviado'); } // Função para trackear evento de page exit function trackPageExitEvent() { if (trackerState.pageExitSent) return; trackerState.pageExitSent = true; const event = { e: 'ue', eid: generateUUID(), aid: trackerState.appId || 'flow-tracking-cdn', tna: 'flowtrk', uid: trackerState.userId, sid: trackerState.sessionId, url: window.location.href, dtm: Date.now(), tv: 'js-1.0.0', p: 'web', ue_pr: btoa(unescape(encodeURIComponent(JSON.stringify({ schema: 'iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0', data: { schema: 'iglu:com.flowtracking/page_exit/jsonschema/1-0-0', data: { pageTitle: document.title, url: window.location.href, path: window.location.pathname, timeOnPage: Date.now() - trackerState.pageStartTime, maxScrollDepth: trackerState.maxScrollDepth, deviceInfo: trackerState.deviceInfo, geoData: trackerState.geoData } } })))) }; sendEvent(event); log('📤 Page exit event enviado'); } // Função para enviar eventos function sendEvent(event, options = {}) { const params = new URLSearchParams(); Object.keys(event).forEach(key => { if (event[key] !== null && event[key] !== undefined) { params.append(key, event[key]); } }); // Determinar endpoint da API de forma dinâmica const apiBaseFromConfig = (window.FlowTrackingConfig && window.FlowTrackingConfig.apiBase) || null; const apiBaseFromWindow = window.FLOW_TRACKING_API_URL || null; const apiBase = apiBaseFromConfig || apiBaseFromWindow || 'https://api.flowtrackingtool.com'; const url = `${apiBase}/i?${params.toString()}`; if (trackerState.debug) { log('📤 Enviando evento para:', url); } // Usar fetch com modo cors para ver erros if (typeof fetch !== 'undefined') { fetch(url, { method: 'GET', mode: 'cors', credentials: 'omit', keepalive: Boolean(options.keepalive) }) .then(response => { if (trackerState.debug) { log('📤 Resposta do servidor:', response.status, response.statusText); } if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } }) .catch(error => { log('❌ Erro ao enviar evento:', error.message); // Fallback para img const img = new Image(); img.src = url; if (trackerState.debug) { log('📤 Fallback para img:', url); } }); } else { const img = new Image(); img.src = url; if (trackerState.debug) { log('📤 Enviando via img:', url); } } } // Utilitários function getOrCreateUserId() { let userId = localStorage.getItem('flowtrk_user_id'); if (!userId) { userId = generateUUID(); localStorage.setItem('flowtrk_user_id', userId); } return userId; } function getOrCreateSessionId() { const params = new URLSearchParams(window.location.search); const fromUrl = params.get('ft_sid') || params.get('ft_journey'); if (fromUrl && FLOW_SESSION_UUID_RE.test(fromUrl)) { sessionStorage.setItem('flowtrk_session_id', fromUrl); try { localStorage.setItem('flowtrk_session_id', fromUrl); } catch (_e) {} trackerState.journeyContinuedFromUrl = true; if (trackerState.debug) { console.log('[FlowTracking] Jornada preservada (Bridge→VSL), session_id=', fromUrl); } return fromUrl; } let sessionId = sessionStorage.getItem('flowtrk_session_id') || (() => { try { return localStorage.getItem('flowtrk_session_id'); } catch (_e) { return null; } })() || parseFlowWindowName(window.name); if (!sessionId) { sessionId = generateUUID(); sessionStorage.setItem('flowtrk_session_id', sessionId); try { localStorage.setItem('flowtrk_session_id', sessionId); } catch (_e) {} } return sessionId; } function generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } function log(...args) { if (trackerState.debug) { console.log('[FlowTracking]', ...args); } } // ======================================== // API PÚBLICA // ======================================== window.FlowTracking = { init: initializeFlowTracking, getState: () => ({ ...trackerState }), getScrollData: () => trackerState.scrollTracker ? trackerState.scrollTracker.getCurrentScroll() : null, getMaxScroll: () => trackerState.maxScrollDepth, reset: () => { if (trackerState.scrollTracker) { trackerState.scrollTracker.reset(); } trackerState.maxScrollDepth = 0; }, destroy: () => { if (trackerState.scrollTracker) { trackerState.scrollTracker.destroy(); } trackerState.initialized = false; } }; // Exportar ScrollTracker para uso global window.ScrollTracker = ScrollTracker; // Auto-inicialização se configurado if (window.FlowTrackingConfig && window.FlowTrackingConfig.autoInit) { initializeFlowTracking(window.FlowTrackingConfig); } log('📚 Flow Tracking v2.0.0 carregado'); })();