PageSourceSearch

https://www.finnovate.in/assets/js/finn-tracking.js

js finnovate.in collected 2026-10-04 02:36:30 UTC 13,842 bytes, 349 lines download raw bytes

1/*!
2 * Finnovate unified event tracking — A-03 (Digital Implementation Tracker)
3 * -----------------------------------------------------------------------
4 * One shared, self-initializing helper that pushes a consistent event schema
5 * to BOTH the GTM dataLayer and GA4 (direct gtag), so tags can be built once
6 * in GTM against these events.
7 *
8 * Auto-fired (no per-page code needed once this file is included):
9 *   - scroll_milestone      : percent_scrolled = 25 | 50 | 75 | 100 (once each)
10 *   - form_submit           : any <form> submit (add data-no-track to opt out)
11 *   - calculator_start      : first interaction inside a [data-calc] container
12 *
13 * Manual / attribute-driven:
14 *   - calculator_step       : focusin on a [data-calc-step="N"] group (once per step)
15 *   - calculator_complete   : click on a [data-calc-complete] element, OR
16 *                             call finnTrackCalcComplete(name, params) once a result renders
17 *   - any custom event      : finnTrack(name, params)  e.g. finnbook / finnfit RSVP / cashfree_initiate
18 *
19 * Every event automatically carries: page_path, page_title.
20 * Safe to include anywhere: no dependency on jQuery; degrades quietly if gtag
21 * or dataLayer is absent.
22 */
23(function (w, d) {
24  'use strict';
25
26  w.dataLayer = w.dataLayer || [];
27
28  function gtagSafe() {
29    if (typeof w.gtag === 'function') {
30      try { w.gtag.apply(null, arguments); } catch (e) {}
31    }
32  }
33
34  function baseParams() {
35    return {
36      page_path: (w.location && w.location.pathname) || '',
37      page_title: (d && d.title) || ''
38    };
39  }
40
41  function assign(target) {
42    for (var i = 1; i < arguments.length; i++) {
43      var src = arguments[i];
44      if (!src) continue;
45      for (var k in src) { if (Object.prototype.hasOwnProperty.call(src, k)) { target[k] = src[k]; } }
46    }
47    return target;
48  }
49
50  // Events that also go to Meta (browser Pixel + first-party server bridge -> CAPI).
51  // Map: our event name -> Meta custom event name (must match server_conversions.php and
52  // track-event.php's allowlist). finnfit_quiz_start/_q3 give Meta early/mid-funnel signal
53  // on the FinnFit Quiz instead of only the final Lead — quiz_q3 is the 60%-progress
54  // milestone, used here as "roughly halfway", not a literal 50% checkpoint.
55  var CAPI_META_EVENTS = {
56    calculator_complete: 'CalculatorComplete',
57    take_finnfit_test: 'TakeFinnFitTest',
58    finnfit_quiz_start: 'FinnFitQuizStart',
59    finnfit_quiz_q3: 'FinnFitQuizHalfway',
60  };
61
62  function readCookie(name) {
63    var m = d.cookie.match(new RegExp('(?:^|; )' + name.replace(/([.$?*|{}()\[\]\\\/+^])/g, '\\$1') + '=([^;]*)'));
64    return m ? decodeURIComponent(m[1]) : '';
65  }
66  function getFbclid() {
67    var m = (w.location.search || '').match(/[?&]fbclid=([^&]+)/);
68    return m ? decodeURIComponent(m[1]) : '';
69  }
70  function getFbc() {
71    var fbc = readCookie('_fbc');
72    if (fbc) return fbc;
73    var id = getFbclid();
74    return id ? ('fb.1.' + Date.now() + '.' + id) : '';
75  }
76
77  // Meta Test Events code, read only from the URL — never a server-side toggle. A
78  // real visitor's URL never carries this, so test traffic can never leak into (or
79  // divert) production conversion data; only someone deliberately testing with
80  // ?fb_test_code=... in the address bar activates it, and only for their own
81  // requests. See ad/server_conversions.php's fhcSendMetaCapi() for the other end.
82  function getFbTestCode() {
83    var m = (w.location.search || '').match(/[?&]fb_test_code=([^&]+)/);
84    return m ? decodeURIComponent(m[1]) : '';
85  }
86  w.finnFbTestCode = getFbTestCode();
87
88  // GA4's own _ga cookie, so lead-capture forms can stitch a CRM record back to the
89  // browser session that produced it. Cookie shape: GA1.<depth>.<clientId part 1>.<part 2>.
90  function getGaClientId() {
91    var raw = readCookie('_ga');
92    var m = raw.match(/GA\d\.\d\.(\d+\.\d+)$/);
93    return m ? m[1] : '';
94  }
95  w.finnGaClientId = getGaClientId();
96
97  // Forward an anonymous top-funnel event to Meta: browser Pixel (if present) with a
98  // shared event_id, plus a first-party POST that the server relays to Meta CAPI.
99  // The shared event_id lets Meta dedupe the browser + server copies.
100  function sendCapiEvent(name, params) {
101    var metaEvent = CAPI_META_EVENTS[name];
102    if (!metaEvent) return;
103    var eventId = name + '.' + Date.now() + '.' + Math.floor(Math.random() * 1e6);
104    var calc = (params && params.calculator) || '';
105    if (typeof w.fbq === 'function') {
106      try { w.fbq('trackCustom', metaEvent, { calculator: calc }, { eventID: eventId }); } catch (e) {}
107    }
108    try {
109      if (w.fetch) {
110        w.fetch('/ad/track-event.php', {
111          method: 'POST',
112          headers: { 'Content-Type': 'application/json' },
113          body: JSON.stringify({
114            event: name, event_id: eventId,
115            fbp: readCookie('_fbp'), fbc: getFbc(), fbclid: getFbclid(),
116            page_url: w.location.href, calculator: calc,
117            test_event_code: w.finnFbTestCode || ''
118          }),
119          keepalive: true
120        }).catch(function () {});
121      }
122    } catch (e) {}
123  }
124
125  // Public: fire an arbitrary event to dataLayer + GA4.
126  function finnTrack(name, params) {
127    if (!name) return;
128    var payload = assign({}, baseParams(), params || {});
129    // GTM dataLayer (event key drives GTM triggers)
130    w.dataLayer.push(assign({ event: name }, payload));
131    // GA4 direct gtag
132    gtagSafe('event', name, payload);
133    // Meta CAPI bridge for the allowlisted anonymous events
134    if (CAPI_META_EVENTS[name]) { sendCapiEvent(name, params); }
135  }
136  w.finnTrack = finnTrack;
137
138  // Most calculators auto-compute a default result on page load, before the user
139  // touches anything. We only count a "complete" once the user has actually
140  // interacted, so the auto-load default render does not inflate the metric.
141  var _userInteracted = false;
142  function markInteracted() { _userInteracted = true; }
143  w.finnMarkCalcInteracted = markInteracted;
144
145  // Public: dedupe-friendly calculator_complete for live-calculating pages.
146  // Suppressed until the user has interacted (unless params.force === true).
147  var _calcCompleteFired = {};
148  function finnTrackCalcComplete(name, params) {
149    params = params || {};
150    if (!_userInteracted && params.force !== true) return;
151    var key = name || 'calculator';
152    if (_calcCompleteFired[key]) return;
153    _calcCompleteFired[key] = true;
154    var out = assign({ calculator: key }, params);
155    delete out.force;
156    finnTrack('calculator_complete', out);
157  }
158  w.finnTrackCalcComplete = finnTrackCalcComplete;
159
160  // Resolve a calculator name: explicit [data-calc] value, else derive from URL.
161  function resolveCalcName() {
162    var el = d.querySelector('[data-calc]');
163    if (el && el.getAttribute('data-calc')) return el.getAttribute('data-calc');
164    var m = (w.location.pathname || '').match(/([a-z0-9]+)-calculator/i);
165    return m ? m[1].toLowerCase() : 'calculator';
166  }
167
168  // ---- Scroll milestones (25/50/75/100) ----
169  (function initScroll() {
170    var milestones = [25, 50, 75, 100];
171    var fired = {};
172    var ticking = false;
173
174    function check() {
175      ticking = false;
176      var docEl = d.documentElement, body = d.body;
177      var scrollTop = w.pageYOffset || docEl.scrollTop || body.scrollTop || 0;
178      var scrollable = Math.max(
179        body.scrollHeight, docEl.scrollHeight, body.offsetHeight, docEl.offsetHeight
180      ) - w.innerHeight;
181      if (scrollable <= 0) return;
182      var pct = Math.round((scrollTop / scrollable) * 100);
183      for (var i = 0; i < milestones.length; i++) {
184        var m = milestones[i];
185        if (pct >= m && !fired[m]) {
186          fired[m] = true;
187          finnTrack('scroll_milestone', { percent_scrolled: m });
188        }
189      }
190    }
191
192    w.addEventListener('scroll', function () {
193      if (ticking) return;
194      ticking = true;
195      w.setTimeout(check, 200);
196    }, { passive: true });
197  })();
198
199  // ---- Form submit ----
200  d.addEventListener('submit', function (e) {
201    var f = e.target;
202    if (!f || f.tagName !== 'FORM') return;
203    if (f.hasAttribute('data-no-track')) return;
204    finnTrack('form_submit', {
205      form_id: f.id || '',
206      form_name: f.getAttribute('name') || f.id || 'unknown'
207    });
208  }, true);
209
210  // ---- Calculator start / steps / complete ----
211  function initCalc() {
212    var calcName = resolveCalcName();
213    var containers = d.querySelectorAll('[data-calc]');
214
215    // calculator_start: first interaction anywhere inside a [data-calc] container.
216    var startFired = false;
217    function onFirstInteract() {
218      markInteracted(); // unlocks calculator_complete counting
219      if (startFired) return;
220      startFired = true;
221      finnTrack('calculator_start', { calculator: calcName });
222    }
223    if (containers.length) {
224      for (var c = 0; c < containers.length; c++) {
225        containers[c].addEventListener('input', onFirstInteract, { once: false });
226        containers[c].addEventListener('change', onFirstInteract, { once: false });
227      }
228    }
229
230    // calculator_step: focusin on a [data-calc-step="N"] group (once per step).
231    var stepFired = {};
232    var stepGroups = d.querySelectorAll('[data-calc-step]');
233    for (var s = 0; s < stepGroups.length; s++) {
234      (function (grp) {
235        var step = grp.getAttribute('data-calc-step');
236        grp.addEventListener('focusin', function () {
237          if (stepFired[step]) return;
238          stepFired[step] = true;
239          finnTrack('calculator_step', { calculator: calcName, step: Number(step) || step });
240        });
241      })(stepGroups[s]);
242    }
243
244    // calculator_complete: any [data-calc-complete] click.
245    var completeEls = d.querySelectorAll('[data-calc-complete]');
246    for (var b = 0; b < completeEls.length; b++) {
247      completeEls[b].addEventListener('click', function () {
248        finnTrackCalcComplete(calcName);
249      });
250    }
251
252    // calculator_complete (generic): watch the primary result element(s) marked
253    // [data-calc-result]. When their rendered value changes AFTER the user has
254    // interacted, fire once (deduped + interaction-gated in finnTrackCalcComplete).
255    // This avoids hooking each calculator's bespoke compute function.
256    var resultEls = d.querySelectorAll('[data-calc-result]');
257    if (resultEls.length && w.MutationObserver) {
258      var obs = new w.MutationObserver(function () {
259        finnTrackCalcComplete(calcName);
260      });
261      for (var r = 0; r < resultEls.length; r++) {
262        obs.observe(resultEls[r], { childList: true, characterData: true, subtree: true });
263      }
264    }
265  }
266
267  if (d.readyState === 'loading') {
268    d.addEventListener('DOMContentLoaded', initCalc);
269  } else {
270    initCalc();
271  }
272
273  // ---- Calendly: one clean event for the popup open, and one for the booking ----
274  // calendly_open  -> any click on a Calendly trigger (works on every page, one GTM trigger).
275  // calendly_booked -> Calendly's own postMessage when a meeting is actually scheduled
276  //                    (client-side booking signal; dedupe against the server-side CAPI Schedule).
277  (function initCalendly() {
278    // Delegated click: catch any element that opens Calendly, however it's wired.
279    d.addEventListener('click', function (e) {
280      var t = e.target;
281      if (!t || !t.closest) return;
282      var el = t.closest('a.CalendlyPopup, [onclick*="Calendly"], a[href*="calendly.com"], [data-calendly]');
283      if (!el) return;
284      finnTrack('calendly_open', {
285        cta_text: (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 80),
286        cta_href: el.getAttribute('href') || ''
287      });
288    }, true);
289
290    // Calendly embeds/popups post a message on scheduling.
291    w.addEventListener('message', function (e) {
292      var data = e && e.data;
293      if (!data || typeof data !== 'object' || data.event !== 'calendly.event_scheduled') return;
294      var p = data.payload || {};
295      finnTrack('calendly_booked', {
296        calendly_event_uri: (p.event && p.event.uri) || '',
297        calendly_invitee_uri: (p.invitee && p.invitee.uri) || ''
298      });
299    });
300  })();
301
302  // ---- take_finnfit_test: any click on a FinnFit CTA (links to plan.finnovate.in/finnfit) ----
303  // Fires wherever this script loads, including pages custom.js's cta_clicked handler misses
304  // (e.g. index.html, ad/ffc.html). One clean event name to build the FinnFit funnel on.
305  d.addEventListener('click', function (e) {
306    var t = e.target;
307    if (!t || !t.closest) return;
308    var el = t.closest('a[href*="plan.finnovate.in/finnfit"], [data-track-click*="finnfit"]');
309    if (!el) return;
310    finnTrack('take_finnfit_test', {
311      cta_text: (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 80),
312      cta_href: el.getAttribute('href') || '',
313      cta_source: el.getAttribute('data-track-click') || ''
314    });
315  }, true);
316
317  // ---- time_on_page_60s: fires once, only counting time the tab was actually visible ----
318  (function initTimeOnPage() {
319    var THRESHOLD_MS = 60000;
320    var accumulated = 0;
321    var visibleSince = (d.visibilityState === 'visible') ? Date.now() : null;
322    var fired = false;
323
324    function flush() {
325      if (visibleSince !== null) {
326        accumulated += Date.now() - visibleSince;
327        visibleSince = null;
328      }
329    }
330    function checkFire() {
331      if (fired || accumulated < THRESHOLD_MS) return;
332      fired = true;
333      finnTrack('time_on_page_60s', {});
334    }
335    d.addEventListener('visibilitychange', function () {
336      if (d.visibilityState === 'visible') {
337        visibleSince = Date.now();
338      } else {
339        flush();
340        checkFire();
341      }
342    });
343    var timer = w.setInterval(function () {
344      if (fired) { w.clearInterval(timer); return; }
345      if (visibleSince !== null) { flush(); visibleSince = Date.now(); }
346      checkFire();
347    }, 5000);
348  })();
349})(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.