PageSourceSearch

https://www.usmoneyreserve.com/wp-content/plugins/usmr-gtm-analyti…/js/gf-datalayer-tracking.js?ver=2.9.1

js usmoneyreserve.com collected 2026-10-02 05:34:10 UTC 33,587 bytes, 910 lines download raw bytes

1/**
2 * Gravity Forms — dataLayer Event Tracking
3 *
4 * Tracks form interactions for GTM / GA4 funnel and drop-off analysis.
5 * Events: gf_form_view, gf_field_focus, gf_field_change, gf_field_abandoned,
6 *         gf_page_advance, gf_validation_error, gf_field_validation_error,
7 *         gf_form_submit, gf_form_abandoned
8 *
9 * Configuration is passed from PHP via `usmrGfDL` (wp_localize_script).
10 *
11 * SHA-256 for user-data hashing is provided by `vendor-sha256.min.js`
12 * (js-sha256, MIT) which exposes `window.sha256` as a synchronous UTF-8 hex helper.
13 *
14 * gf_form_submit can carry two hashed-identifier keys:
15 *   meta_user_data_hashed — FROZEN legacy shape `{ em, ph }`, ph = digits only.
16 *                           Live GTM tags read this; its values must not change.
17 *   user_data_hashed      — per-platform `{ meta, google, microsoft }`, each
18 *                           `{ em, ph }`, normalized to each vendor's spec
19 *                           (E.164 for phones; Microsoft strips email dots/alias).
20 */
21
22(function () {
23  'use strict';
24
25  window.dataLayer = window.dataLayer || [];
26
27  var cfg = window.usmrGfDL || {};
28  var formNames     = cfg.formNames  || {};
29  var formTypes     = cfg.formTypes  || {};
30  var postName      = cfg.postName   || '';
31  var DEBUG         = !!cfg.debug;
32  var TIMING        = cfg.trackTiming !== false;
33  var ABANDON       = cfg.trackAbandon !== false;
34  var INCLUDE_USER_DATA = !!cfg.includeUserData;
35  var BLOCKLIST     = Array.isArray(cfg.userDataBlocklist) ? cfg.userDataBlocklist : [];
36  var META_HASHED_IDS = !!cfg.metaHashedIds;
37  var DEFAULT_COUNTRY_CODE = cfg.defaultCountryCode || '+1';
38  var sha256Hex     = typeof window.sha256 === 'function' ? window.sha256 : null;
39  var DEFAULT_EMAIL_META_TOKENS = ['email', 'emailaddress', 'email_address'];
40  var DEFAULT_PHONE_META_TOKENS = ['phone', 'phone_number', 'phonenumber', 'telephone'];
41  var HARD_BLOCKED_TYPES = ['password', 'file', 'hidden'];
42  var VALUE_MAX_LEN = 200;
43  var SS_PREFIX     = 'usmrGfValues_';
44  var SS_META_PREFIX = 'usmrGfMeta_';
45  var SS_META_HASH_PREFIX = 'usmrGfMetaHash_';
46  var SS_TTL_MS     = 60 * 1000;
47
48  function mergeMetaTokenLists(defaults, extra) {
49    var map = {};
50    var i;
51    for (i = 0; i < defaults.length; i++) {
52      map[defaults[i]] = true;
53    }
54    var ex = Array.isArray(extra) ? extra : [];
55    for (i = 0; i < ex.length; i++) {
56      var t = String(ex[i] || '').toLowerCase().trim();
57      if (t) map[t] = true;
58    }
59    return Object.keys(map);
60  }
61
62  var EMAIL_META_TOKENS = mergeMetaTokenLists(DEFAULT_EMAIL_META_TOKENS, cfg.metaEmailTokensExtra);
63  var PHONE_META_TOKENS = mergeMetaTokenLists(DEFAULT_PHONE_META_TOKENS, cfg.metaPhoneTokensExtra);
64
65  /* ─────────────────────────────────────────────
66   * UTILITIES
67   * ───────────────────────────────────────────── */
68
69  function getFieldLabel(el) {
70    var gfield = el.closest('.gfield');
71    if (gfield) {
72      var label = gfield.querySelector('label');
73      if (label) return label.textContent.trim();
74    }
75    return (
76      el.getAttribute('aria-label') ||
77      el.getAttribute('placeholder') ||
78      el.name ||
79      el.id ||
80      'unknown'
81    );
82  }
83
84  function getFormId(el) {
85    var form = el.closest('form[id^="gform_"]');
86    if (form) {
87      var match = form.id.match(/gform_(\d+)/);
88      if (match) return match[1];
89    }
90    return 'unknown';
91  }
92
93  function getFormName(formId) {
94    return formNames[formId] || '';
95  }
96
97  function getFormType(formId) {
98    if (formTypes[formId]) return formTypes[formId];
99
100    var formEl = document.getElementById('gform_' + formId);
101    var wrapperEl = formEl ? formEl.closest('.gform_wrapper') : document.getElementById('gform_wrapper_' + formId);
102
103    // Back-compat: some installs still output legacy attributes during migration.
104    var fromDom = (
105      (formEl && (formEl.getAttribute('usmrformtype') || formEl.getAttribute('data-usmr-form-type'))) ||
106      (wrapperEl && (wrapperEl.getAttribute('usmrformtype') || wrapperEl.getAttribute('data-usmr-form-type'))) ||
107      ''
108    );
109    if (fromDom) return fromDom;
110
111    return readPersistedMeta(formId);
112  }
113
114  function push(eventName, params) {
115    var payload = Object.assign({ event: eventName }, params);
116    window.dataLayer.push(payload);
117    if (DEBUG) {
118      console.log('[GF dataLayer]', payload);
119    }
120  }
121
122  function now() {
123    return Date.now ? Date.now() : +new Date();
124  }
125
126
127  /* ─────────────────────────────────────────────
128   * USER-DATA UTILITIES
129   * Sanitize labels into stable GA4-safe keys, filter out
130   * sensitive fields, and truncate values to a safe length.
131   * ───────────────────────────────────────────── */
132
133  function slugifyLabel(label) {
134    if (!label) return '';
135    return String(label)
136      .toLowerCase()
137      .replace(/\s*\*+\s*$/, '')           // trailing "*" (required marker)
138      .replace(/\s*\(required\)\s*$/i, '') // trailing "(required)"
139      .replace(/[^a-z0-9]+/g, '_')
140      .replace(/^_+|_+$/g, '');
141  }
142
143  function matchesBlocklist(haystack) {
144    if (!haystack) return false;
145    haystack = String(haystack).toLowerCase();
146    for (var i = 0; i < BLOCKLIST.length; i++) {
147      if (haystack.indexOf(BLOCKLIST[i]) !== -1) return true;
148    }
149    return false;
150  }
151
152  function isBlockedField(el, label) {
153    var type = (el.type || '').toLowerCase();
154    if (HARD_BLOCKED_TYPES.indexOf(type) !== -1) return true;
155    if (matchesBlocklist(type)) return true;
156    if (matchesBlocklist(el.name)) return true;
157    if (matchesBlocklist(el.id)) return true;
158    if (matchesBlocklist(slugifyLabel(label))) return true;
159    return false;
160  }
161
162  function sanitizeValue(value) {
163    if (value === null || value === undefined) return '';
164    var str = String(value).replace(/\s+/g, ' ').trim();
165    if (str.length > VALUE_MAX_LEN) {
166      str = str.slice(0, VALUE_MAX_LEN);
167    }
168    return str;
169  }
170
171  var formValues = {};
172
173  function cacheFieldValue(el) {
174    if (!INCLUDE_USER_DATA) return;
175    var label = getFieldLabel(el);
176    if (isBlockedField(el, label)) return;
177    var slug = slugifyLabel(label);
178    if (!slug) return;
179    var formId = getFormId(el);
180    formValues[formId] = formValues[formId] || {};
181    formValues[formId][slug] = sanitizeValue(el.value);
182  }
183
184  function persistFormValues(formId) {
185    if (!INCLUDE_USER_DATA) return;
186    if (!formValues[formId]) return;
187    try {
188      window.sessionStorage.setItem(SS_PREFIX + formId, JSON.stringify({
189        t: now(),
190        v: formValues[formId]
191      }));
192    } catch (e) { /* sessionStorage unavailable / quota */ }
193  }
194
195  function readPersistedValues(formId) {
196    try {
197      var raw = window.sessionStorage.getItem(SS_PREFIX + formId);
198      if (!raw) return null;
199      window.sessionStorage.removeItem(SS_PREFIX + formId);
200      var parsed = JSON.parse(raw);
201      if (!parsed || typeof parsed !== 'object') return null;
202      if (now() - parsed.t > SS_TTL_MS) return null;
203      return parsed.v || null;
204    } catch (e) { return null; }
205  }
206
207  function persistFormMeta(formId, formEl) {
208    var formType = (
209      (formEl && (formEl.getAttribute('usmrformtype') || formEl.getAttribute('data-usmr-form-type'))) ||
210      ''
211    );
212    if (!formType) return;
213    try {
214      window.sessionStorage.setItem(SS_META_PREFIX + formId, JSON.stringify({
215        t: now(),
216        form_type: formType
217      }));
218    } catch (e) { /* sessionStorage unavailable / quota */ }
219  }
220
221  function readPersistedMeta(formId) {
222    try {
223      var raw = window.sessionStorage.getItem(SS_META_PREFIX + formId);
224      if (!raw) return '';
225      window.sessionStorage.removeItem(SS_META_PREFIX + formId);
226      var parsed = JSON.parse(raw);
227      if (!parsed || typeof parsed !== 'object') return '';
228      if (now() - parsed.t > SS_TTL_MS) return '';
229      return parsed.form_type || '';
230    } catch (e) { return ''; }
231  }
232
233  function attachValues(params, values) {
234    if (!values) return;
235    params.user_data = values;
236  }
237
238  /* ─────────────────────────────────────────────
239   * META CAPI — hashed email (em) / phone (ph) on gf_form_submit
240   * Normalized values are hashed with SHA-256 (hex). No raw PII in dataLayer.
241   * ───────────────────────────────────────────── */
242
243  function normalizeMetaHaystackPart(s) {
244    if (!s) return '';
245    return String(s).toLowerCase().replace(/[^a-z0-9]+/g, '_');
246  }
247
248  function buildMetaFieldHaystack(el) {
249    var label = getFieldLabel(el);
250    return [
251      normalizeMetaHaystackPart(slugifyLabel(label)),
252      normalizeMetaHaystackPart(el.name || ''),
253      normalizeMetaHaystackPart(el.id || '')
254    ].join('_');
255  }
256
257  function longestTokenInHaystack(haystack, tokens) {
258    var sorted = tokens.slice().sort(function (a, b) { return b.length - a.length; });
259    var ti;
260    for (ti = 0; ti < sorted.length; ti++) {
261      if (haystack.indexOf(sorted[ti]) !== -1) return sorted[ti];
262    }
263    return '';
264  }
265
266  function classifyMetaIdentifierField(el) {
267    var tag = el.tagName ? el.tagName.toLowerCase() : '';
268    if (tag !== 'input') return null;
269    var type = (el.type || '').toLowerCase();
270    if (type === 'hidden' || type === 'password' || type === 'file' || type === 'submit' || type === 'button' || type === 'checkbox' || type === 'radio') {
271      return null;
272    }
273    if (type === 'email') return 'email';
274    if (type === 'tel') return 'phone';
275    var haystack = buildMetaFieldHaystack(el);
276    if (longestTokenInHaystack(haystack, EMAIL_META_TOKENS)) return 'email';
277    if (longestTokenInHaystack(haystack, PHONE_META_TOKENS)) return 'phone';
278    return null;
279  }
280
281  function normalizeEmailForMetaHash(value) {
282    if (value === null || value === undefined) return '';
283    return String(value).trim().toLowerCase();
284  }
285
286  function normalizePhoneForMetaHash(value) {
287    if (value === null || value === undefined) return '';
288    return String(value).replace(/\D/g, '');
289  }
290
291  /**
292   * Coerce a configured country code (`1`, `+1`, `+ 1`, `44`, …) to `+` + digits.
293   * Falls back to `+1` when the configured value contains no digits at all.
294   */
295  function normalizeCountryCode(cc) {
296    var digits = String(cc === null || cc === undefined ? '' : cc).replace(/\D/g, '');
297    if (!digits) return '+1';
298    return '+' + digits;
299  }
300
301  /**
302   * Normalize a user-typed phone number to E.164 (`+` + country code + subscriber digits).
303   *
304   * Google Enhanced Conversions and Microsoft Enhanced Conversions both require
305   * E.164 *with* the leading `+` before SHA-256. Meta wants the same digits but
306   * without the `+`.
307   *
308   * HEURISTIC LIMITS: a number typed without a leading `+` carries no reliable
309   * signal about its country. We can only guess:
310   *   - a leading `+` is authoritative — trust it and keep the digits as typed
311   *   - 11 digits starting with `1` is treated as NANP with the country code already present
312   *   - anything else gets the configured default country code prefixed
313   * That last rule will mis-tag an international number a visitor typed in local
314   * form (e.g. a UK "020 7946 0958" on a US-default site). This is exactly why the
315   * default country code is a configurable setting rather than a hardcoded `+1`:
316   * site owners whose traffic is predominantly non-US must set it to match.
317   */
318  function normalizePhoneE164(value, defaultCc) {
319    if (value === null || value === undefined) return '';
320    var str = String(value).trim();
321    var hasPlus = str.charAt(0) === '+';
322    var digits = str.replace(/\D/g, '');
323    if (digits.length < 7) return '';
324    if (hasPlus) return '+' + digits;
325    if (digits.length === 11 && digits.charAt(0) === '1') return '+' + digits;
326    return normalizeCountryCode(defaultCc) + digits;
327  }
328
329  /**
330   * Microsoft Enhanced Conversions normalizes email more aggressively than
331   * Google/Meta: after trim + lowercase it also strips every `.` from the user
332   * portion and truncates at the first `+` alias. The domain is left untouched.
333   */
334  function normalizeEmailForMicrosoft(value) {
335    if (value === null || value === undefined) return '';
336    var email = String(value).trim().toLowerCase();
337    var at = email.indexOf('@');
338    if (at === -1) return '';
339    var user = email.slice(0, at).replace(/\./g, '');
340    var domain = email.slice(at + 1);
341    var plus = user.indexOf('+');
342    if (plus !== -1) user = user.slice(0, plus);
343    if (!user || !domain) return '';
344    return user + '@' + domain;
345  }
346
347  function isPlausibleMetaEmail(normalized) {
348    return normalized.length > 3 && normalized.indexOf('@') !== -1 && normalized.indexOf('.', normalized.indexOf('@')) !== -1;
349  }
350
351  function isPlausibleMetaPhone(digits) {
352    return digits.length >= 7;
353  }
354
355  var metaIdentifierValues = {};
356
357  function collectNormalizedEmailPhoneFromForm(form) {
358    var out = { email: '', phone: '', phoneE164: '' };
359    if (!form || !form.querySelectorAll) return out;
360    var fields = form.querySelectorAll('.gfield input');
361    var fi;
362    for (fi = 0; fi < fields.length; fi++) {
363      var el = fields[fi];
364      var cat = classifyMetaIdentifierField(el);
365      if (!cat) continue;
366      var raw = (el.value || '').trim();
367      if (!raw) continue;
368      if (cat === 'email' && !out.email) {
369        var ne = normalizeEmailForMetaHash(raw);
370        if (isPlausibleMetaEmail(ne)) out.email = ne;
371      } else if (cat === 'phone' && !out.phone) {
372        var np = normalizePhoneForMetaHash(raw);
373        if (isPlausibleMetaPhone(np)) {
374          out.phone = np;
375          out.phoneE164 = normalizePhoneE164(raw, DEFAULT_COUNTRY_CODE);
376        }
377      }
378    }
379    return out;
380  }
381
382  /**
383   * Legacy shape, frozen: `{ em, ph }` with `ph` hashed from digits-only, no
384   * country code. Live GTM tags read `meta_user_data_hashed` — do not change
385   * these values. Per-platform corrections live in `user_data_hashed` instead.
386   */
387  function buildMetaUserDataHashedObject(norm) {
388    if (!META_HASHED_IDS || !sha256Hex || !norm) return null;
389    var obj = {};
390    if (norm.email) obj.em = sha256Hex(norm.email);
391    if (norm.phone) obj.ph = sha256Hex(norm.phone);
392    if (!Object.keys(obj).length) return null;
393    return obj;
394  }
395
396  /**
397   * Per-platform hashed identifiers. Each vendor normalizes differently:
398   *   meta      — trim/lower email; E.164 digits WITHOUT the leading `+`
399   *   google    — trim/lower email; full E.164 WITH the `+`
400   *   microsoft — dot/alias-stripped email; full E.164 WITH the `+`
401   */
402  function buildUserDataHashedObject(norm) {
403    if (!META_HASHED_IDS || !sha256Hex || !norm) return null;
404
405    var email = norm.email || '';
406    var e164  = norm.phoneE164 || '';
407    var msEmail = email ? normalizeEmailForMicrosoft(email) : '';
408
409    var emHash = email ? sha256Hex(email) : '';
410    var msEmHash = msEmail ? sha256Hex(msEmail) : '';
411    var phPlus = e164 ? sha256Hex(e164) : '';
412    var phBare = e164 ? sha256Hex(e164.replace(/^\+/, '')) : '';
413
414    var out = {};
415
416    function addPlatform(key, em, ph) {
417      var obj = {};
418      if (em) obj.em = em;
419      if (ph) obj.ph = ph;
420      if (Object.keys(obj).length) out[key] = obj;
421    }
422
423    addPlatform('meta', emHash, phBare);
424    addPlatform('google', emHash, phPlus);
425    addPlatform('microsoft', msEmHash, phPlus);
426
427    if (!Object.keys(out).length) return null;
428    return out;
429  }
430
431  function buildHashedPayloads(norm) {
432    return {
433      h: buildMetaUserDataHashedObject(norm),
434      u: buildUserDataHashedObject(norm)
435    };
436  }
437
438  function cacheMetaIdentifierValue(el) {
439    if (!META_HASHED_IDS || !el) return;
440    var formId = getFormId(el);
441    if (!formId || formId === 'unknown') return;
442    var cat = classifyMetaIdentifierField(el);
443    if (!cat) return;
444    var raw = (el.value || '').trim();
445    if (!raw) return;
446    metaIdentifierValues[formId] = metaIdentifierValues[formId] || { email: '', phone: '', phoneE164: '' };
447    if (cat === 'email' && !metaIdentifierValues[formId].email) {
448      var ne = normalizeEmailForMetaHash(raw);
449      if (isPlausibleMetaEmail(ne)) metaIdentifierValues[formId].email = ne;
450    } else if (cat === 'phone' && !metaIdentifierValues[formId].phone) {
451      var np = normalizePhoneForMetaHash(raw);
452      if (isPlausibleMetaPhone(np)) {
453        metaIdentifierValues[formId].phone = np;
454        metaIdentifierValues[formId].phoneE164 = normalizePhoneE164(raw, DEFAULT_COUNTRY_CODE);
455      }
456    }
457  }
458
459  function persistMetaHashesForRedirect(formId, form) {
460    if (!META_HASHED_IDS || !sha256Hex) return;
461    var norm = collectNormalizedEmailPhoneFromForm(form);
462    var payloads = buildHashedPayloads(norm);
463    if (!payloads.h && !payloads.u) return;
464    try {
465      // `h` is the frozen legacy shape; `u` is the per-platform sibling. Both are
466      // already SHA-256 hex — no raw PII ever reaches sessionStorage.
467      window.sessionStorage.setItem(SS_META_HASH_PREFIX + formId, JSON.stringify({
468        t: now(),
469        h: payloads.h,
470        u: payloads.u
471      }));
472    } catch (e) { /* sessionStorage unavailable / quota */ }
473  }
474
475  function persistMetaHashesFromSubmitButton(el) {
476    if (!META_HASHED_IDS || !sha256Hex || !el || !el.closest) return;
477    var form = el.closest('form[id^="gform_"]');
478    if (!form) return;
479    var match = form.id.match(/gform_(\d+)/);
480    if (!match) return;
481    persistMetaHashesForRedirect(match[1], form);
482  }
483
484  function hasLegacyHashes(payloads) {
485    return !!(payloads && payloads.h && (payloads.h.em || payloads.h.ph));
486  }
487
488  /**
489   * Returns `{ h, u }`. Entries written by an older version of this script hold
490   * only `{ t, h }` — `u` is then simply absent and the redirect confirmation
491   * emits `meta_user_data_hashed` alone rather than throwing.
492   */
493  function readPersistedMetaHashes(formId) {
494    try {
495      var raw = window.sessionStorage.getItem(SS_META_HASH_PREFIX + formId);
496      if (!raw) return null;
497      window.sessionStorage.removeItem(SS_META_HASH_PREFIX + formId);
498      var parsed = JSON.parse(raw);
499      if (!parsed || typeof parsed !== 'object') return null;
500      if (now() - parsed.t > SS_TTL_MS) return null;
501      var h = (parsed.h && typeof parsed.h === 'object') ? parsed.h : null;
502      var u = (parsed.u && typeof parsed.u === 'object') ? parsed.u : null;
503      if (h && !h.em && !h.ph) h = null;
504      if (u && !Object.keys(u).length) u = null;
505      if (!h && !u) return null;
506      return { h: h, u: u };
507    } catch (e) { return null; }
508  }
509
510  function attachMetaUserDataHashed(params, formId) {
511    if (!META_HASHED_IDS || !sha256Hex) return;
512
513    // Tier 1: hashes persisted at submit time (survives a redirect confirmation).
514    var payloads = readPersistedMetaHashes(formId);
515
516    // Tier 2: in-memory cache from input/change events (survives AJAX DOM swap).
517    if (!hasLegacyHashes(payloads) && metaIdentifierValues[formId]) {
518      payloads = buildHashedPayloads(metaIdentifierValues[formId]);
519    }
520
521    // Tier 3: live re-read of the form still in the DOM.
522    if (!hasLegacyHashes(payloads)) {
523      var formEl = document.getElementById('gform_' + formId);
524      if (formEl) {
525        payloads = buildHashedPayloads(collectNormalizedEmailPhoneFromForm(formEl));
526      }
527    }
528
529    if (!payloads) return;
530    if (hasLegacyHashes(payloads)) {
531      params.meta_user_data_hashed = payloads.h;
532    }
533    if (payloads.u && Object.keys(payloads.u).length) {
534      params.user_data_hashed = payloads.u;
535    }
536  }
537
538
539  /* ─────────────────────────────────────────────
540   * TIMING STATE
541   * Tracks when the user first interacted with each form
542   * so we can report time-on-form at submit / abandon.
543   * ───────────────────────────────────────────── */
544
545  var formStartTimes  = {};
546  var fieldStartTimes = {};
547
548  function markFormStart(formId) {
549    if (!formStartTimes[formId]) {
550      formStartTimes[formId] = now();
551    }
552  }
553
554  function getFormDuration(formId) {
555    if (!formStartTimes[formId]) return 0;
556    return Math.round((now() - formStartTimes[formId]) / 1000);
557  }
558
559
560  /* ─────────────────────────────────────────────
561   * 1. FORM VIEW
562   * Fires once per form when it becomes visible.
563   * ───────────────────────────────────────────── */
564
565  function trackFormViews() {
566    var observed = {};
567
568    function observe(forms) {
569      forms.forEach(function (form) {
570        if (observed[form.id]) return;
571        observed[form.id] = true;
572
573        var match  = form.id.match(/gform_(\d+)/);
574        var formId = match ? match[1] : 'unknown';
575
576        if ('IntersectionObserver' in window) {
577          var io = new IntersectionObserver(function (entries, obs) {
578            entries.forEach(function (entry) {
579              if (entry.isIntersecting) {
580                push('gf_form_view', {
581                  form_id:    formId,
582                  form_name:  getFormName(formId),
583                  form_type:  getFormType(formId),
584                  page_path:  window.location.pathname,
585                  page_title: document.title
586                });
587                obs.disconnect();
588              }
589            });
590          }, { threshold: 0.25 });
591          io.observe(form);
592        } else {
593          push('gf_form_view', {
594            form_id:    formId,
595            form_name:  getFormName(formId),
596            form_type:  getFormType(formId),
597            page_path:  window.location.pathname,
598            page_title: document.title
599          });
600        }
601      });
602    }
603
604    observe(document.querySelectorAll('form[id^="gform_"]'));
605
606    if (window.jQuery) {
607      jQuery(document).on('gform_post_render', function () {
608        observe(document.querySelectorAll('form[id^="gform_"]'));
609      });
610    }
611  }
612
613
614  /* ─────────────────────────────────────────────
615   * 2. FIELD FOCUS (first interaction with a field)
616   * ───────────────────────────────────────────── */
617
618  var focusedFields = {};
619
620  document.addEventListener('focus', function (e) {
621    var el = e.target;
622    if (!el.matches || !el.matches('.gfield input, .gfield select, .gfield textarea')) return;
623
624    var formId = getFormId(el);
625    var key    = formId + '|' + (el.name || el.id);
626
627    markFormStart(formId);
628
629    if (focusedFields[key]) {
630      if (TIMING) fieldStartTimes[key] = now();
631      return;
632    }
633    focusedFields[key] = true;
634    if (TIMING) fieldStartTimes[key] = now();
635
636    push('gf_field_focus', {
637      form_id:     formId,
638      form_name:   getFormName(formId),
639      form_type:   getFormType(formId),
640      field_label: getFieldLabel(el),
641      field_type:  el.type || el.tagName.toLowerCase(),
642      field_name:  el.name || el.id,
643      page_path:   window.location.pathname,
644      page_title:  document.title
645    });
646  }, true);
647
648
649  /* ─────────────────────────────────────────────
650   * 3. FIELD CHANGE (value was modified)
651   * ───────────────────────────────────────────── */
652
653  document.addEventListener('change', function (e) {
654    var el = e.target;
655    if (!el.matches || !el.matches('.gfield input, .gfield select, .gfield textarea')) return;
656    if (el.type === 'hidden') return;
657
658    var formId = getFormId(el);
659    var key    = formId + '|' + (el.name || el.id);
660    var params = {
661      form_id:     formId,
662      form_name:   getFormName(formId),
663      form_type:   getFormType(formId),
664      field_label: getFieldLabel(el),
665      field_type:  el.type || el.tagName.toLowerCase(),
666      field_name:  el.name || el.id,
667      page_path:   window.location.pathname,
668      page_title:  document.title
669    };
670
671    if (TIMING && fieldStartTimes[key]) {
672      params.field_duration_sec = Math.round((now() - fieldStartTimes[key]) / 1000);
673    }
674
675    cacheFieldValue(el);
676    cacheMetaIdentifierValue(el);
677
678    push('gf_field_change', params);
679  }, true);
680
681  document.addEventListener('input', function (e) {
682    var el = e.target;
683    if (!el.matches || !el.matches('.gfield input, .gfield select, .gfield textarea')) return;
684    if (el.type === 'hidden') return;
685    cacheFieldValue(el);
686    cacheMetaIdentifierValue(el);
687  }, true);
688
689  document.addEventListener('submit', function (e) {
690    var form = e.target;
691    if (!form || !form.id || form.id.indexOf('gform_') !== 0) return;
692    var match = form.id.match(/gform_(\d+)/);
693    if (!match) return;
694    var formId = match[1];
695    persistFormValues(formId);
696    persistFormMeta(formId, form);
697    persistMetaHashesForRedirect(formId, form);
698  }, true);
699
700  document.addEventListener('click', function (e) {
701    var el = e.target;
702    if (!el || !el.matches) return;
703    if (!el.matches('input[id^="gform_submit_button_"], button[id^="gform_submit_button_"]')) return;
704    persistMetaHashesFromSubmitButton(el);
705  }, true);
706
707
708  /* ─────────────────────────────────────────────
709   * 4. FIELD BLUR (left a field empty — drop-off signal)
710   * ───────────────────────────────────────────── */
711
712  document.addEventListener('blur', function (e) {
713    var el = e.target;
714    if (!el.matches || !el.matches('.gfield input, .gfield select, .gfield textarea')) return;
715    if (el.type === 'hidden' || el.type === 'submit') return;
716
717    var isEmpty = el.value === '' || el.value === null;
718    var formId  = getFormId(el);
719    var key     = formId + '|' + (el.name || el.id);
720
721    if (focusedFields[key] && isEmpty) {
722      push('gf_field_abandoned', {
723        form_id:     formId,
724        form_name:   getFormName(formId),
725        form_type:   getFormType(formId),
726        field_label: getFieldLabel(el),
727        field_type:  el.type || el.tagName.toLowerCase(),
728        field_name:  el.name || el.id,
729        page_path:   window.location.pathname,
730        page_title:  document.title
731      });
732    }
733  }, true);
734
735
736  /* ─────────────────────────────────────────────
737   * 5. MULTI-PAGE STEP ADVANCE
738   * ───────────────────────────────────────────── */
739
740  if (window.jQuery) {
741    jQuery(document).on('gform_page_loaded', function (e, formId, currentPage) {
742      push('gf_page_advance', {
743        form_id:     String(formId),
744        form_name:   getFormName(String(formId)),
745        form_type:   getFormType(String(formId)),
746        step_number: currentPage,
747        step_name:   'Step ' + currentPage,
748        page_path:   window.location.pathname,
749        page_title:  document.title
750      });
751    });
752  }
753
754
755  /* ─────────────────────────────────────────────
756   * 6. VALIDATION ERRORS
757   * ───────────────────────────────────────────── */
758
759  if (window.jQuery) {
760    jQuery(document).on('gform_post_render', function (e, formId, pageNum) {
761      var form = document.getElementById('gform_' + formId);
762      if (!form) return;
763
764      var errorFields = form.querySelectorAll('.gfield_error');
765      if (!errorFields.length) return;
766
767      var errorDetails = Array.from(errorFields).map(function (gfield) {
768        var label = gfield.querySelector('label');
769        var msgEl = gfield.querySelector('.validation_message');
770        return {
771          label:   label ? label.textContent.trim() : 'unknown field',
772          message: msgEl ? msgEl.textContent.trim() : ''
773        };
774      });
775
776      var errorLabels = errorDetails.map(function (d) { return d.label; });
777      var errorMessages = errorDetails.map(function (d) { return d.message; });
778
779      push('gf_validation_error', {
780        form_id:        String(formId),
781        form_name:      getFormName(String(formId)),
782        form_type:      getFormType(String(formId)),
783        step_number:    pageNum,
784        error_count:    errorFields.length,
785        error_fields:   errorLabels.join(' | '),
786        error_messages: errorMessages.join(' | '),
787        page_path:      window.location.pathname,
788        page_title:     document.title
789      });
790
791      errorDetails.forEach(function (detail) {
792        push('gf_field_validation_error', {
793          form_id:       String(formId),
794          form_name:     getFormName(String(formId)),
795          form_type:     getFormType(String(formId)),
796          step_number:   pageNum,
797          field_label:   detail.label,
798          error_message: detail.message,
799          page_path:     window.location.pathname,
800          page_title:    document.title
801        });
802      });
803    });
804  }
805
806
807  /* ─────────────────────────────────────────────
808   * 7. FORM SUBMIT SUCCESS
809   * ───────────────────────────────────────────── */
810
811  if (window.jQuery) {
812    jQuery(document).on('gform_confirmation_loaded', function (e, formId) {
813      var fid = String(formId);
814      var params = {
815        form_id:    fid,
816        form_name:  getFormName(fid),
817        form_type:  getFormType(fid),
818        post_name:  postName,
819        page_path:  window.location.pathname,
820        page_title: document.title
821      };
822      if (TIMING) {
823        params.time_on_form_sec = getFormDuration(fid);
824      }
825      if (INCLUDE_USER_DATA) {
826        attachValues(params, formValues[fid]);
827      }
828      attachMetaUserDataHashed(params, fid);
829      if (window.usmrAttribution) { params.attribution = window.usmrAttribution; }
830      push('gf_form_submit', params);
831    });
832  }
833
834  // Non-AJAX redirect confirmations
835  (function () {
836    var urlParams = new URLSearchParams(window.location.search);
837    if (urlParams.get('gf_page') === 'confirmation') {
838      var formId = urlParams.get('id') || 'unknown';
839      var params = {
840        form_id:           formId,
841        form_name:         getFormName(formId),
842        form_type:         getFormType(formId),
843        post_name:         postName,
844        page_path:         window.location.pathname,
845        page_title:        document.title,
846        confirmation_type: 'redirect'
847      };
848      if (INCLUDE_USER_DATA && formId !== 'unknown') {
849        attachValues(params, readPersistedValues(formId));
850      }
851      if (formId !== 'unknown') {
852        attachMetaUserDataHashed(params, formId);
853      }
854      if (window.usmrAttribution) { params.attribution = window.usmrAttribution; }
855      push('gf_form_submit', params);
856    }
857  })();
858
859
860  /* ─────────────────────────────────────────────
861   * 8. FORM ABANDONMENT (user navigates away mid-fill)
862   * Only fires if at least one field was interacted with
863   * and the form was NOT successfully submitted.
864   * ───────────────────────────────────────────── */
865
866  if (ABANDON) {
867    var submitted = {};
868
869    if (window.jQuery) {
870      jQuery(document).on('gform_confirmation_loaded', function (e, formId) {
871        submitted[String(formId)] = true;
872      });
873    }
874
875    window.addEventListener('beforeunload', function () {
876      Object.keys(formStartTimes).forEach(function (formId) {
877        if (submitted[formId]) return;
878
879        var filledCount = 0;
880        Object.keys(focusedFields).forEach(function (key) {
881          if (key.indexOf(formId + '|') === 0) filledCount++;
882        });
883
884        if (filledCount === 0) return;
885
886        push('gf_form_abandoned', {
887          form_id:          formId,
888          form_name:        getFormName(formId),
889          form_type:        getFormType(formId),
890          fields_filled:    filledCount,
891          time_on_form_sec: getFormDuration(formId),
892          page_path:        window.location.pathname,
893          page_title:       document.title
894        });
895      });
896    });
897  }
898
899
900  /* ─────────────────────────────────────────────
901   * INIT
902   * ───────────────────────────────────────────── */
903
904  if (document.readyState === 'loading') {
905    document.addEventListener('DOMContentLoaded', trackFormViews);
906  } else {
907    trackFormViews();
908  }
909
910})();

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.