1/** 2 * Expedify Visitor Tracking Script 3 * Organization: 4c991bc6-7356-4c6c-b63d-a06b5cb4370a 4 * Generated: production environment 5 */ 6 7(function(window, document) { 8 'use strict'; 9 10 // Configuration 11 const CONFIG = { 12 orgId: '4c991bc6-7356-4c6c-b63d-a06b5cb4370a', 13 apiBase: 'https://api.expedify.ai/api/public/tracking', 14 debug: false, 15 version: '1.0.0' 16 }; 17 18 // State management 19 let visitorId = null; 20 let visitorToken = null; 21 let sessionId = null; 22 let dbSessionId = null; // The actual database UUID session ID 23 let isInitialized = false; 24 let eventQueue = []; 25 26 // Utility functions 27 function generateFingerprint() { 28 const canvas = document.createElement('canvas'); 29 const ctx = canvas.getContext('2d'); 30 if (ctx) { 31 ctx.textBaseline = 'top'; 32 ctx.font = '14px Arial'; 33 ctx.fillText('Expedify tracking', 2, 2); 34 } 35 36 const fingerprint = { 37 userAgent: navigator.userAgent, 38 language: navigator.language, 39 platform: navigator.platform, 40 screenResolution: `${screen.width}x${screen.height}`, 41 timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, 42 canvasFingerprint: canvas.toDataURL(), 43 cookieEnabled: navigator.cookieEnabled, 44 doNotTrack: navigator.doNotTrack || 'unspecified' 45 }; 46 47 // Simple hash function 48 let hash = 0; 49 const str = JSON.stringify(fingerprint); 50 for (let i = 0; i < str.length; i++) { 51 const char = str.charCodeAt(i); 52 hash = ((hash << 5) - hash) + char; 53 hash = hash & hash; // Convert to 32bit integer 54 } 55 56 return 'v_' + Math.abs(hash).toString(36) + '_' + Date.now().toString(36); 57 } 58 59 function generateSessionId() { 60 return 's_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now().toString(36); 61 } 62 63 function getStoredVisitorId() { 64 try { 65 return localStorage.getItem('expedify_visitor_id'); 66 } catch (e) { 67 return null; 68 } 69 } 70 71 // Proves to the server that THIS browser owns visitor_id. Issued once by 72 // /identify the first time a visitor is seen. Without it, anyone who knows 73 // a visitor_id could attach an identity to that visitor or link a contact 74 // to it, poisoning lead attribution. 75 function getStoredVisitorToken() { 76 try { 77 return localStorage.getItem('expedify_visitor_token'); 78 } catch (e) { 79 return null; 80 } 81 } 82 83 function setStoredVisitorToken(token) { 84 try { 85 if (token) localStorage.setItem('expedify_visitor_token', token); 86 } catch (e) { 87 // localStorage unavailable - token stays in memory for this page only 88 } 89 } 90 91 function setStoredVisitorId(id) { 92 try { 93 localStorage.setItem('expedify_visitor_id', id); 94 } catch (e) { 95 // Ignore if localStorage is not available 96 } 97 } 98 99 function getStoredSessionId() { 100 try { 101 return sessionStorage.getItem('expedify_session_id'); 102 } catch (e) { 103 return null; 104 } 105 } 106 107 function getStoredDbSessionId() { 108 try { 109 return sessionStorage.getItem('expedify_db_session_id'); 110 } catch (e) { 111 return null; 112 } 113 } 114 115 function setStoredSessionId(id) { 116 try { 117 sessionStorage.setItem('expedify_session_id', id); 118 } catch (e) { 119 // Ignore if sessionStorage is not available 120 } 121 } 122 123 function extractUtmParams() { 124 const params = new URLSearchParams(window.location.search); 125 return { 126 utm_source: params.get('utm_source'), 127 utm_medium: params.get('utm_medium'), 128 utm_campaign: params.get('utm_campaign'), 129 utm_term: params.get('utm_term'), 130 utm_content: params.get('utm_content'), 131 gclid: params.get('gclid'), 132 fbclid: params.get('fbclid') 133 }; 134 } 135 136 function getDeviceInfo() { 137 return { 138 userAgent: navigator.userAgent, 139 language: navigator.language, 140 platform: navigator.platform, 141 screenResolution: `${screen.width}x${screen.height}`, 142 viewportSize: `${window.innerWidth}x${window.innerHeight}`, 143 timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, 144 cookieEnabled: navigator.cookieEnabled, 145 onlineStatus: navigator.onLine 146 }; 147 } 148
149 async function sendTrackingData(endpoint, data, method = 'POST') { 150 const url = `${CONFIG.apiBase}/${CONFIG.orgId}/${endpoint}`; 151 152 try { 153 const response = await fetch(url, { 154 method: method, 155 headers: { 156 'Content-Type': 'application/json', 157 'X-Visitor-ID': visitorId, 158 'X-Session-ID': dbSessionId || sessionId // Use DB session ID if available 159 }, 160 body: JSON.stringify(data), 161 mode: 'cors', 162 credentials: 'omit' 163 }); 164 165 if (CONFIG.debug) { 166 console.log('Expedify Tracker: Data sent to', endpoint, response.status); 167 } 168 169 return response; 170 } catch (error) { 171 if (CONFIG.debug) { 172 console.error('Expedify Tracker: Failed to send data', error); 173 } 174 175 // Fallback to pixel tracking 176 const img = new Image(1, 1); 177 const params = new URLSearchParams({ 178 ...data, 179 visitor_id: visitorId, 180 session_id: sessionId, 181 fallback: 'true' 182 }); 183 img.src = `${CONFIG.apiBase}/${CONFIG.orgId}/pixel.gif?${params.toString()}`; 184 185 throw error; 186 } 187 } 188 189 async function initializeTracking() { 190 // Get or create visitor ID 191 visitorId = getStoredVisitorId(); 192 if (!visitorId) { 193 visitorId = generateFingerprint(); 194 setStoredVisitorId(visitorId); 195 } 196 visitorToken = getStoredVisitorToken(); 197 198 // Get or create session ID 199 sessionId = getStoredSessionId(); 200 if (!sessionId) { 201 sessionId = generateSessionId(); 202 setStoredSessionId(sessionId); 203 } 204 205 // Get stored database session ID 206 dbSessionId = getStoredDbSessionId(); 207 208 try { 209 // Initialize session with backend 210 const sessionData = { 211 visitor_id: visitorId, 212 session_id: sessionId, 213 fingerprint: visitorId, 214 page_url: window.location.href, 215 page_title: document.title, 216 referrer: document.referrer, 217 ...getDeviceInfo(), 218 ...extractUtmParams(), 219 initialized_at: new Date().toISOString() 220 }; 221 222 const response = await sendTrackingData('identify', sessionData); 223 const result = await response.json(); 224 225 // Issued only the first time this visitor_id is seen. Persist it - 226 // it is the one chance to receive it, and the mutation endpoints 227 // (identify-user / link-contact) require it from here on. 228 if (result.visitor_token) { 229 visitorToken = result.visitor_token; 230 setStoredVisitorToken(visitorToken); 231 } 232 233 // Store the actual database session ID 234 if (result.session_id) { 235 dbSessionId = result.session_id; 236 // Optionally store in sessionStorage 237 try { 238 sessionStorage.setItem('expedify_db_session_id', dbSessionId); 239 } catch (e) { 240 // Ignore if sessionStorage is not available 241 } 242 } 243 244 if (CONFIG.debug) { 245 console.log('Expedify Tracker: Session initialized', sessionData); 246 console.log('Expedify Tracker: DB Session ID', dbSessionId); 247 } 248 249 isInitialized = true; 250 251 // Track initial page view 252 trackPageView(); 253 254 // Process any queued events 255 processEventQueue(); 256 257 } catch (error) { 258 if (CONFIG.debug) { 259 console.error('Expedify Tracker: Initialization failed', error); 260 } 261 // Continue with basic tracking even if initialization fails 262 isInitialized = true; 263 trackPageView(); 264 } 265 } 266 267 function trackPageView(customData = {}) { 268 const pageData = { 269 visitor_id: visitorId, 270 session_id: dbSessionId || sessionId, // Use DB session ID if available 271 page_url: window.location.href, 272 page_title: document.title, 273 page_path: window.location.pathname, 274 referrer_url: document.referrer, 275 viewed_at: new Date().toISOString(), 276 ...extractUtmParams(), 277 ...customData 278 }; 279 280 if (!isInitialized) { 281 eventQueue.push(['pageview', pageData]); 282 return; 283 } 284 285 sendTrackingData('page-view', pageData).catch(() => { 286 // Pixel fallback already handled in sendTrackingData 287 }); 288 } 289 290 function trackEvent(category, action, label = null, value = null, properties = {}) { 291 const eventData = { 292 visitor_id: visitorId, 293 session_id: dbSessionId || sessionId, // Use DB session ID if available 294 category: category, 295 action: action, 296 label: label, 297 value: value, 298 page_url: window.location.href, 299 properties: properties, 300 occurred_at: new Date().toISOString() 301 }; 302 303 if (!isInitialized) { 304 eventQueue.push(['event', eventData]); 305 return; 306 } 307 308 sendTrackingData('event', eventData).catch(() => { 309 // Pixel fallback already handled in sendTrackingData 310 }); 311 } 312 313 function processEventQueue() { 314 while (eventQueue.length > 0) { 315 const [eventType, data] = eventQueue.shift(); 316 317 if (eventType === 'pageview') { 318 trackPageView(data); 319 } else if (eventType === 'event') { 320 trackEvent(data.category, data.action, data.label, data.value, data.properties); 321 } else if (eventType === 'form') { 322 trackFormInteraction(data); 323 } 324 } 325 } 326 327 // Track form interactions via dedicated endpoint (creates form_submission activities) 328 function trackFormInteraction(formData) { 329 const interactionData = { 330 visitor_id: visitorId, 331 session_id: dbSessionId || sessionId, 332 form_id: formData.form_id, 333 interaction_type: formData.interaction_type || 'submit', 334 form_name: formData.form_name || '', 335 page_url: formData.form_url || window.location.href, 336 field_count: formData.field_count || 0, 337 ...extractUtmParams() 338 }; 339 340 if (!isInitialized) { 341 eventQueue.push(['form', interactionData]); 342 return; 343 } 344 345 // Use dedicated form-interaction endpoint (creates form_submission activity) 346 sendTrackingData('form-interaction', interactionData).catch(() => { 347 // Pixel fallback already handled in sendTrackingData 348 }); 349 } 350 351 // Auto-track form submissions 352 function setupFormTracking() { 353 document.addEventListener('submit', function(e) { 354 const form = e.target; 355 if (form.tagName === 'FORM') { 356 const formId = form.id || form.action || 'unknown_form'; 357 358 // Collect form data for tracking 359 const formData = { 360 form_id: formId, 361 form_action: form.action || '', 362 form_method: form.method || 'GET', 363 form_name: form.name || '', 364 form_class: form.className || '', 365 field_count: form.elements.length, 366 form_url: window.location.href,
367 interaction_type: 'submit' 368 }; 369 370 // Use dedicated form tracking (creates form_submission activity) 371 trackFormInteraction(formData); 372 } 373 }); 374 375 // Track iframe form submissions via postMessage 376 setupIframeFormTracking(); 377 } 378 379 // Setup iframe form tracking 380 function setupIframeFormTracking() { 381 // Listen for messages from embedded forms 382 window.addEventListener('message', function(event) { 383 // Verify the message is from a trusted source (your forms) 384 const trustedOrigins = [ 385 'http://localhost:3000', 386 'https://localhost:3000', 387 'http://localhost:8000', 388 'https://localhost:8000', 389 window.location.origin, 390 // Add your form hosting domains here 391 'https://forms.expedify.io', 392 'https://your-form-domain.com' 393 ]; 394 395 if (!trustedOrigins.includes(event.origin)) { 396 return; // Ignore messages from untrusted origins 397 } 398 399 const data = event.data; 400 401 // Handle form submission tracking 402 if (data && data.type === 'form_submit') { 403 const formData = { 404 iframe_origin: event.origin, 405 form_id: data.formId || 'embedded_form', 406 form_type: data.formType || 'iframe', 407 form_name: data.formName || '', 408 submission_data: data.submissionData || {}, 409 iframe_url: data.formUrl || '', 410 parent_url: window.location.href 411 }; 412 413 trackEvent('form', 'iframe_submit', data.formId || 'embedded_form', null, formData); 414 415 if (CONFIG.debug) { 416 console.log('Expedify Tracker: Iframe form submitted', formData); 417 } 418 } 419 420 // Handle form load tracking 421 else if (data && data.type === 'form_load') { 422 const formData = { 423 iframe_origin: event.origin, 424 form_id: data.formId || 'embedded_form', 425 form_type: data.formType || 'iframe', 426 form_name: data.formName || '', 427 iframe_url: data.formUrl || '', 428 parent_url: window.location.href 429 }; 430 431 trackEvent('form', 'iframe_load', data.formId || 'embedded_form', null, formData); 432 } 433 434 // Handle form interaction tracking 435 else if (data && data.type === 'form_interaction') { 436 const formData = { 437 iframe_origin: event.origin, 438 form_id: data.formId || 'embedded_form', 439 interaction_type: data.interactionType || 'unknown', 440 field_name: data.fieldName || '', 441 field_type: data.fieldType || '', 442 parent_url: window.location.href 443 }; 444 445 trackEvent('form', 'iframe_interaction', `${data.formId}_${data.interactionType}`, null, formData); 446 } 447 }); 448 } 449 450 // Auto-track button clicks and external links 451 function setupClickTracking() { 452 document.addEventListener('click', function(e) { 453 const target = e.target.closest('button, input[type="button"], input[type="submit"], a'); 454 455 if (!target) return; 456 457 // Handle buttons (including add to cart, etc.) 458 if (target.tagName === 'BUTTON' || 459 (target.tagName === 'INPUT' && ['button', 'submit'].includes(target.type))) { 460 461 const buttonData = { 462 button_text: target.textContent?.trim() || target.value || '', 463 button_name: target.name || '', 464 button_id: target.id || '', 465 button_class: target.className || '', 466 button_type: target.type || 'button', 467 button_value: target.value || '', 468 form_id: target.form?.id || '', 469 form_name: target.form?.name || '' 470 }; 471 472 // Detect add to cart buttons 473 const buttonText = buttonData.button_text.toLowerCase(); 474 const buttonClass = buttonData.button_class.toLowerCase(); 475 const buttonId = buttonData.button_id.toLowerCase(); 476 477 let eventAction = 'button_click'; 478 let eventLabel = buttonData.button_text || buttonData.button_name || 'unknown_button'; 479 480 // Detect common e-commerce actions 481 if (buttonText.includes('add to cart') || 482 buttonText.includes('add to bag') || 483 buttonClass.includes('add-to-cart') || 484 buttonClass.includes('addtocart') || 485 buttonId.includes('add-to-cart')) { 486 eventAction = 'add_to_cart'; 487 eventLabel = 'add_to_cart_button'; 488 } else if (buttonText.includes('buy now') || 489 buttonText.includes('purchase')) {
490 eventAction = 'buy_now'; 491 eventLabel = 'buy_now_button'; 492 } else if (buttonText.includes('checkout')) { 493 eventAction = 'checkout'; 494 eventLabel = 'checkout_button'; 495 } else if (buttonText.includes('subscribe') || 496 buttonText.includes('sign up')) { 497 eventAction = 'subscribe'; 498 eventLabel = 'subscribe_button'; 499 } else if (target.type === 'submit' && target.form) { 500 // Skip tracking submit buttons ONLY if they're inside a form 501 // Form submission is tracked separately via setupFormTracking() 502 // Buttons without forms should still be tracked (e.g., React nav buttons) 503 return; 504 } 505 506 // Use 'click' category for button clicks (not 'engagement') 507 trackEvent('click', eventAction, eventLabel, null, buttonData); 508 509 if (CONFIG.debug) { 510 console.log('Expedify Tracker: Button clicked', eventAction, buttonData); 511 } 512 } 513 514 // Handle links (existing functionality) 515 else if (target.tagName === 'A' && target.href) { 516 const isExternal = target.hostname !== window.location.hostname; 517 const isDownload = target.download || /\.(pdf|doc|docx|xls|xlsx|zip|mp3|mp4|avi)$/i.test(target.href); 518 519 if (isExternal || isDownload) { 520 // Use proper categories: 'download' for downloads, 'link' for external links 521 const category = isDownload ? 'download' : 'link'; 522 trackEvent(category, isDownload ? 'file_download' : 'external_link', target.href, null, { 523 link_text: target.textContent.trim(), 524 link_url: target.href, 525 is_external: isExternal, 526 is_download: isDownload 527 }); 528 } 529 } 530 }); 531 } 532 533 // Track page unload - uses 'session' category for session-level events 534 function setupUnloadTracking() { 535 window.addEventListener('beforeunload', function() { 536 trackEvent('session', 'page_unload', null, Date.now() - performance.now()); 537 }); 538 } 539 540 // Track SPA navigation (React Router, History API changes) 541 function setupSPATracking() { 542 // Store the original pushState and replaceState 543 const originalPushState = history.pushState; 544 const originalReplaceState = history.replaceState; 545 546 // Override pushState 547 history.pushState = function(...args) { 548 originalPushState.apply(this, args); 549 onSPANavigation('pushState'); 550 }; 551 552 // Override replaceState 553 history.replaceState = function(...args) { 554 originalReplaceState.apply(this, args); 555 onSPANavigation('replaceState'); 556 }; 557 558 // Also listen for popstate (browser back/forward buttons) 559 window.addEventListener('popstate', function() { 560 onSPANavigation('popstate'); 561 }); 562 563 function onSPANavigation(source) { 564 // Small delay to let React update the DOM/title 565 setTimeout(function() { 566 if (isInitialized) { 567 trackPageView({ navigation_type: source }); 568 if (CONFIG.debug) { 569 console.log('Expedify Tracker: SPA navigation detected (' + source + '):', window.location.pathname); 570 } 571 } 572 }, 100); 573 } 574 } 575 576 // Identify user by email - links visitor to contact 577 async function identifyUser(userData) { 578 if (!userData || !userData.email) { 579 console.error('ExpedifyTracker: email is required for identify()'); 580 return { success: false, error: 'email required' }; 581 } 582 583 // Wait for initialization if needed 584 if (!isInitialized) { 585 console.warn('ExpedifyTracker: Waiting for initialization before identify...'); 586 await new Promise(resolve => setTimeout(resolve, 1000)); 587 } 588 589 const identifyData = { 590 visitor_id: visitorId, 591 session_id: dbSessionId || sessionId, 592 email: userData.email, 593 name: userData.name || null, 594 phone: userData.phone || null, 595 visitor_token: visitorToken, 596 properties: { ...userData } 597 }; 598 599 // Remove standard fields from properties to avoid duplication 600 delete identifyData.properties.email; 601 delete identifyData.properties.name; 602 delete identifyData.properties.phone; 603 604 try { 605 const response = await sendTrackingData('identify-user', identifyData); 606 const result = await response.json(); 607 608 if (CONFIG.debug) { 609 console.log('ExpedifyTracker: User identified', result); 610 } 611 612 return result; 613 } catch (error) { 614 if (CONFIG.debug) {
615 console.error('ExpedifyTracker: Failed to identify user', error); 616 } 617 return { success: false, error: error.message }; 618 } 619 } 620 621 // Public API - Object-based 622 window.ExpedifyTracker = { 623 identify: identifyUser, // Link visitor to contact by email 624 track: trackEvent, 625 trackPageView: trackPageView, 626 getVisitorId: () => visitorId, 627 getSessionId: () => sessionId, 628 getDbSessionId: () => dbSessionId, 629 version: CONFIG.version 630 }; 631 632 // Compatibility API - Function-based (for tracking.ts) 633 // This allows: window.expedify('track', 'event_name', {data}) 634 window.expedify = function(command, ...args) { 635 if (CONFIG.debug) { 636 console.log('Expedify Tracker: Command received -', command, args); 637 } 638 639 switch (command) { 640 case 'init': 641 // Already initialized, just return 642 if (CONFIG.debug) { 643 console.log('Expedify Tracker: Already initialized for org', CONFIG.orgId); 644 } 645 return Promise.resolve(); 646 647 case 'track': 648 // args[0] = eventName, args[1] = data 649 const eventName = args[0]; 650 const eventData = args[1] || {}; 651 trackEvent( 652 eventData.category || 'custom', 653 eventName, 654 eventData.label || eventName, 655 eventData.value || null, 656 eventData 657 ); 658 break; 659 660 case 'identify': 661 // args[0] = userId or email 662 const userId = args[0]; 663 if (typeof userId === 'string' && userId.includes('@')) { 664 identifyUser({ email: userId }); 665 } else if (typeof userId === 'object') { 666 identifyUser(userId); 667 } 668 break; 669 670 case 'pageview': 671 trackPageView(args[0] || {}); 672 break; 673 674 case 'consent': 675 // Consent management (placeholder) 676 if (CONFIG.debug) { 677 console.log('Expedify Tracker: Consent set to', args[0]); 678 } 679 break; 680 681 default: 682 if (CONFIG.debug) { 683 console.warn('Expedify Tracker: Unknown command -', command); 684 } 685 } 686 }; 687 688 // Mark expedify as ready for tracking.ts 689 window.expedify.isReady = true; 690 691 // Initialize when DOM is ready 692 if (document.readyState === 'loading') { 693 document.addEventListener('DOMContentLoaded', function() { 694 initializeTracking(); 695 setupFormTracking(); 696 setupClickTracking(); 697 setupUnloadTracking(); 698 setupSPATracking(); // Track React Router navigation 699 }); 700 } else { 701 initializeTracking(); 702 setupFormTracking(); 703 setupClickTracking(); 704 setupUnloadTracking(); 705 setupSPATracking(); // Track React Router navigation 706 } 707 708 // Always log when tracker loads (even in production) for debugging 709 console.log('%c[Expedify Tracker] Loaded for org: ' + CONFIG.orgId, 'color: #8B5CF6; font-weight: bold'); 710 console.log('%c[Expedify Tracker] Debug mode: ' + CONFIG.debug + ', API: ' + CONFIG.apiBase, 'color: #8B5CF6'); 711 712 if (CONFIG.debug) { 713 console.log('Expedify Tracker: Full config', CONFIG); 714 } 715 716})(window, document);
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.