PageSourceSearch

https://riddimbank.com/js/attribution.js

js riddimbank.com collected 2026-10-07 22:24:18 UTC 7,441 bytes, 175 lines download raw bytes

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.