Files
warmbly/tracking/static/tracking.js
T

128 lines
4.7 KiB
JavaScript

/* Warmbly website tracking. Records page views for the workspace whose site
* key the script tag carries, sends nothing before consent when the
* workspace requires it, and honours Global Privacy Control / Do Not Track.
* API: warmbly('consent','granted'|'denied'), warmbly('page'), warmbly('reset'). */
(function () {
if (window.__wbly) return;
window.__wbly = 1;
var w = window, d = document, n = navigator;
var sc = d.currentScript;
if (!sc) {
var all = d.getElementsByTagName('script');
for (var i = all.length - 1; i >= 0; i--) {
if (all[i].getAttribute('data-site')) { sc = all[i]; break; }
}
}
if (!sc) return;
var key = sc.getAttribute('data-site');
if (!key) return;
var mode = sc.getAttribute('data-consent') === 'implicit' ? 'implicit' : 'explicit';
var base = sc.getAttribute('data-endpoint') || String(sc.src).replace(/\/tracking\.js.*$/, '');
var endpoint = base + '/p';
var VID = 'wbly_vid', SID = 'wbly_sid', SAT = 'wbly_sat', CON = 'wbly_consent', TOK = 'wbly_t';
function rnd() {
var a = new Uint8Array(16);
if (w.crypto && w.crypto.getRandomValues) { w.crypto.getRandomValues(a); }
else { for (var i = 0; i < 16; i++) a[i] = (Math.random() * 256) | 0; }
var s = '';
for (var j = 0; j < 16; j++) s += ('0' + a[j].toString(16)).slice(-2);
return s;
}
function get(k) { try { return w.localStorage.getItem(k); } catch (e) { return null; } }
function set(k, v) { try { w.localStorage.setItem(k, v); } catch (e) {} }
function del(k) { try { w.localStorage.removeItem(k); } catch (e) {} }
function sget(k) { try { return w.sessionStorage.getItem(k); } catch (e) { return null; } }
function sset(k, v) { try { w.sessionStorage.setItem(k, v); } catch (e) {} }
function sdel(k) { try { w.sessionStorage.removeItem(k); } catch (e) {} }
function optedOut() {
return n.globalPrivacyControl === true || n.doNotTrack === '1' || w.doNotTrack === '1';
}
function consent() {
if (optedOut()) return null;
var c = get(CON);
if (c === 'granted') return 'granted';
if (c === 'denied') return null;
return mode === 'implicit' ? 'implicit' : null;
}
/* The click redirect appends the identification ticket; take it off the
* address bar right away so it is never bookmarked or shared. */
var token = '';
try {
var u = new URL(w.location.href);
var t = u.searchParams.get(TOK);
if (t) {
token = t;
u.searchParams.delete(TOK);
w.history.replaceState(w.history.state, '', u.toString());
}
} catch (e) {}
var landing = false, lastUrl = '', lastAt = 0;
function session() {
var now = Date.now();
var sid = sget(SID), at = +sget(SAT) || 0;
if (!sid || now - at > 30 * 60 * 1000) { sid = rnd(); landing = true; }
sset(SID, sid);
sset(SAT, String(now));
return sid;
}
function tz() {
try { return Intl.DateTimeFormat().resolvedOptions().timeZone || ''; } catch (e) { return ''; }
}
function send() {
var c = consent();
if (!c) return;
var url = w.location.href, now = Date.now();
if (url === lastUrl && now - lastAt < 1000) return;
lastUrl = url; lastAt = now;
var vid = get(VID);
if (!vid) { vid = rnd(); set(VID, vid); }
var sid = session();
var body = {
k: key, v: vid, s: sid, c: c, t: token, u: url,
ti: d.title || '', r: d.referrer || '', l: n.language || '', tz: tz(),
sw: (w.screen && w.screen.width) || 0, sh: (w.screen && w.screen.height) || 0, ld: landing
};
landing = false; token = '';
try {
/* text/plain keeps this a simple request: no preflight on every view. */
fetch(endpoint, {
method: 'POST', keepalive: true, credentials: 'omit',
headers: { 'Content-Type': 'text/plain' }, body: JSON.stringify(body)
}).then(function (r) { return r.status === 200 ? r.json() : null; })
.then(function (j) { if (j && j.vid) set(VID, j.vid); })
.catch(function () {});
} catch (e) {}
}
/* Single-page apps: a pushed history entry is a page view. */
var push = w.history.pushState;
if (push) {
w.history.pushState = function () {
push.apply(this, arguments);
setTimeout(send, 0);
};
}
w.addEventListener('popstate', function () { setTimeout(send, 0); });
var queued = (w.warmbly && w.warmbly.q) || [];
function api(cmd, arg) {
if (cmd === 'consent') {
if (arg === 'granted') { set(CON, 'granted'); send(); }
else if (arg === 'denied') { set(CON, 'denied'); del(VID); sdel(SID); sdel(SAT); }
} else if (cmd === 'reset') {
del(VID); sdel(SID); sdel(SAT);
} else if (cmd === 'page') {
send();
}
}
w.warmbly = function () { api.apply(null, arguments); };
w.warmbly.q = [];
for (var q = 0; q < queued.length; q++) api.apply(null, queued[q]);
send();
})();