(function () { // Configuration const ENDPOINT_ORIGIN = 'https://lltracking.fly.dev'; // Change this to production URL later const SCRIPT_ID = 'lltracking-script'; // Get Account ID from script attribute const scriptTag = document.getElementById(SCRIPT_ID); const ACCOUNT_ID = scriptTag ? scriptTag.getAttribute('data-account-id') : null; if (!ACCOUNT_ID) { console.error('LLTracking: Account ID not found. ensure script has id="lltracking-script" and data-account-id attribute.'); return; } // Utilities function generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } function getCookie(name) { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop().split(';').shift(); } function setCookie(name, value, days) { let expires = ""; if (days) { const date = new Date(); date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); expires = "; expires=" + date.toUTCString(); } document.cookie = name + "=" + (value || "") + expires + "; path=/"; } // Session Management // Requirement: Session ID in cookie, persists across pages/restarts, 90 day expiry. let sessionId = getCookie('ll_session_id'); if (!sessionId) { sessionId = generateUUID(); // Set cookie for 90 days setCookie('ll_session_id', sessionId, 90); } else { // Refresh cookie expiry on each visit to ensure 90 days from LAST visit? // User said "reset after 90 days, if it still exists". Usually this means rolling window. setCookie('ll_session_id', sessionId, 90); } let visitorId = getCookie('ll_visitor_id'); if (!visitorId) { visitorId = generateUUID(); setCookie('ll_visitor_id', visitorId, 365); } else { setCookie('ll_visitor_id', visitorId, 365); } // Collect Data const visitData = { account_id: ACCOUNT_ID, session_id: sessionId, visitor_id: visitorId, page_url: window.location.href, referrer: document.referrer, utm_source: new URLSearchParams(window.location.search).get('utm_source') || '', utm_medium: new URLSearchParams(window.location.search).get('utm_medium') || '', utm_campaign: new URLSearchParams(window.location.search).get('utm_campaign') || '', utm_term: new URLSearchParams(window.location.search).get('utm_term') || '', utm_content: new URLSearchParams(window.location.search).get('utm_content') || '', gclid: new URLSearchParams(window.location.search).get('gclid') || '', user_agent: navigator.userAgent, screen_width: window.screen.width, screen_height: window.screen.height, device_pixel_ratio: window.devicePixelRatio || 1 }; let visitId = null; // Send Visit Data fetch(`${ENDPOINT_ORIGIN}/api/track/visit`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(visitData) }) .then(res => res.json()) .then(data => { if (data.visit_id) { visitId = data.visit_id; startHeartbeat(); } }) .catch(err => console.error('LLTracking: Failed to send visit', err)); // Event Tracking window.lltrack = { trackEvent: function (eventName, eventData) { if (!visitId) return; // Wait for visit init or queue? For now drop. const payload = { account_id: ACCOUNT_ID, session_id: sessionId, visit_id: visitId, event_name: eventName, event_data: JSON.stringify(eventData) }; fetch(`${ENDPOINT_ORIGIN}/api/track/event`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, // keepalive not always supported with json body in old browsers, but mostly ok keepalive: true, body: JSON.stringify(payload) }).catch(e => console.error('LLTracking event error', e)); } }; // Heartbeat & Scroll Tracking let maxScroll = 0; let timeOnPage = 0; function updateScroll() { const docHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight; const scrollTop = document.documentElement.scrollTop; const percentage = Math.round((scrollTop / docHeight) * 100); if (percentage > maxScroll) maxScroll = percentage; } window.addEventListener('scroll', () => { requestAnimationFrame(updateScroll); }); function sendHeartbeat() { if (!visitId) return; const payload = { visit_id: visitId, time_on_page: timeOnPage, scroll_depth: maxScroll }; // Use sendBeacon if possible for reliability on unload, but JSON requirement makes fetch easier fetch(`${ENDPOINT_ORIGIN}/api/track/heartbeat`, { method: 'POST', body: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, keepalive: true }).catch(() => { }); } function startHeartbeat() { setInterval(() => { timeOnPage += 5; // 5 seconds sendHeartbeat(); }, 5000); window.addEventListener('beforeunload', () => { sendHeartbeat(); }); } })();