1/** 2 * Landing Pages Analytics 3 * 4 * Tracks user interactions on landing pages including: 5 * - Page views and time on page 6 * - CTA clicks 7 * - Form submissions 8 * - Scroll depth 9 * - Element visibility 10 */ 11 12(function() { 13 'use strict'; 14 15 // Configuration 16 const CONFIG = { 17 trackingEndpoint: '/landing/api/track/', 18 debounceTime: 250, 19 scrollThresholds: [25, 50, 75, 100], 20 visibilityThreshold: 0.5, 21 }; 22 23 // State 24 const state = { 25 pageLoadTime: Date.now(), 26 scrollDepthTracked: new Set(), 27 visibleZones: new Set(), 28 pageId: null, 29 variantId: null, 30 }; 31 32 // Initialize 33 function init() { 34 const pageEl = document.querySelector('.landing-page'); 35 if (!pageEl) return; 36 37 state.pageId = pageEl.dataset.pageId; 38 state.variantId = pageEl.dataset.variantId || null; 39 40 // Track page view 41 trackEvent('page_view', { page_id: state.pageId, variant_id: state.variantId }); 42 43 // Set up event listeners 44 setupClickTracking(); 45 setupFormTracking(); 46 setupScrollTracking(); 47 setupVisibilityTracking(); 48 setupUnloadTracking(); 49 } 50 51 // Track event 52 function trackEvent(eventType, data = {}) { 53 if (!state.pageId) return; // Don't track if no page ID 54 55 const payload = { 56 event_type: eventType, 57 landing_page_id: parseInt(state.pageId, 10), 58 variant_id: state.variantId ? parseInt(state.variantId, 10) : null, 59 timestamp: new Date().toISOString(), 60 url: window.location.href, 61 referrer: document.referrer, 62 metadata: data, 63 }; 64 65 // Use sendBeacon for reliability 66 if (navigator.sendBeacon) { 67 const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' }); 68 navigator.sendBeacon(CONFIG.trackingEndpoint, blob); 69 } else { 70 // Fallback to fetch 71 fetch(CONFIG.trackingEndpoint, { 72 method: 'POST', 73 headers: { 74 'Content-Type': 'application/json', 75 'X-CSRFToken': getCsrfToken(), 76 }, 77 body: JSON.stringify(payload), 78 keepalive: true, 79 }).catch(() => {}); 80 } 81 } 82 83 // Get CSRF token 84 function getCsrfToken() { 85 const cookieValue = document.cookie 86 .split('; ') 87 .find(row => row.startsWith('csrftoken=')); 88 return cookieValue ? cookieValue.split('=')[1] : ''; 89 } 90 91 // Click tracking 92 function setupClickTracking() { 93 document.addEventListener('click', function(e) { 94 const target = e.target.closest('[data-event]'); 95 if (!target) return; 96 97 const eventType = target.dataset.event; 98 const zoneId = target.dataset.zoneId; 99 const data = { 100 zone_id: zoneId, 101 element_text: target.textContent.trim().substring(0, 100), 102 element_href: target.href || null, 103 }; 104 105 // Include any additional data-* attributes 106 for (const [key, value] of Object.entries(target.dataset)) { 107 if (key !== 'event' && key !== 'zoneId') { 108 data[key] = value; 109 } 110 } 111 112 trackEvent(eventType, data); 113 }); 114 } 115 116 // Form tracking 117 function setupFormTracking() { 118 document.addEventListener('submit', function(e) { 119 const form = e.target; 120 const zoneId = form.dataset.zoneId; 121 122 if (!zoneId) return; 123 124 trackEvent('form_submit', { 125 zone_id: zoneId, 126 form_id: form.id || null, 127 }); 128 }); 129 130 // Track form field focus 131 document.addEventListener('focus', function(e) { 132 const input = e.target; 133 if (!input.name) return; 134 135 const form = input.closest('form[data-zone-id]'); 136 if (!form) return; 137 138 trackEvent('form_field_focus', { 139 zone_id: form.dataset.zoneId, 140 field_name: input.name, 141 }); 142 }, true); 143 } 144 145 // Scroll tracking 146 function setupScrollTracking() { 147 let ticking = false; 148 149 function checkScrollDepth() { 150 const scrollHeight = document.documentElement.scrollHeight - window.innerHeight; 151 const scrollPercent = scrollHeight > 0 ? Math.round((window.scrollY / scrollHeight) * 100) : 100; 152 153 for (const threshold of CONFIG.scrollThresholds) { 154 if (scrollPercent >= threshold && !state.scrollDepthTracked.has(threshold)) { 155 state.scrollDepthTracked.add(threshold); 156 trackEvent('scroll_depth', { depth: threshold }); 157 } 158 } 159 ticking = false;
160 } 161 162 window.addEventListener('scroll', function() { 163 if (!ticking) { 164 requestAnimationFrame(checkScrollDepth); 165 ticking = true; 166 } 167 }, { passive: true }); 168 } 169 170 // Visibility tracking using Intersection Observer 171 function setupVisibilityTracking() { 172 if (!('IntersectionObserver' in window)) return; 173 174 const observer = new IntersectionObserver( 175 (entries) => { 176 entries.forEach((entry) => { 177 const zoneId = entry.target.dataset.zoneId; 178 if (!zoneId) return; 179 180 if (entry.isIntersecting && !state.visibleZones.has(zoneId)) { 181 state.visibleZones.add(zoneId); 182 trackEvent('zone_visible', { 183 zone_id: zoneId, 184 zone_type: entry.target.dataset.zoneType, 185 }); 186 } 187 }); 188 }, 189 { threshold: CONFIG.visibilityThreshold } 190 ); 191 192 // Observe all zones 193 document.querySelectorAll('[data-zone-id]').forEach((zone) => { 194 observer.observe(zone); 195 }); 196 } 197 198 // Unload tracking 199 function setupUnloadTracking() { 200 window.addEventListener('beforeunload', function() { 201 const timeOnPage = Math.round((Date.now() - state.pageLoadTime) / 1000); 202 trackEvent('page_exit', { 203 time_on_page: timeOnPage, 204 scroll_depth: Math.max(...state.scrollDepthTracked, 0), 205 zones_viewed: state.visibleZones.size, 206 }); 207 }); 208 209 // Also track visibility change (mobile) 210 document.addEventListener('visibilitychange', function() { 211 if (document.visibilityState === 'hidden') { 212 const timeOnPage = Math.round((Date.now() - state.pageLoadTime) / 1000); 213 trackEvent('page_hidden', { 214 time_on_page: timeOnPage, 215 }); 216 } 217 }); 218 } 219 220 // Initialize when DOM is ready 221 if (document.readyState === 'loading') { 222 document.addEventListener('DOMContentLoaded', init); 223 } else { 224 init(); 225 } 226 227 // Expose for external use 228 window.LandingPageAnalytics = { 229 trackEvent: trackEvent, 230 }; 231})();
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.