1/** 2 * WSN Funnel Tracker â front-end. 3 * 1. Anonymous visitor_id (first-party cookie, 2 yrs). 4 * 2. First-touch source (UTM / referrer / AI-chat / social / organic). 5 * 3. GA4 client_id. 6 * 4. Visitor's browser timezone (Intl API). 7 * 5. Identity stitching: if an identity token is present in the URL (param name 8 * from Settings, default `wsn_cid`), it is stored in a cookie and sent with 9 * every hit, so the anonymous visitor becomes a known contact â retroactively, 10 * since all history is keyed on visitor_id. 11 * 12 * No X-WP-Nonce on /track and /lead (public, CDN-safe). 13 */ 14(function () { 15 'use strict'; 16 17 var CFG = window.WSN_CFG || {}; 18 var COOKIE = 'wsn_vid'; 19 var SRC_COOKIE = 'wsn_src'; 20 var CID_COOKIE = 'wsn_cid'; 21 var ID_PARAM = CFG.id_param || 'wsn_cid'; 22 23 function setCookie(name, value, days) { 24 var d = new Date(); 25 d.setTime(d.getTime() + days * 864e5); 26 document.cookie = name + '=' + encodeURIComponent(value) + ';expires=' + d.toUTCString() + ';path=/;SameSite=Lax'; 27 } 28 function getCookie(name) { 29 return document.cookie.split('; ').reduce(function (acc, c) { 30 var parts = c.split('='); 31 return parts[0] === name ? decodeURIComponent(parts[1]) : acc; 32 }, ''); 33 } 34 35 function visitorId() { 36 var id = getCookie(COOKIE); 37 if (!id) { 38 id = 'v_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10); 39 setCookie(COOKIE, id, 730); 40 } 41 return id; 42 } 43 44 // Identity token: from the URL param on click-through, else the sticky cookie. 45 function contactId() { 46 var v = new URLSearchParams(window.location.search).get(ID_PARAM); 47 if (v) { 48 v = v.slice(0, 191); 49 setCookie(CID_COOKIE, v, 730); 50 return v; 51 } 52 return getCookie(CID_COOKIE); 53 } 54 55 function timezone() { 56 try { 57 var tz = Intl.DateTimeFormat().resolvedOptions().timeZone || ''; 58 return tz === 'Asia/Calcutta' ? 'Asia/Kolkata' : tz; 59 } catch (e) { 60 return ''; 61 } 62 } 63 64 function detectSource() { 65 var q = new URLSearchParams(window.location.search); 66 var ref = document.referrer || ''; 67 var host = ''; 68 try { host = ref ? new URL(ref).hostname : ''; } catch (e) {} 69 70 if (q.get('utm_source')) { 71 return { source: q.get('utm_source'), medium: q.get('utm_medium') || 'campaign', campaign: q.get('utm_campaign') || '', term: q.get('utm_term') || q.get('keyword') || '' }; 72 } 73 var ai = /chatgpt\.com|openai\.com|perplexity\.ai|gemini\.google|bard\.google|copilot\.microsoft|claude\.ai/i; 74 if (ai.test(host)) { return { source: host, medium: 'ai_chat', campaign: '', term: '' }; } 75 if (/google\./i.test(host)) { return { source: 'google', medium: 'organic', campaign: '', term: q.get('q') || q.get('keyword') || '' }; } 76 var social = /facebook|instagram|linkedin|t\.co|twitter|x\.com|youtube|reddit|tiktok/i; 77 if (social.test(host)) { return { source: host, medium: 'social', campaign: '', term: '' }; } 78 if (host && host !== window.location.hostname) { return { source: host, medium: 'referral', campaign: '', term: '' }; } 79 return { source: 'direct', medium: 'none', campaign: '', term: '' }; 80 } 81 82 function firstTouch() { 83 var saved = getCookie(SRC_COOKIE); 84 if (saved) { try { return JSON.parse(saved); } catch (e) {} } 85 var src = detectSource(); 86 setCookie(SRC_COOKIE, JSON.stringify(src), 730); 87 return src; 88 } 89 90 function measurementId() { 91 if (CFG.ga4_id) return CFG.ga4_id; 92 var m = document.cookie.match(/(?:^|;\s*)_ga_([A-Za-z0-9]+)=/); 93 return m ? ('G-' + m[1].toUpperCase()) : ''; 94 } 95 96 function gaClientId(cb) { 97 var mid = measurementId(); 98 if (typeof window.gtag === 'function' && mid) { 99 try { window.gtag('get', mid, 'client_id', function (cid) { cb(cid || ''); }); return; } catch (e) {} 100 } 101 var ga = getCookie('_ga'); 102 cb(ga ? ga.split('.').slice(-2).join('.') : ''); 103 } 104 105 function post(path, payload) { 106 return fetch(CFG.rest + path, { 107 method: 'POST', 108 headers: { 'Content-Type': 'application/json' }, 109 credentials: 'same-origin', 110 body: JSON.stringify(payload) 111 }); 112 } 113 114 function basePayload() { 115 var s = firstTouch(); 116 return { 117 visitor_id: visitorId(), 118 contact_id: contactId(), 119 path: window.location.pathname, 120 title: document.title, 121 referrer: document.referrer || '', 122 source: s.source, 123 medium: s.medium, 124 campaign: s.campaign, 125 term: s.term, 126 tz: timezone() 127 }; 128 } 129 130 function trackPageView() { 131 gaClientId(function (cid) { 132 var p = basePayload(); 133 p.event = 'page_view'; 134 p.ga_client_id = cid; 135 post('track', p); 136 if (typeof window.gtag === 'function') { 137 window.gtag('event', 'page_view', { wsn_visitor_id: p.visitor_id, campaign_source: p.source, campaign_medium: p.medium }); 138 } 139 }); 140 } 141 142 window.WSN = { 143 visitorId: visitorId, 144 contactId: contactId, 145 firstTouch: firstTouch, 146 timezone: timezone, 147 trackLead: function (lead) { 148 gaClientId(function (cid) { 149 var p = basePayload(); 150 var dp = CFG.demo_path || ''; 151 p.event = ( dp && window.location.pathname.indexOf(dp) !== -1 ) ? 'demo_lead' : 'form_lead'; 152 p.ga_client_id = cid; 153 p.email = lead.email; 154 p.name = lead.name || ''; 155 p.company = lead.company || ''; 156 post('lead', p); 157 if (typeof window.gtag === 'function') { 158 window.gtag('event', 'generate_lead', { wsn_visitor_id: p.visitor_id, campaign_source: p.source, campaign_medium: p.medium }); 159 } 160 }); 161 } 162 }; 163 164 function trackEvent(name) { 165 gaClientId(function (cid) { 166 var p = basePayload(); 167 p.event = name; 168 p.ga_client_id = cid; 169 post('track', p); 170 }); 171 } 172 if (window.WSN) { window.WSN.trackEvent = trackEvent; } 173 174 // Bricks form success -> Demo Form Submit (step 4), scoped to the demo path. 175 var WSN_DEMO_PATH = CFG.demo_path || ''; 176 if (WSN_DEMO_PATH) { 177 document.addEventListener('bricks/form/success', function () { 178 if (window.location.pathname.indexOf(WSN_DEMO_PATH) !== -1) { trackEvent('demo_lead'); } 179 }); 180 } 181 182 // Post-submit redirect flag: the demo form redirects to the thank-you page 183 // with ?wsn_demo=1, so only real submissions (not direct visits) record demo_lead. 184 if (new URLSearchParams(window.location.search).get('wsn_demo') === '1') { 185 trackEvent('demo_lead'); 186 } 187 188 // Fill any hidden field named visitor_id / wsn_visitor_id (e.g. on your 189 // Request-a-Demo form) with the anonymous ID, so it submits to your CRM/Zapier. 190 function fillVisitorFields() { 191 var vid = visitorId(); 192 var els = document.querySelectorAll('input[name="visitor_id"], input[name="wsn_visitor_id"]'); 193 for (var i = 0; i < els.length; i++) { els[i].value = vid; els[i].setAttribute('value', vid); } 194 } 195 if (document.readyState !== 'loading') { fillVisitorFields(); } 196 else { document.addEventListener('DOMContentLoaded', fillVisitorFields); } 197 setTimeout(fillVisitorFields, 1500); 198 document.addEventListener('bricks/form/submit', fillVisitorFields); 199 200 // --- Auto-capture form leads: read email on Bricks submit, send to /lead on success --- 201 function wsnIsEmail(v) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((v || '').trim()); } 202 function wsnLabelText(el) { 203 var t = ''; 204 if (el.id) { var l = document.querySelector('label[for="' + el.id + '"]'); if (l) { t = l.textContent; } } 205 if (!t && el.closest) { var p = el.closest('label'); if (p) { t = p.textContent; } } 206 if (!t) { t = el.getAttribute('placeholder') || el.name || ''; } 207 return (t || '').toLowerCase(); 208 } 209 function wsnFieldByHint(form, hints) {
210 var els = form.querySelectorAll('input[type="text"], input:not([type])'); 211 for (var i = 0; i < els.length; i++) { 212 var t = wsnLabelText(els[i]); 213 for (var h = 0; h < hints.length; h++) { if (t.indexOf(hints[h]) !== -1) { return (els[i].value || '').trim(); } } 214 } 215 return ''; 216 } 217 function wsnFindEmail(form) { 218 var e = form.querySelector('input[type="email"]'); 219 if (e && wsnIsEmail(e.value)) { return e.value.trim(); } 220 var all = form.querySelectorAll('input, textarea'); 221 for (var i = 0; i < all.length; i++) { 222 var lab = wsnLabelText(all[i]); 223 if (wsnIsEmail(all[i].value) && lab.indexOf('email') !== -1 && lab.indexOf('emailto') === -1) { return all[i].value.trim(); } 224 } 225 for (var j = 0; j < all.length; j++) { if (wsnIsEmail(all[j].value)) { return all[j].value.trim(); } } 226 return ''; 227 } 228 var wsnPendingLead = null; 229 function wsnCaptureFrom(form) { 230 if (!form || !form.querySelectorAll) { return; } 231 var email = wsnFindEmail(form); 232 if (!wsnIsEmail(email)) { return; } 233 wsnPendingLead = { 234 email: email, 235 name: (wsnFieldByHint(form, ['first name', 'first']) + ' ' + wsnFieldByHint(form, ['last name', 'last', 'surname'])).trim(), 236 company: wsnFieldByHint(form, ['company']) 237 }; 238 } 239 document.addEventListener('submit', function (e) { fillVisitorFields(); wsnCaptureFrom(e.target); }, true); 240 document.addEventListener('bricks/form/submit', function () { 241 if (wsnPendingLead) { return; } 242 var forms = document.querySelectorAll('form.brxe-form, form'); 243 for (var i = 0; i < forms.length; i++) { wsnCaptureFrom(forms[i]); if (wsnPendingLead) { break; } } 244 }); 245 document.addEventListener('bricks/form/success', function () { 246 if (wsnPendingLead && window.WSN && typeof window.WSN.trackLead === 'function') { 247 window.WSN.trackLead(wsnPendingLead); 248 wsnPendingLead = null; 249 } 250 }); 251 252 if (document.readyState !== 'loading') { 253 trackPageView(); 254 } else { 255 document.addEventListener('DOMContentLoaded', trackPageView); 256 } 257})();
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.