1/* DC analytics bootstrap: iubenda consent -> Segment analytics.js -> events. 2 Loaded on every page by Layout AFTER the iubenda config/scripts. 3 4 Contract (matches the historical taxonomy - names are LOCKED): 5 - page views: analytics.page() + "<Page> View" named events (map below) 6 - "Link Click" via ONE delegated listener (event bubbling) 7 - conversions: forms call window.DCA.payload() to get {utm, consent, 8 event_id, fbc, fbp} for the serverless POST, and DCA.track('Subscribe - FE'| 9 'Agency Apply - FE', {event_id}) so Meta can dedupe browser vs server 10 (old-site pattern: FE + server events merged in Meta via shared event_id). 11 12 Consent: NOTHING loads until iubenda grants consent (per-purpose 4/5 = 13 measurement/marketing). No consent -> no Segment, no pixels, DCA.consent()=false. 14 UTM: first-touch stored ONCE (dc_utm_first), last-touch refreshed each 15 landing (dc_utm_last) - the initial/last model the old backend kept. */ 16(function () { 17 'use strict'; 18 19 /* ---------- write key by host (prod key only on demandcurve.com) ---------- */ 20 var HOST = location.hostname; 21 var IS_PROD = /(^|\.)demandcurve\.com$/.test(HOST); 22 var WRITE_KEY = IS_PROD ? window.DC_SEGMENT_KEY_PROD : (window.DC_SEGMENT_KEY_DEV || window.DC_SEGMENT_KEY_PROD); 23 24 /* ---------- UTM capture: initial (set once) + last (each landing) ---------- */ 25 function readUtm() { 26 var q = new URLSearchParams(location.search), u = {}, any = false; 27 ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'].forEach(function (k) { 28 var v = q.get(k); if (v) { u[k] = v; any = true; } 29 }); 30 if (!any && document.referrer) { 31 try { 32 var rh = new URL(document.referrer).hostname; 33 if (rh && rh !== HOST) { u.utm_source = rh; u.utm_medium = 'referral'; any = true; } 34 } catch (e) {} 35 } 36 return any ? u : null; 37 } 38 function store(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) {} } 39 function load(k) { try { return JSON.parse(localStorage.getItem(k) || 'null'); } catch (e) { return null; } } 40 var landed = readUtm(); 41 if (landed) { if (!load('dc_utm_first')) store('dc_utm_first', landed); store('dc_utm_last', landed); } 42 43 function utmPayload() { 44 var first = load('dc_utm_first') || {}, last = load('dc_utm_last') || {}, out = {}; 45 Object.keys(last).forEach(function (k) { out[k] = last[k]; }); 46 Object.keys(first).forEach(function (k) { out['first_' + k] = first[k]; }); 47 return out; 48 } 49 50 /* ---------- helpers exposed to forms ---------- */ 51 function cookie(name) { 52 var m = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)')); return m ? decodeURIComponent(m[1]) : ''; 53 } 54 var consented = false; 55 /* Segment's anonymousId = the browser identity every client-side event carries 56 (Mixpanel maps it to $device_id). Forwarding it with a form POST is what lets 57 a server-side conversion stitch to that person's browsing history - without it 58 the server event arrives with only userId(email) and Mixpanel has no way to 59 link it to the session. Verified 2026-08-27: 119 of 131 `Agency Apply` rows had 60 ZERO pageviews on the profile for exactly this reason. */ 61 function anonId() { 62 try { 63 if (window.analytics && typeof window.analytics.user === 'function') { 64 var u = window.analytics.user(); 65 if (u && typeof u.anonymousId === 'function') return u.anonymousId() || ''; 66 } 67 } catch (e) {} 68 return ''; 69 } 70 window.DCA = { 71 consent: function () { return consented; }, 72 eventId: function () { 73 return 'dc-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10); 74 }, 75 payload: function () { 76 // analytics_consent (NOT `consent`) - the lead form has its own consent 77 // checkbox field and the two must never collide in the POST body. 78 return { utm: utmPayload(), analytics_consent: consented, fbc: cookie('_fbc'), fbp: cookie('_fbp'), event_id: this.eventId(), page: location.pathname, anonymous_id: anonId() }; 79 }, 80 track: function (name, props) { 81 if (window.analytics && window.analytics.track) window.analytics.track(name, props || {}); 82 }, 83 identify: function (email, traits) { 84 if (window.analytics && window.analytics.identify) { 85 var t = utmPayload(); Object.keys(traits || {}
85).forEach(function (k) { t[k] = traits[k]; }); 86 window.analytics.identify(email, t); 87 } 88 }, 89 }; 90 91 /* ---------- named page-view events (historical "<Page> View" convention) ---------- */ 92 var PAGE_EVENTS = { 93 '/': 'Home View', 94 '/paid-marketing': 'Paid Media View', /* path renamed 2026-07-07; event name is the historical series - never rename */ '/ai-search': 'AI Search View', '/chatgpt-ads': 'ChatGPT Ads View', 95 '/story-systems': 'Story Systems View', '/ad-creative': 'Ad Creative View', '/landing-pages': 'Landing Pages View', 96 '/about': 'DC Model View', /* path renamed 2026-07-07; event name is the historical series - never rename */ '/services': 'Services View', 97 '/sponsorships': 'Sponsorships View', '/sponsor-apply': 'Sponsor Apply View', '/contact': 'Contact View', '/talent': 'Talent View', 98 }; 99 100 /* ---------- Frontier taxonomy (locked with Justin 2026-07-10) ---------- 101 Two events, properties carry the "which" (NOT per-type events): 102 - "Frontier View" app screens, prop view 103 - "Content View" reader pages, props format/slug/title(/edition) 104 format values mirror pieces.json exactly (deepdive|playbook|teardown| 105 newsletter). Legacy names (Blog View etc.) deliberately NOT continued. 106 member = has a Supabase session on this device (key survives until 107 sign-out/refresh-failure, so "member-ish" not "logged-in this second). */ 108 var FRONTIER_VIEWS = { 109 '/frontier': 'dashboard', '/frontier-library': 'library', '/newsletter': 'newsletters', /* path moved 2026-07-17; view value is the series - never rename */ 110 '/frontier-ads': 'ads', '/frontier-deals': 'deals', '/frontier-saved': 'saved', 111 }; 112 var CONTENT_DIRS = { blog: 'deepdive', playbooks: 'playbook', teardowns: 'teardown', newsletters: 'newsletter', growth: 'guide' }; 113 function isMember() { 114 try { 115 for (var i = 0; i < localStorage.length; i++) { 116 var k = localStorage.key(i); 117 if (/^sb-.+-auth-token$/.test(k) && localStorage.getItem(k)) return true; 118 } 119 } catch (e) {} 120 return false; 121 } 122 123 /* ---------- Segment loader (runs only after consent) ---------- */ 124 function loadSegment() { 125 if (!WRITE_KEY || window.analytics) return; 126 /* Segment analytics.js snippet (standard, abridged bootstrap) */ 127 var analytics = window.analytics = window.analytics || []; 128 analytics.invoked = true; 129 analytics.methods = ['trackSubmit','trackClick','trackLink','trackForm','pageview','identify','reset','group','track','ready','alias','debug','page','once','off','on','addSourceMiddleware','addIntegrationMiddleware','setAnonymousId','addDestinationMiddleware']; 130 analytics.factory = function (m) { return function () { var a = Array.prototype.slice.call(arguments); a.unshift(m); analytics.push(a); return analytics; }; }; 131 for (var i = 0; i < analytics.methods.length; i++) { var key = analytics.methods[i]; analytics[key] = analytics.factory(key); } 132 analytics.load = function (key, opts) { 133 var s = document.createElement('script'); s.type = 'text/javascript'; s.async = true; 134 s.src = 'https://cdn.segment.com/analytics.js/v1/' + key + '/analytics.min.js'; 135 var f = document.getElementsByTagName('script')[0]; f.parentNode.insertBefore(s, f); 136 analytics._loadOptions = opts; 137 }; 138 analytics.SNIPPET_VERSION = '4.15.3'; 139 analytics.load(WRITE_KEY); 140 141 /* page + named view */ 142 analytics.page(); 143 var path = location.pathname.replace(/\/$/, '') || '/'; 144 if (PAGE_EVENTS[path]) analytics.track(PAGE_EVENTS[path]); 145 146 /* Frontier app views + content readers */ 147 var m = isMember(); 148 if (FRONTIER_VIEWS[path]) analytics.track('Frontier View', { view: FRONTIER_VIEWS[path], member: m, path: path }); 149 var seg = path.split('/'); 150 if (seg.length === 3 && CONTENT_DIRS[seg[1]]) { 151 var cprops = { format: CONTENT_DIRS[seg[1]], slug: seg[2], title: document.title, path: path, member: m }; 152 if (cprops.format === 'newsletter') { var ed = seg[2].match(/(\d+)$/); if (ed) cprops.edition = +ed[1]; } 153 analytics.track('Content View', cprops); 154 } 155 156 /* delegated Link Click (one listener, event bubbling - old-site pattern) */ 157 document.addEventListener('click', function (e) { 158 var a = e.target && e.target.closest ? e.target.closest('a[href]') : null; 159 if (!a) return; 160 analytics.track('Link Click', { href: a.getAttribute('href'), text: (a.textContent || '').trim().slice(0, 80), page: path }); 161 }, { capture: true, passive: true }); 162 163 /* ---------- contact-CTA clicks: `Agency Form Click` ---------- 164 WHY: the delegated Link Click above fires, then the browser navigates and 165 cancels Segment's in-flight XHR, so CTA clicks are lost. Measured 166 2026-08-27 over Jul 14 - Aug 26: of 11,028 `Link Click` events only 43 167 carried the `page` property (i.e. originated in THIS file) - the other 168 ~11k are gp.demandcurve.com and the legacy tracker. Page-load events 169 survive because the page stays put; click-then-navigate events do not.
170 That is why the service pages looked untracked even though their CTAs work 171 (17% of /contact arrivals come straight from a service page). 172 173 FIX: for contact-bound links ONLY, hold navigation until the track call 174 acknowledges, capped at 300ms, then navigate regardless. Deliberately 175 narrow - intercepting every link would put a delay on the whole site. 176 Name reuses the historical `Agency Form Click` series from the old 177 Webflow site (died week 27 at cutover) so the series continues. */ 178 document.addEventListener('click', function (e) { 179 if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; 180 var a = e.target && e.target.closest ? e.target.closest('a[href]') : null; 181 if (!a || a.target === '_blank' || a.hasAttribute('download')) return; 182 var href = a.getAttribute('href') || ''; 183 if (!/^\/contact(?:[\/?#]|$)/.test(href)) return; 184 e.preventDefault(); 185 var went = false; 186 var go = function () { if (went) return; went = true; window.location.href = a.href; }; 187 setTimeout(go, 300); 188 try { 189 analytics.track('Agency Form Click', 190 { href: href, text: (a.textContent || '').trim().slice(0, 80), page: path }, go); 191 } catch (err) { go(); } 192 }, false); 193 } 194 195 /* ---------- consent gate (iubenda) ---------- 196 Three ways consent becomes true, all funneled through grant(): 197 1. onConsentGiven - EU visitor clicks Accept 198 2. onConsentRead - returning visitor with stored consent 199 3. isConsentGiven() polled after init - NON-GDPR visitors (US opt-out 200 regime): iubenda considers consent given by default but fires NO 201 callback (verified live 2026-07-05: banner suppressed, isConsentGiven() 202 === true, neither callback fires -> without this check the entire US 203 majority of traffic would silently go untracked). 204 If iubenda never loads or never says yes: stay dark (fail CLOSED). */ 205 function grant() { if (!consented) { consented = true; loadSegment(); } } 206 function checkApi() { 207 try { 208 if (window._iub && window._iub.cs && window._iub.cs.api && 209 window._iub.cs.api.isConsentGiven && window._iub.cs.api.isConsentGiven()) grant(); 210 } catch (e) {} 211 } 212 window._iub = window._iub || []; 213 var cs = (window._iub.csConfiguration = window._iub.csConfiguration || {}); 214 var prev = cs.callback || {}; 215 cs.callback = Object.assign({}, prev, { 216 onConsentGiven: function () { if (prev.onConsentGiven) prev.onConsentGiven(); grant(); }, 217 onConsentRead: function () { if (prev.onConsentRead) prev.onConsentRead(); grant(); }, 218 onReady: function () { if (prev.onReady) prev.onReady.apply(null, arguments); checkApi(); }, 219 }); 220 /* bounded fallback poll (API-shape / timing insurance): every 500ms for 12s */ 221 var tries = 0; 222 var poll = setInterval(function () { 223 if (consented || ++tries > 24) { clearInterval(poll); return; } 224 checkApi(); 225 }, 500); 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.