1/* --------------------------------------------------------------------------- 2 attribution.js â where did this subscriber come from? 19 September 2026 3 4 Loaded on every page from _head.njk, deferred, AFTER consent.js. 5 6 WHY: a subscriber's utm_*, gclid, fbclid, landing page and referrer exist 7 only in their browser at the moment they arrive. Nothing can recover them 8 later, so every signup before this shipped is permanently unattributed. 9 10 TWO PARTS, and the split is the consent rule: 11 12 1. REMEMBERING ACROSS PAGES â only with a "Yes please" on the banner. 13 Someone who lands on /quiz-20-questions from an ad and reaches /pricing 14 three pages later can only be attributed if the landing visit was stored 15 on their device. That is storage for marketing, so it rides the same yes 16 as Google Analytics and OptiMonk. A "No thanks" (now or later) deletes 17 anything stored. 18 19 2. THE PAGE THEY ARE ON â always. On /pricing, whatever is in the address 20 bar right now (utm tags, gclid, the site that sent them) goes with the 21 order. Nothing is stored on the device for that; it is read from the 22 link they clicked and sent to our own server. A decliner who lands 23 straight on a tagged /pricing link is still attributed; one who landed 24 elsewhere first is not. That is the honest cost of a real "no". 25 26 ON /pricing ONLY: mint an id, POST the touch to recordAttribution, and put 27 the id on the six Payment Links as client_reference_id. Stripe carries it 28 to the Checkout Session; handleNewSubscription reads it back. 29 30 FAILS OPEN. No URLSearchParams, no storage, the POST fails, anything throws: 31 the plan buttons keep working exactly as before. Losing one attribution is 32 a shame; breaking checkout is a disaster. 33 34 NOT STORED ANYWHERE: the full referrer URL (origin + path only â a query 35 string can hold a search term or an email address), the landing page's 36 query string, or any IP address. 37 ------------------------------------------------------------------------- */ 38(function () { 39 'use strict'; 40 41 var STORE = 'rb_attr_v1'; 42 var CONSENT_KEY = 'rb_consent_v1'; // written by consent.js 43 var CONSENT_AGE = 365 * 24 * 60 * 60 * 1000; // same expiry consent.js uses 44 var KEEP_FOR = 90 * 24 * 60 * 60 * 1000; // a remembered first visit lasts 90 days 45 var ENDPOINT = 'https://us-central1-riddimbank-webauth.cloudfunctions.net/recordAttribution'; 46 var OWN_HOSTS = ['riddimbank.com', 'www.riddimbank.com', 47 'riddimbank-webauth.web.app', 'riddimbank-webauth.firebaseapp.com']; 48 var PARAMS = { utm_source: 200, utm_medium: 200, utm_campaign: 200, utm_term: 200, 49 utm_content: 200, gclid: 500, fbclid: 500, ref: 40 }; 50 51 function clip(v, n) { return String(v).trim().slice(0, n); } 52 53 function externalReferrer() { 54 try { 55 if (!document.referrer) return null; 56 var u = new URL(document.referrer); 57 if (u.protocol !== 'https:' && u.protocol !== 'http:') return null; 58 if (OWN_HOSTS.indexOf(u.hostname) !== -1) return null; // moving around our own site 59 return clip(u.origin + u.pathname, 300); 60 } catch (e) { return null; } 61 } 62 63 /* This page view as a touch. `signal` is true when it says anything about 64 where the visitor came from; a bare internal page view does not. */ 65 function thisVisit() { 66 var t = {}, signal = false; 67 try { 68 var q = new URLSearchParams(window.location.search); 69 for (var k in PARAMS) { 70 var v = q.get(k); 71 if (v && String(v).trim()) { t[k] = clip(v, PARAMS[k]); signal = true; } 72 } 73 } catch (e) { /* no URLSearchParams â treat as no parameters */ } 74 var r = externalReferrer(); 75 if (r) { t.referrer = r; signal = true; } 76 t.landingPage = clip(window.location.pathname || '/', 200); 77 t.at = Date.now(); 78 return { touch: t, signal: signal }; 79 } 80 81 function consent() { 82 try { 83 var v = JSON.parse(window.localStorage.getItem(CONSENT_KEY) || 'null'); 84 if (!v || typeof v.granted !== 'boolean') return null;
85 if (Date.now() - (v.at || 0) > CONSENT_AGE) return null; 86 return v.granted; 87 } catch (e) { return null; } 88 } 89 90 function load() { 91 try { 92 var s = JSON.parse(window.localStorage.getItem(STORE) || 'null'); 93 if (!s || !s.first || !s.first.at) return null; 94 if (Date.now() - s.first.at > KEEP_FOR || s.first.at > Date.now() + 86400000) { 95 forget(); return null; 96 } 97 return s; 98 } catch (e) { return null; } 99 } 100 function save(s) { try { window.localStorage.setItem(STORE, JSON.stringify(s)); } catch (e) {} } 101 function forget() { try { window.localStorage.removeItem(STORE); } catch (e) {} } 102 103 var here = thisVisit(); 104 105 /* Store this visit â only ever called with consent. The first visit we are 106 allowed to keep is `first`, even a direct one ("came straight to us" is an 107 answer too). `last` is the most recent visit that carried a source. */ 108 function remember() { 109 var s = load() || {}; 110 if (!s.first) s.first = here.touch; 111 if (here.signal) s.last = here.touch; 112 save(s); 113 } 114 115 function onConsent(granted) { 116 if (granted === true) remember(); 117 else if (granted === false) forget(); 118 } 119 120 /* consent.js announces a fresh answer with this event, so a visitor who 121 clicks "Yes please" on the page they landed on has THAT page remembered. */ 122 document.addEventListener('rb:consent', function (e) { 123 onConsent(e && e.detail ? e.detail.granted : null); 124 }); 125 onConsent(consent()); 126 127 /* ----------------------------- /pricing only ----------------------------- */ 128 var links = document.querySelectorAll('a[data-tier]'); // the six plan buttons, as pricing.js finds them 129 if (!links.length) return; 130 131 function newId() { 132 try { if (window.crypto && crypto.randomUUID) return crypto.randomUUID(); } catch (e) {} 133 var s = ''; 134 for (var i = 0; i < 3; i++) s += Math.random().toString(36).slice(2, 12); 135 return 'r' + Date.now().toString(36) + s; 136 } 137 138 var stored = consent() === true ? load() : null; 139 var payload = { 140 id: newId(), 141 consent: consent(), 142 page: window.location.pathname, 143 first: (stored && stored.first) || here.touch, 144 last: here.signal ? here.touch : ((stored && stored.last) || null) 145 }; 146 147 var sent = false; 148 function send(keepalive) { 149 if (sent) return; 150 try { 151 fetch(ENDPOINT, { 152 method: 'POST', 153 headers: { 'Content-Type': 'text/plain' }, // simple request: no preflight, keepalive-safe 154 body: JSON.stringify(payload), 155 keepalive: !!keepalive, 156 credentials: 'omit' 157 }).then(function (r) { if (r.ok) sent = true; }).catch(function () {}); 158 } catch (e) { /* fetch missing or refused â the links still work */ } 159 } 160 161 for (var i = 0; i < links.length; i++) { 162 try { 163 var href = links[i].getAttribute('href'); 164 if (!href) continue; 165 var u = new URL(href, window.location.href); 166 u.searchParams.set('client_reference_id', payload.id); 167 links[i].setAttribute('href', u.toString()); 168 /* If the first POST has not landed by the time they click, try once 169 more with keepalive so it survives the page leaving for Stripe. */ 170 links[i].addEventListener('click', function () { send(true); }); 171 } catch (e) { /* leave this link exactly as it was */ } 172 } 173 174 send(false); 175})();
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.