1/* Lead attribution (2026-08-18) - runs on EVERY page, including the 2 * full-page /account/* forms, which is why it lives outside 3 * account-modal.js (that file returns early when the modal markup is 4 * absent, so the capture never ran on the account pages). 5 * 6 * Two fields, two questions: 7 * src/page = the ENTRY: the click that sent them toward Premium and the 8 * page it sat on (header button on the BTC forecast page, a 7d 9 * lock chip, the popup, or an external referrer) 10 * cta = which button on /premium/ finally closed it 11 * 12 * Clicks made ON /premium/ never overwrite the entry: every purchase 13 * happens there, so last-touch would report "/premium/" for everyone. 14 * 15 * Coverage rule: a signup with NO recorded click still records where the 16 * visitor was standing (src="direct"), because "unknown" taught us nothing 17 * - 22 of 26 signups came back blank on the first version. */ 18(function () { 19 'use strict'; 20 var KEY = 'wiLead'; 21 22 function read() { 23 try { return JSON.parse(sessionStorage.getItem(KEY) || 'null') || {}; } catch (e) { return {}; } 24 } 25 function save(lead) { 26 try { sessionStorage.setItem(KEY, JSON.stringify(lead)); } catch (e) {} 27 } 28 function onPremium() { return /^\/premium/.test(window.location.pathname); } 29 function here() { return window.location.pathname.slice(0, 200); } 30 31 function sourceOf(el) { 32 var explicit = el.getAttribute('data-src'); 33 if (explicit) return explicit; 34 if (el.closest('#wiPopup')) return 'popup'; 35 var chip = el.classList.contains('nt-lock') ? el : el.closest('.nt-lock'); 36 if (chip) { 37 var kind = chip.getAttribute('data-nt-kind') || 'lock'; 38 var sym = chip.getAttribute('data-nt') || ''; 39 return 'lock_' + kind + (sym ? '_' + sym : ''); 40 } 41 if (el.closest('header')) return 'header'; 42 if (el.closest('footer')) return 'footer'; 43 var acct = el.getAttribute('data-account'); 44 if (acct) return acct; // login / register / checkout trigger 45 return 'link'; 46 } 47 48 // arriving from outside counts as an entry, but only if nothing on-site 49 // has been clicked yet 50 (function () { 51 var lead = read(); 52 if (lead.src || !document.referrer) return; 53 try { 54 var u = new URL(document.referrer); 55 if (u.host === window.location.host) return; 56 lead.src = 'ext'; 57 lead.page = (u.host + u.pathname).slice(0, 200); 58 save(lead); 59 } catch (e) {} 60 })(); 61 62 // ?src= on the landing URL (popup hand-off, campaigns) sets the entry and 63 // credits the page the visitor came from 64 (function () { 65 var m = /[?&]src=([A-Za-z0-9_.-]{1,60})/.exec(window.location.search); 66 if (!m) return; 67 var ref = ''; 68 try { 69 var u = document.referrer ? new URL(document.referrer) : null; 70 if (u && u.host === window.location.host) ref = u.pathname; 71 } catch (e) {} 72 var lead = read(); 73 lead.src = m[1]; 74 lead.page = (ref || here()).slice(0, 200); 75 save(lead); 76 })(); 77 78 document.addEventListener('click', function (e) { 79 var t = e.target.closest('a[href^="/premium"], a[href^="/account"], [data-account], .nt-lock'); 80 if (!t) return; 81 var lead = read(); 82 var src = sourceOf(t); 83 if (onPremium()) { 84 lead.cta = src; 85 if (!lead.src) { lead.src = src; lead.page = here(); } 86 } else { 87 lead.src = src; 88 lead.page = here(); 89 lead.cta = ''; 90 } 91 save(lead); 92 }, true); 93 94 // email-flow attribution (2026-09-07): links in the MailerLite automations 95 // and the daily CTA carry ?ml=<flow>&e=<email#> or &v=<variant>&d=<day>. 96 // Kept 30 days in a plain cookie (the checkout stamps it onto the 97 // purchase) and counted server-side - pages are cached, the server never 98 // sees the landing itself. 99 (function () { 100 var q = window.location.search, m = /[?&]ml=(cart|ebook|daily|gov)\b/.exec(q); 101 if (!m) return; 102 var e = (/[?&]e=(\d{1,2})\b/.exec(q) || [])[1], v = (/[?&]v=([ab])\b/.exec(q) || [])[1], d = (/[?&]d=(\d{8})\b/.exec(q) || [])[1]; 103 var ref = e || (v && d ? v + '.' + d : (v || '')); // gov.capital's top bar: just the variant 104 if (!ref) return; 105 // &l=<link#>: the second (third...) link of the same email is its own 106 // ref ("3.2" = email 3, link 2) so each link converts on its own; the 107 // first link stays the bare ref 108 var l = (/[?&]l=(\d{1,2})\b/.exec(q) || [])[1]; 109 if (l && l !== '1') ref += '.' + l; 110 // &until=<unix>
110: when this person's offer closes (the MailerLite field 111 // our flow job set) - the offer page counts down to it 112 var u = (/[?&]until=(\d{10})\b/.exec(q) || [])[1]; 113 if (u) document.cookie = 'wi_offer_until=' + u + '; expires=' + exp + '; path=/; SameSite=Lax'; 114 var cid = (/(?:^|; )wi_cid=([A-Za-z0-9]{8,32})/.exec(document.cookie) || [])[1] || Math.random().toString(36).slice(2, 14); 115 var exp = new Date(Date.now() + 30 * 86400000).toUTCString(); 116 document.cookie = 'wi_cid=' + cid + '; expires=' + exp + '; path=/; SameSite=Lax'; 117 document.cookie = 'wi_flow=' + m[1] + '|' + ref + '|' + cid + '; expires=' + exp + '; path=/; SameSite=Lax'; 118 try { 119 fetch('/account/flow-hit', { method: 'POST', keepalive: true, headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ f: m[1], r: ref, cid: cid }) }).catch(function () {}); 120 } catch (err) {} 121 })(); 122 123 // campaign attribution (2026-09-15): any link carrying standard UTM 124 // parameters. The five fields ride in ONE cookie for 30 days, the newest 125 // landing overwriting the last (last touch, like the flows above), and 126 // the landing is counted server-side because the page itself is cached. 127 (function () { 128 var q = window.location.search; 129 if (!/[?&]utm_source=/.test(q)) return; 130 var fields = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_date'], utm = {}, any = false; 131 fields.forEach(function (f) { 132 var m = new RegExp('[?&]' + f + '=([^&#]{1,120})').exec(q); 133 var v = m ? decodeURIComponent(m[1].replace(/\+/g, ' ')).trim().slice(0, 80) : ''; 134 utm[f] = v; 135 if (v) any = true; 136 }); 137 if (!any || !utm.utm_source) return; 138 var exp = new Date(Date.now() + 30 * 86400000).toUTCString(); 139 var cid = (/(?:^|; )wi_cid=([A-Za-z0-9]{8,32})/.exec(document.cookie) || [])[1] || Math.random().toString(36).slice(2, 14); 140 document.cookie = 'wi_cid=' + cid + '; expires=' + exp + '; path=/; SameSite=Lax'; 141 document.cookie = 'wi_utm=' + fields.map(function (f) { return encodeURIComponent(utm[f]); }).join('|') + '; expires=' + exp + '; path=/; SameSite=Lax'; 142 utm.f = 'utm'; utm.cid = cid; 143 try { 144 fetch('/account/flow-hit', { method: 'POST', keepalive: true, headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(utm) }).catch(function () {}); 145 } catch (err) {} 146 })(); 147 148 /** What to send with a signup. Never empty: falls back to this page. */ 149 window.wiLead = function () { 150 var lead = read(); 151 if (!lead.src) { lead.src = 'direct'; lead.page = here(); } 152 153 return lead; 154 }; 155 156 // full-page /account/register and /account/checkout forms post normally, 157 // so the fields have to ride along as hidden inputs 158 document.addEventListener('submit', function (e) { 159 var form = e.target; 160 if (!form || form.tagName !== 'FORM') return; 161 var action = form.getAttribute('action') || window.location.pathname; 162 if (!/\/account\/(register|checkout)/.test(action)) return; 163 var lead = window.wiLead();
164 ['src', 'src_page', 'cta'].forEach(function (name) { 165 var value = name === 'src' ? lead.src : (name === 'src_page' ? lead.page : lead.cta); 166 if (!value || form.querySelector('input[name="' + name + '"]')) return; 167 var input = document.createElement('input'); 168 input.type = 'hidden'; 169 input.name = name; 170 input.value = value; 171 form.appendChild(input); 172 }); 173 }, true); 174})();
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.