PageSourceSearch

https://intake-form-theta.vercel.app/js/form-tracker.js

js intake-form-theta.vercel.app collected 2026-10-03 07:07:18 UTC 6,117 bytes, 198 lines download raw bytes

1/**
2 * Form Tracker — lightweight customer journey telemetry for Show Off Inc forms.
3 * Loaded via <script src="/js/form-tracker.js"></script> on each form page.
4 * MUST NEVER break the form — every public method is wrapped in try/catch.
5 */
6(function () {
7  'use strict';
8
9  var FLUSH_INTERVAL = 30000; // 30 seconds
10  var FIELD_FOCUS_MIN = 3000; // 3 seconds minimum to log a field interaction
11  var MAX_BATCH = 50;
12  var ENDPOINT = '/api/form-tracking';
13
14  var config = { formType: '', email: null, dealNumber: null };
15  var sessionId = '';
16  var queue = [];
17  var stepTimer = null; // timestamp when current step started
18  var currentStep = null;
19  var fieldTimers = {}; // field_name -> focus timestamp
20  var flushTimer = null;
21  var initialized = false;
22
23  // ---- Helpers ----
24
25  function generateId() {
26    return 'ft_' + Date.now().toString(36) + '_' + Math.random().toString(36).substr(2, 8);
27  }
28
29  function enqueue(eventType, extra) {
30    try {
31      var evt = {
32        session_id: sessionId,
33        form_type: config.formType,
34        event_type: eventType,
35        email: config.email || null,
36        deal_number: config.dealNumber || null,
37        step: currentStep || null,
38        field_name: (extra && extra.field_name) || null,
39        detail: (extra && extra.detail) || null,
40        duration_ms: (extra && extra.duration_ms) || null
41      };
42      queue.push(evt);
43    } catch (e) { /* silent */ }
44  }
45
46  function flush() {
47    try {
48      if (queue.length === 0) return;
49      var batch = queue.splice(0, MAX_BATCH);
50      var body = JSON.stringify({ events: batch });
51
52      // Prefer sendBeacon for reliability during page leave; fall back to fetch
53      if (navigator.sendBeacon) {
54        navigator.sendBeacon(ENDPOINT, new Blob([body], { type: 'application/json' }));
55      } else {
56        fetch(ENDPOINT, {
57          method: 'POST',
58          headers: { 'Content-Type': 'application/json' },
59          body: body,
60          keepalive: true
61        }).catch(function () { /* silent */ });
62      }
63    } catch (e) { /* silent */ }
64  }
65
66  function startFlushTimer() {
67    if (flushTimer) return;
68    flushTimer = setInterval(function () {
69      try { flush(); } catch (e) { /* silent */ }
70    }, FLUSH_INTERVAL);
71  }
72
73  // ---- Public API ----
74
75  var FormTracker = {
76    init: function (opts) {
77      try {
78        if (initialized) return;
79        initialized = true;
80
81        config.formType = (opts && opts.formType) || 'unknown';
82        config.email = (opts && opts.email) || null;
83        config.dealNumber = (opts && opts.dealNumber) || null;
84
85        // Session ID: reuse within same tab visit, detect return visits
86        var storageKey = 'ft_session_' + config.formType;
87        var prev = sessionStorage.getItem(storageKey);
88        if (prev) {
89          sessionId = prev;
90          enqueue('return_visit');
91        } else {
92          sessionId = generateId();
93          sessionStorage.setItem(storageKey, sessionId);
94        }
95
96        stepTimer = Date.now();
97        enqueue('page_load');
98        startFlushTimer();
99
100        // Global error capture
101        window.addEventListener('error', function (e) {
102          try {
103            FormTracker.trackError({
104              message: e.message,
105              source: e.filename,
106              line: e.lineno,
107              col: e.colno
108            });
109          } catch (ex) { /* silent */ }
110        });
111
112        window.addEventListener('unhandledrejection', function (e) {
113          try {
114            FormTracker.trackError({
115              message: e.reason ? (e.reason.message || String(e.reason)) : 'Unhandled rejection'
116            });
117          } catch (ex) { /* silent */ }
118        });
119
120        // Page leave — flush remaining events
121        document.addEventListener('visibilitychange', function () {
122          try {
123            if (document.visibilityState === 'hidden') {
124              if (currentStep) {
125                enqueue('page_leave', { duration_ms: Date.now() - (stepTimer || Date.now()) });
126              }
127              flush();
128            }
129          } catch (e) { /* silent */ }
130        });
131
132        window.addEventListener('beforeunload', function () {
133          try {
134            if (currentStep) {
135              enqueue('page_leave', { duration_ms: Date.now() - (stepTimer || Date.now()) });
136            }
137            flush();
138          } catch (e) { /* silent */ }
139        });
140
141      } catch (e) { /* silent — never break the form */ }
142    },
143
144    setEmail: function (email) {
145      try { config.email = email || null; } catch (e) { /* silent */ }
146    },
147
148    setDealNumber: function (num) {
149      try { config.dealNumber = num || null; } catch (e) { /* silent */ }
150    },
151
152    trackStep: function (stepName) {
153      try {
154        var now = Date.now();
155        var duration = stepTimer ? (now - stepTimer) : null;
156        enqueue('step_change', { duration_ms: duration });
157        currentStep = stepName;
158        stepTimer = now;
159      } catch (e) { /* silent */ }
160    },
161
162    trackField: function (fieldName) {
163      try {
164        // Called on blur — only log if focus lasted > 3 seconds
165        var start = fieldTimers[fieldName];
166        if (!start) return;
167        delete fieldTimers[fieldName];
168        var duration = Date.now() - start;
169        if (duration >= FIELD_FOCUS_MIN) {
170          enqueue('field_focus', { field_name: fieldName, duration_ms: duration });
171        }
172      } catch (e) { /* silent */ }
173    },
174
175    fieldFocusStart: function (fieldName) {
176      try { fieldTimers[fieldName] = Date.now(); } catch (e) { /* silent */ }
177    },
178
179    trackSubmit: function (success, detail) {
180      try {
181        enqueue(success ? 'submit_success' : 'submit_failure', {
182          detail: detail || null,
183          duration_ms: stepTimer ? (Date.now() - stepTimer) : null
184        });
185        flush(); // immediate flush on submit
186      } catch (e) { /* silent */ }
187    },
188
189    trackError: function (error) {
190      try {
191        enqueue('js_error', { detail: error || null });
192      } catch (e) { /* silent */ }
193    }
194  };
195
196  // Expose globally
197  window.FormTracker = FormTracker;
198})();

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.