1/*! 2 * Go4whatsup â Site-wide CTA Conversion Tracker 3 * ---------------------------------------------- 4 * Fires GA4 / Meta Pixel / TikTok Pixel events on every conversion-intent click 5 * across the site â book-demo CTAs, start-free links, Calendly clicks, 6 * phone-tap, email-click. Without this, organic + non-LP paid traffic 7 * shows 0 events in GA4 even when users genuinely convert. 8 * 9 * Auto-attaches via delegated click listener on document.body â works for 10 * SSR pages, dynamically injected content, and anchors added post-load. 11 * 12 * Dedupes: fires each event only once per element per session so a user 13 * double-clicking a button doesn't double-count. 14 * 15 * Events fired (canonical GA4 event names, all also marked as Key events in GA4 admin): 16 * generate_lead â canonical "conversion" event, fires on every CTA 17 * book_demo_cta_click â /request-demo/ link clicked from anywhere 18 * start_free_click â Registration link clicked 19 * book_demo_calendly â Calendly link clicked 20 * see_pricing_click â /pricing/ link clicked (soft conversion) 21 * phone_click â tel: link tapped (mobile conversion signal) 22 * email_click â mailto: link clicked 23 * 24 * Meta Pixel echo: fbq('trackCustom', 'CTAClick', ...) on every CTA click. 25 * NOTE (2026-09-06): renamed from fbq('track','Lead') per Fable audit â 26 * the standard 'Lead' event was polluted because every button click fired 27 * Lead, making Meta optimize for clickers not submitters. Now: 28 * - CTAClick = every book_demo/start_free/phone CTA click (upper funnel) 29 * - Lead = only real form submits (fired by meta-ads-events.js with event_id) 30 * + Calendly clicks (real intent to book) 31 * TikTok Pixel echo: ttq.track on Calendly + registration 32 * 33 * All fires are wrapped in try/catch so Consent Mode blocks or missing 34 * gtag/fbq/ttq don't break navigation. return true always so href follows. 35 */ 36(function(){ 37 'use strict'; 38 39 // Path-based skip: on already-converted pages, don't re-fire generate_lead. 40 // /thank-you/ = post-conversion. /holding-site/ = maintenance/temp landing. 41 var PATH = (location.pathname || '/').toLowerCase(); 42 var SKIP_PATHS = ['/thank-you', '/holding-site']; 43 if (SKIP_PATHS.some(function(p){ return PATH.indexOf(p) === 0; })) { 44 // Still expose the diagnostic hook so console tests confirm the skip 45 window.G4WCtaTracker = {version:'1.0.1', skippedForPath:PATH}; 46 return; 47 } 48 49 // Session-level dedup so the same user clicking a button 3x doesn't count 3x 50 var FIRED = new Set(); 51 function once(key){ 52 if (FIRED.has(key)) return false; 53 FIRED.add(key); 54 return true; 55 } 56 57 function fireGtag(name, params){ 58 try { 59 if (typeof window.gtag === 'function'){ 60 window.gtag('event', name, params || {}); 61 } 62 } catch(_){} 63 } 64 65 function fireFbq(action, params){ 66 try { 67 if (typeof window.fbq === 'function'){ 68 window.fbq('track', action, params || {}); 69 } 70 } catch(_){} 71 } 72 73 // 2026-09-06: use trackCustom for CTA clicks so we don't pollute the 74 // standard Lead event. Real Lead fires from form-submit (via 75 // meta-ads-events.js with event_id) + inline Calendly onclick handlers. 76 function fireFbqCustom(name, params){ 77 try { 78 if (typeof window.fbq === 'function'){ 79 window.fbq('trackCustom', name, params || {}); 80 } 81 } catch(_){} 82 } 83 84 function fireTtq(action, params){ 85 try { 86 if (typeof window.ttq !== 'undefined' && window.ttq && typeof window.ttq.track === 'function'){ 87 window.ttq.track(action, params || {}); 88 } 89 } catch(_){} 90 } 91 92 function classify(href){ 93 if (!href) return null; 94 var h = String(href).toLowerCase(); 95 if (h.indexOf('/request-demo') !== -1) return 'book_demo_cta'; 96 if (h.indexOf('app.go4whatsup.com/registration') !== -1) return 'start_free'; 97 if (h.indexOf('app.go4whatsup.com/pricing') !== -1) return 'see_pricing'; 98 if (h === '/pricing/' || h === '/pricing' || h.endsWith('/pricing/') || h.endsWith('/pricing')) return 'see_pricing'; 99 if (h.indexOf('calendly.com/') !== -1) return 'book_demo_calendly'; 100 if (h.indexOf('mailto:') === 0) return 'email'; 101 if (h.indexOf('tel:') === 0) return 'phone'; 102 return null; 103 } 104 105 function dedupeKey(kind, el){ 106 // Include href + a hash of the closest section id / text so different 107 // buttons of the same type on the same page each fire once. 108 var href = el.getAttribute('href') || ''; 109 var sect = ''; 110 var s = el.closest && el.closest('section, header, footer, main'); 111 if (s && s.id) sect = s.id; 112 var txt = (el.textContent || '').trim().slice(0, 40); 113 return kind + ':' + href + ':' + sect + ':' + txt; 114 } 115 116 function eventLabel(el){ 117 // Meaningful label so we can attribute conversion in GA4 reports â
118 // "which section/page pushed this CTA to a click" 119 var path = (location.pathname || '/').replace(/\/$/, ''); 120 var sect = ''; 121 var s = el.closest && el.closest('section, header, footer'); 122 if (s && s.id) sect = '#' + s.id; 123 else if (s && s.className) sect = '.' + String(s.className).split(/\s+/)[0]; 124 return (path || '/') + sect; 125 } 126 127 function handleClick(e){ 128 var a = e.target && e.target.closest && e.target.closest('a[href]'); 129 if (!a) return; 130 // Skip if: 131 // - Explicit opt-out marker 132 // - Element already has inline onclick fires from an earlier patch 133 // (marked with data-g4w-conversion). Prevents double-count on the 134 // 5 pages that shipped inline handlers on 2026-08-26 (/request-demo/, 135 // /thank-you/, /holding-site/, /holding-site/contact/, /industries/retail/). 136 // - onclick attribute contains gtag/generate_lead literal (defence-in-depth) 137 if (a.hasAttribute('data-g4w-skip-track')) return; 138 if (a.hasAttribute('data-g4w-conversion')) return; 139 var oc = a.getAttribute('onclick') || ''; 140 if (oc.indexOf('generate_lead') !== -1) return; 141 var kind = classify(a.getAttribute('href')); 142 if (!kind) return; 143 144 var key = dedupeKey(kind, a); 145 if (!once(key)) return; 146 147 var label = eventLabel(a); 148 149 switch (kind){ 150 case 'book_demo_cta': 151 fireGtag('book_demo_cta_click', {event_category:'conversion', event_label:label, value:1}); 152 fireGtag('generate_lead', {event_category:'conversion', event_label:label + ' | book_demo_cta', value:1}); 153 fireFbqCustom('CTAClick', {content_category:'book_demo_cta', source:label}); 154 fireTtq('SubmitForm', {content_name:'book_demo_cta_' + label}); 155 break; 156 case 'start_free': 157 fireGtag('start_free_click', {event_category:'conversion', event_label:label, value:1}); 158 fireGtag('generate_lead', {event_category:'conversion', event_label:label + ' | start_free', value:1}); 159 fireFbqCustom('CTAClick', {content_category:'start_free', source:label}); 160 fireTtq('CompleteRegistration', {content_name:'start_free_' + label}); 161 break; 162 case 'see_pricing': 163 // Soft conversion â pricing view is intent but not lead 164 fireGtag('see_pricing_click', {event_category:'engagement', event_label:label, value:1}); 165 fireFbq('ViewContent', {content_category:'pricing', source:label}); 166 break; 167 case 'book_demo_calendly': 168 // Calendly click IS real intent to book â keep as standard Lead so 169 // it counts alongside form-submit Lead fires from meta-ads-events.js. 170 fireGtag('book_demo_calendly', {event_category:'conversion', event_label:label, value:1}); 171 fireGtag('generate_lead', {event_category:'conversion', event_label:label + ' | calendly', value:1}); 172 fireFbq('Lead', {content_category:'calendly_click', source:label}); 173 fireTtq('CompletePayment', {content_name:'calendly_' + label}); 174 break; 175 case 'phone': 176 fireGtag('phone_click', {event_category:'conversion', event_label:label, value:1}); 177 fireGtag('generate_lead', {event_category:'conversion', event_label:label + ' | phone', value:1}); 178 fireFbqCustom('CTAClick', {content_category:'phone_click', source:label}); 179 break; 180 case 'email': 181 fireGtag('email_click', {event_category:'engagement', event_label:label, value:1}); 182 fireFbq('Contact', {content_category:'email_click', source:label}); 183 break; 184 } 185 } 186 187 // Delegated listener â catches all clicks including dynamically added anchors 188 document.addEventListener('click', handleClick, {capture:true, passive:true}); 189 190 // ---- 6. UTM propagation â carry paid-traffic UTMs through to demo form ----- 191 // When a paid ad lands the user on /industries/retail/?utm_source=meta&..., 192 // this appends the same UTMs to every /request-demo/ and /registration link 193 // on the page. Zoho form hidden fields then capture the source â attribution. 194 try { 195 var incoming = new URLSearchParams(window.location.search); 196 var utmKeys = ['utm_source','utm_medium','utm_campaign','utm_content','utm_term','gclid','fbclid']; 197 var carry = new URLSearchParams();
198 utmKeys.forEach(function(k){ if(incoming.has(k)) carry.set(k, incoming.get(k)); }); 199 if (carry.toString()) { 200 var propagate = function(){ 201 var anchors = document.querySelectorAll( 202 'a[href*="/request-demo"], a[href*="app.go4whatsup.com/registration"]' 203 ); 204 anchors.forEach(function(a){ 205 try { 206 var u = new URL(a.href, window.location.origin); 207 carry.forEach(function(v,k){ if(!u.searchParams.has(k)) u.searchParams.set(k,v); }); 208 a.href = u.toString(); 209 } catch(e){} 210 }); 211 }; 212 if (document.readyState === 'loading') { 213 document.addEventListener('DOMContentLoaded', propagate); 214 } else { 215 propagate(); 216 } 217 } 218 } catch(e){} 219 220 // Diagnostic hook â window.G4WCtaTracker for testing in console 221 window.G4WCtaTracker = { 222 version: '1.2.0', // 2026-09-09: UTM propagation for paid-traffic attribution 223 firedKeys: FIRED, 224 classify: classify 225 }; 226})();
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.