PageSourceSearch

https://api.expedify.ai/api/tracker.js?org_id=4c991bc6-7356-4c6c-b63d-a06b5cb4370a

js expedify.ai collected 2026-10-05 12:00:03 UTC 26,565 bytes, 716 lines download raw bytes

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.