1/* CalExitNow.com â analytics + funnel instrumentation (privacy-light, Umami). 2 * 3 * Self-hosted Umami, no cookies, no consent banner required. 4 * Set UMAMI_SRC + UMAMI_WEBSITE_ID below once the Umami instance is live and a 5 * "website" has been created in its dashboard. Until then this file runs 6 * harmlessly: UTM/attribution capture + funnel event wiring stay active and 7 * caTrack() is a safe no-op, so nothing breaks and no data leaks. 8 */ 9(function () { 10 'use strict'; 11 12 /* ===== CONFIG â fill these two when Umami is deployed ===== */ 13 var UMAMI_SRC = ''; // e.g. 'https://analytics.calexitnow.com/script.js' 14 var UMAMI_WEBSITE_ID = ''; // e.g. '00000000-0000-0000-0000-000000000000' 15 /* ========================================================== */ 16 17 // Load the Umami tracker only when configured. 18 if (UMAMI_SRC && UMAMI_WEBSITE_ID) { 19 var s = document.createElement('script'); 20 s.defer = true; 21 s.src = UMAMI_SRC; 22 s.setAttribute('data-website-id', UMAMI_WEBSITE_ID); 23 document.head.appendChild(s); 24 } 25 26 // Event helper â no-op until Umami's global is present. 27 window.caTrack = function (name, props) { 28 try { 29 if (window.umami && typeof window.umami.track === 'function') { 30 window.umami.track(name, props || {}); 31 } 32 } catch (e) { /* never break the page for analytics */ } 33 }; 34 35 // ----- First-touch attribution (UTMs + referrer), persisted for the session ----- 36 // 'ref' is the referral code from a supporter's share link. Captured 37 // first-touch alongside the UTMs so an invite still gets credit if the 38 // visitor wanders the site before signing up. 39 var UTM_KEYS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'ref']; 40 function captureAttribution() { 41 try { 42 var sp = new URLSearchParams(location.search), got = {}; 43 UTM_KEYS.forEach(function (k) { var v = sp.get(k); if (v) got[k] = v.slice(0, 120); }); 44 if (Object.keys(got).length && !sessionStorage.getItem('ca_attr')) { 45 got.landing_path = location.pathname; 46 got.referrer = (document.referrer || '').slice(0, 200); 47 sessionStorage.setItem('ca_attr', JSON.stringify(got)); 48 } 49 } catch (e) {} 50 } 51 window.caAttribution = function () { 52 try { return JSON.parse(sessionStorage.getItem('ca_attr') || '{}'); } catch (e) { return {}; } 53 }; 54 captureAttribution(); 55 56 document.addEventListener('DOMContentLoaded', function () { 57 var attr = window.caAttribution(); 58 59 // Persist attribution into the signup form so it lands in the DB (raw JSON). 60 var form = document.querySelector('form[name="signup2"]'); 61 if (form) { 62 Object.keys(attr).forEach(function (k) { 63 var existing = form.querySelector('[name="' + k + '"]'); 64 if (existing) { existing.value = attr[k]; return; } 65 var i = document.createElement('input'); 66 i.type = 'hidden'; i.name = k; i.value = attr[k]; 67 form.appendChild(i); 68 }); 69 // form_view: signup card scrolled into view (fires once). 70 var card = form.closest('.signup-card') || form; 71 if ('IntersectionObserver' in window) { 72 var io = new IntersectionObserver(function (ents) { 73 ents.forEach(function (en) { if (en.isIntersecting) { window.caTrack('form_view'); io.disconnect(); } }); 74 }, { threshold: 0.5 }); 75 io.observe(card); 76 } 77 } 78 79 // Delegated click tracking for the key funnel actions. 80 document.addEventListener('click', function (e) { 81 var el = e.target.closest('a, button'); 82 if (!el) return; 83 var href = (el.getAttribute('href') || '').toLowerCase(); 84 var label = (el.textContent || '').trim().slice(0, 60); 85 if (href.indexOf('/donate') > -1 || href.indexOf('donate@') > -1) { 86 window.caTrack('donate_click', { label: label }); 87 } else if (href.indexOf('#signup') > -1 || /join the movement|count me in|get involved/i.test(label)) { 88 window.caTrack('cta_join_click', { label: label }); 89 } else if (/tiktok|instagram|twitch|bsky|bluesky|youtube/.test(href)) { 90 window.caTrack('channel_click', { href: href }); 91 } else if (el.classList && el.classList.contains('release-card')) { 92 window.caTrack('press_click', { href: el.getAttribute('href') || '' }); 93 } 94 }, true); 95 96 // video_play: lite-youtube upgrades the facade on first activation. 97 document.querySelectorAll('lite-youtube').forEach(function (ly) { 98 ly.addEventListener('click', function () { 99 window.caTrack('video_play', { id: ly.getAttribute('videoid') || '' }); 100 }, { once: true }); 101 }); 102 }); 103})();
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.