1/** 2 * D5 Event Tracking 3 * 4 * Tracks user interactions (phone calls, emails, WhatsApp, form submissions) 5 * Pure vanilla JavaScript - no jQuery dependency 6 * 7 * @package D5_Utilities 8 * @since 2.0.0 9 */ 10 11document.addEventListener('DOMContentLoaded', function() { 12 13 // Capture ad tracking params on first page of session (cache-safe: reads live from browser URL) 14 if (!sessionStorage.getItem('d5_entry_page')) { 15 var params = new URLSearchParams(window.location.search); 16 sessionStorage.setItem('d5_entry_page', window.location.href.split('?')[0]); 17 sessionStorage.setItem('d5_gclid', params.get('gclid') || ''); 18 sessionStorage.setItem('d5_msclkid', params.get('msclkid') || ''); 19 sessionStorage.setItem('d5_fbclid', params.get('fbclid') || ''); 20 sessionStorage.setItem('d5_ttclid', params.get('ttclid') || ''); 21 sessionStorage.setItem('d5_utm_source', params.get('utm_source') || ''); 22 sessionStorage.setItem('d5_utm_medium', params.get('utm_medium') || ''); 23 sessionStorage.setItem('d5_utm_campaign', params.get('utm_campaign') || ''); 24 sessionStorage.setItem('d5_utm_term', params.get('utm_term') || ''); 25 sessionStorage.setItem('d5_utm_content', params.get('utm_content') || ''); 26 } 27 28 // Track recent events to prevent duplicates (2 second window) 29 const recentEvents = new Map(); 30 const DEBOUNCE_DURATION = 2000; // 2 seconds 31 32 /** 33 * Check if event is duplicate within debounce window 34 * 35 * @param {string} type Event type (Phone Call, Email, WhatsApp, Form Submission) 36 * @param {string} subject Event subject/identifier 37 * @return {boolean} True if duplicate, false if new event 38 */ 39 function isDuplicateEvent(type, subject) { 40 const eventKey = `${type}:${subject}`; 41 const now = Date.now(); 42 const lastEventTime = recentEvents.get(eventKey); 43 44 if (lastEventTime && (now - lastEventTime) < DEBOUNCE_DURATION) { 45 return true; // Duplicate within debounce window 46 } 47 48 // Record this event 49 recentEvents.set(eventKey, now); 50 51 // Clean up old entries (older than debounce duration) 52 for (const [key, timestamp] of recentEvents.entries()) { 53 if (now - timestamp > DEBOUNCE_DURATION) { 54 recentEvents.delete(key); 55 } 56 } 57 58 return false; 59 } 60 61 /** 62 * Send event data directly to logEvent.php via sendBeacon (URLSearchParams). 63 * Sends cross-domain directly â no WordPress AJAX server-side hop needed. 64 * Falls back to fetch() for browsers that don't support sendBeacon. 65 * 66 * @param {Object} eventData Event data to send 67 */ 68 function d5SendEventData(eventData) { 69 var params = new URLSearchParams(); 70 71 for (var key in eventData) { 72 params.append(key, eventData[key]); 73 } 74 75 if (navigator.sendBeacon) { 76 navigator.sendBeacon(d5_events_script_vars.logurl, params); 77 } else { 78 fetch(d5_events_script_vars.logurl, { 79 method: 'POST', 80 body: params 81 }).catch(function(error) { 82 console.error('D5 Event tracking error:', error); 83 }); 84 } 85 } 86 87 /** 88 * Get base event data from localized script variables 89 * 90 * @return {Object} Base event data 91 */ 92 function d5GetBaseEventData() { 93 return { 94 domain: window.location.host, 95 url: window.location.pathname, 96 entrypage: sessionStorage.getItem('d5_entry_page') || d5_events_script_vars.entrypage, 97 email: d5_events_script_vars.email, 98 user_agent: d5_events_script_vars.userAgent, 99 county: d5_events_script_vars.county, 100 city: d5_events_script_vars.city, 101 referer: d5_events_script_vars.referer, 102 gclid: sessionStorage.getItem('d5_gclid') || '', 103 msclkid: sessionStorage.getItem('d5_msclkid') || '', 104 fbclid: sessionStorage.getItem('d5_fbclid') || '', 105 ttclid: sessionStorage.getItem('d5_ttclid') || '', 106 utm_source: sessionStorage.getItem('d5_utm_source') || '', 107 utm_medium: sessionStorage.getItem('d5_utm_medium') || '', 108 utm_campaign: sessionStorage.getItem('d5_utm_campaign') || '', 109 utm_term: sessionStorage.getItem('d5_utm_term') || '', 110 utm_content: sessionStorage.getItem('d5_utm_content') || '' 111 }; 112 } 113 114 // Track phone call clicks 115 document.querySelectorAll('a[href^="tel:"]').forEach(function(telLink) { 116 telLink.addEventListener('click', function() { 117 const phoneNumber = this.getAttribute('href').replace('tel:', ''); 118 119 // Check for duplicate 120 if (isDuplicateEvent('Phone Call', phoneNumber)) { 121 return; 122 } 123 124 const eventData = d5GetBaseEventData(); 125 eventData.subject = phoneNumber; 126 eventData.type = 'Phone Call'; 127 128 d5SendEventData(eventData); 129 }); 130 }); 131 132 // Track email clicks 133 document.querySelectorAll('a[href^="mailto:"]').forEach(function(emailLink) { 134 emailLink.addEventListener('click', function() { 135 const emailAddress = this.getAttribute('href').replace('mailto:', ''); 136 137 // Check for duplicate 138 if (isDuplicateEvent('Email', emailAddress)) { 139 return; 140 } 141 142 const eventData = d5GetBaseEventData(); 143 eventData.subject = 'Email link clicked'; 144 eventData.type = 'Email'; 145 146 d5SendEventData(eventData); 147 }); 148 }); 149 150 // Track WhatsApp clicks 151 document.querySelectorAll('a[href^="https://wa.me"]').forEach(function(whatsappLink) { 152 whatsappLink.addEventListener('click', function(e) { 153 const waUrl = this.getAttribute('href'); 154 155 // Check for duplicate 156 if (isDuplicateEvent('WhatsApp', waUrl)) { 157 e.preventDefault(); 158 window.location.href = waUrl; // Still navigate, just don't track 159 return; 160 } 161 162 e.preventDefault(); 163 164 const eventData = d5GetBaseEventData(); 165 eventData.subject = 'Whats App clicked'; 166 eventData.type = 'WhatsApp'; 167 168 d5SendEventData(eventData); 169 170 window.location.href = waUrl; 171 }); 172 }); 173 174 // Track form submissions 175 document.querySelectorAll('form:not(.notracking):not([id^="gform"])').forEach(function(form) { 176 form.addEventListener('submit', function() { 177 let formSubject = ''; 178 let submissionType = 'Form Submission'; 179 180 // WooCommerce order detection - Skip standard checkout (tracked server-side) 181 const isWooCheckout = this.classList.contains('woocommerce-checkout') || 182 this.classList.contains('wc-block-checkout__form') || 183 this.closest('.woocommerce-checkout') || 184 this.closest('.wp-block-woocommerce-checkout') || 185 (this.getAttribute('name') && this.getAttribute('name') === 'checkout'); 186 187 if (isWooCheckout) { 188 return; // Skip - server-side tracking handles WooCommerce checkout 189 } 190 // Custom checkout forms (d5-simple-checkout-form, productForm) - track client-side 191 else if (this.getAttribute('id') === 'd5-simple-checkout-form' || this.getAttribute('id') === 'productForm') { 192 formSubject = 'Order Placed'; 193 submissionType = 'Order'; 194 } 195 // Ignore WooCommerce non-checkout forms 196 else if ( 197 this.classList.contains('cart') || 198 this.classList.contains('woocommerce-cart-form') || 199 this.classList.contains('woocommerce-form-coupon') || 200 this.classList.contains('woocommerce-shipping-calculator') || 201 this.classList.contains('woocommerce-ordering') || 202 this.classList.contains('pa_size') || 203 this.classList.contains('orderby') || 204 this.getAttribute('action') === window.location.pathname 205 ) { 206 return; 207 } 208 // Extract form subject 209 else { 210 const subjectField = this.querySelector('.subject-field'); 211 212 if (subjectField) { 213 const subjectInput = subjectField.querySelector('input'); 214 const subjectSelect = subjectField.querySelector('select'); 215 216 if (subjectInput) { 217 formSubject = subjectInput.value; 218 } else if (subjectSelect) { 219 formSubject = subjectSelect.value; 220 } 221 } else { 222 const firstSelect = this.querySelector('select'); 223 if (firstSelect) { 224 formSubject = firstSelect.value; 225 } 226 } 227 } 228 229 // Check for duplicate submission 230 const formIdentifier = formSubject || this.getAttribute('id') || this.getAttribute('action'); 231 if (isDuplicateEvent(submissionType, formIdentifier)) { 232 // Don't prevent form submission, just don't track it again 233 return; 234 } 235 236 const eventData = d5GetBaseEventData(); 237 eventData.subject = formSubject; 238 eventData.type = submissionType; 239 240 d5SendEventData(eventData); 241 }); 242 }); 243});
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.