PageSourceSearch

https://www.demandcurve.com/dc-analytics.js

js demandcurve.com collected 2026-10-02 03:39:00 UTC 12,406 bytes, 226 lines download raw bytes

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.