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.