PageSourceSearch

https://www.go4whatsup.com/assets/js/cta-tracker.js?v=202609081720

js go4whatsup.com collected 2026-10-01 12:12:01 UTC 10,074 bytes, 226 lines download raw bytes

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.