PageSourceSearch

https://exiap.ca/js/attribution.js

js exiap.ca collected 2026-10-08 01:44:42 UTC 7,358 bytes, 209 lines download raw bytes

1/**
2 * Attribution Script
3 *
4 * Captures paid campaign parameters (UTM, gclid, fbclid, etc.) from landing URLs,
5 * stores them in localStorage, and decorates outbound links to configured target
6 * domains with a base64-encoded query parameter.
7 *
8 * No internal consent logic — consent gating is handled externally via Cookiebot
9 * (type="text/plain" data-cookieconsent="marketing") or omitted for non-GDPR regions.
10 *
11 * Configuration via data attributes on the script tag:
12 *   data-target-domains   — comma-separated list of domains to decorate (required)
13 *   data-storage-key      — localStorage key name (default: "orgsource")
14 *   data-param-name       — query parameter name appended to links (default: "orgsource")
15 *   data-tracked-params   — comma-separated override of tracked params allowlist
16 *   data-ttl-days         — days before stored data expires (default: 90)
17 *   data-allowed-domains  — comma-separated list of domains where the script should run;
18 *                           if omitted, the script runs on all domains
19 *
20 * For GTM deployment (where document.currentScript is unavailable), use attribution-gtm.js
21 * which reads config from window.__attribution instead.
22 */
23(function () {
24  'use strict';
25
26  // --- Configuration -----------------------------------------------------------
27
28  var scriptEl = document.currentScript;
29
30  var DEFAULT_TRACKED_PARAMS = [
31    'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
32    'gclid', 'fbclid'
33  ];
34
35  function attr(name) {
36    return scriptEl && scriptEl.getAttribute(name);
37  }
38
39  // Domain allowlist — if set, only run on matching domains
40  var allowedDomainsAttr = attr('data-allowed-domains');
41  if (allowedDomainsAttr) {
42    var allowedDomains = allowedDomainsAttr.split(',').map(function (d) { return d.trim().toLowerCase(); }).filter(Boolean);
43    var currentHost = window.location.hostname.toLowerCase();
44    var allowed = false;
45    for (var a = 0; a < allowedDomains.length; a++) {
46      if (currentHost === allowedDomains[a] || currentHost.endsWith('.' + allowedDomains[a])) {
47        allowed = true;
48        break;
49      }
50    }
51    if (!allowed) return;
52  }
53
54  var targetDomains = (attr('data-target-domains') || '')
55    .split(',')
56    .map(function (d) { return d.trim().toLowerCase(); })
57    .filter(Boolean);
58
59  var storageKey = attr('data-storage-key') || 'orgsource';
60  var paramName = attr('data-param-name') || 'orgsource';
61
62  var trackedParams = attr('data-tracked-params')
63    ? attr('data-tracked-params').split(',').map(function (p) { return p.trim(); }).filter(Boolean)
64    : DEFAULT_TRACKED_PARAMS;
65
66  var ttlDays = parseInt(attr('data-ttl-days'), 10) || 90;
67  var ttlMs = ttlDays * 24 * 60 * 60 * 1000;
68
69  // --- Helpers -----------------------------------------------------------------
70
71  /**
72   * Encode a string to standard base64.
73   * Uses TextEncoder for UTF-8 safety, falls back to unescape/encodeURIComponent.
74   */
75  function toUrlSafeBase64(str) {
76    var bytes;
77    if (typeof TextEncoder !== 'undefined') {
78      bytes = new TextEncoder().encode(str);
79    } else {
80      bytes = unescape(encodeURIComponent(str));
81    }
82
83    var binary = '';
84    for (var i = 0; i < bytes.length; i++) {
85      binary += String.fromCharCode(typeof bytes === 'string' ? bytes.charCodeAt(i) : bytes[i]);
86    }
87
88    return btoa(binary);
89  }
90
91  /**
92   * Check whether a hostname matches one of the configured target domains.
93   * Matches exactly or as a subdomain (e.g. "pay.wise.com" matches "wise.com").
94   */
95  function isTargetDomain(hostname) {
96    hostname = hostname.toLowerCase();
97    for (var i = 0; i < targetDomains.length; i++) {
98      var target = targetDomains[i];
99      if (hostname === target || hostname.endsWith('.' + target)) {
100        return true;
101      }
102    }
103    return false;
104  }
105
106  /**
107   * Append the orgsource parameter to a URL string. Returns the decorated URL,
108   * or the original URL if it already contains the parameter or has no hostname match.
109   */
110  function decorateUrl(url) {
111    try {
112      var parsed = new URL(url, window.location.origin);
113      if (!isTargetDomain(parsed.hostname)) return url;
114      if (parsed.searchParams.has(paramName)) return url;
115      parsed.searchParams.set(paramName, encodedValue);
116      return parsed.toString();
117    } catch (_e) {
118      return url;
119    }
120  }
121
122  /**
123   * Decorate a single anchor element if it points to a target domain.
124   */
125  function decorateAnchor(anchor) {
126    var href = anchor.getAttribute('href');
127    if (!href) return;
128    var decorated = decorateUrl(href);
129    if (decorated !== href) {
130      anchor.setAttribute('href', decorated);
131    }
132  }
133
134  // --- Main logic --------------------------------------------------------------
135
136  // 1. Capture tracked params from the current URL
137  var params = {};
138  var searchParams = new URLSearchParams(window.location.search);
139  for (var i = 0; i < trackedParams.length; i++) {
140    var key = trackedParams[i];
141    var val = searchParams.get(key);
142    if (val) {
143      params[key] = val;
144    }
145  }
146
147  // 2. Persist to localStorage
148  var stored = {};
149  try {
150    var raw = localStorage.getItem(storageKey);
151    if (raw) {
152      var parsed2 = JSON.parse(raw);
153      var storedTs = parsed2._ts || 0;
154      // Expire if older than TTL
155      if (storedTs && (Date.now() - storedTs > ttlMs)) {
156        localStorage.removeItem(storageKey);
157      } else {
158        stored = parsed2;
159      }
160    }
161  } catch (_e) { /* ignore parse errors */ }
162
163  // Only store params if we captured new ones AND no valid first-touch data exists 
163yet.
164  // This preserves first-touch attribution — subsequent visits with different UTM params
165  // (e.g. returning via social) will not overwrite the original source.
166  var hasExistingData = Object.keys(stored).some(function (k) { return k !== '_ts'; });
167  if (Object.keys(params).length > 0 && !hasExistingData) {
168    var merged = {};
169    for (var k2 in params) {
170      if (params.hasOwnProperty(k2)) {
171        merged[k2] = params[k2];
172      }
173    }
174    merged._ts = Date.now();
175    stored = merged;
176    try {
177      localStorage.setItem(storageKey, JSON.stringify(stored));
178    } catch (_e) { /* storage full or disabled */ }
179  }
180
181  // 3. If there's nothing stored (besides _ts), nothing to decorate — exit early
182  var payloadKeys = Object.keys(stored).filter(function (k) { return k !== '_ts'; });
183  if (!payloadKeys.length || !targetDomains.length) return;
184
185  // 4. Encode the stored data once (exclude internal _ts from payload)
186  var payload = {};
187  for (var p = 0; p < payloadKeys.length; p++) {
188    payload[payloadKeys[p]] = stored[payloadKeys[p]];
189  }
190  var encodedValue = toUrlSafeBase64(JSON.stringify(payload));
191
192  // 5. Initial scan — decorate all existing <a> elements
193  var anchors = document.querySelectorAll('a[href]');
194  for (var j = 0; j < anchors.length; j++) {
195    decorateAnchor(anchors[j]);
196  }
197
198  // 6. Click-time delegation — catch dynamically rendered links (SPA navigation)
199  document.addEventListener('click', function (e) {
200    var target = e.target;
201    // Walk up the DOM to find the closest <a> element
202    while (target && target.tagName !== 'A') {
203      target = target.parentElement;
204    }
205    if (target && target.tagName === 'A') {
206      decorateAnchor(target);
207    }
208  }, true); // capture phase — runs before navigation
209})();

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.