Add website analytics
Release / release (push) Successful in 3m49s
CI / test (push) Successful in 4m45s

A Tianji-style Websites section: a small tracking script, a public collection endpoint, and a
page per website with visitors, page views, visits, bounce rate, a chart and top lists for pages,
referrers, browsers, systems, devices, countries, languages and events. No cookies and no stored
IP address or user agent. SANDBAR_TRUST_PROXY makes the client address come from the proxy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
VeillaxandClaude Sonnet 5.5 committed 2026-10-27 04:22:55 -06:00
1 parent 4ea4fe5289
commit b4fd31a182
13 files changed
+843 -22

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
/* Sandbar website analytics. Add to a page:
<script defer src="https://sandbar.example.com/script.js" data-website="WEBSITE_ID"></script>
It sets no cookies and stores nothing in the browser. It sends the page path, the referrer, the
language and the browser's own user agent, and it honors Do Not Track.
Options on the tag: data-hash (count #hash changes as pages), data-auto="false" (send nothing
until you call sandbar.view() yourself). Custom events: sandbar.track("signup", {plan: "pro"}). */
(function () {
var tag = document.currentScript;
if (!tag) return;
var site = tag.getAttribute('data-website');
if (!site) return;
var base = tag.src.replace(/\/script\.js(\?.*)?$/, '');
var dnt = navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNotTrack === '1';
var withHash = tag.hasAttribute('data-hash');
var last = '';
var first = true;
function send(body) {
if (dnt) return;
body.website = site;
body.hostname = location.hostname;
body.language = navigator.language || '';
var json = JSON.stringify(body);
// text/plain keeps this a "simple" cross-origin request, so no preflight is needed
try {
if (navigator.sendBeacon && navigator.sendBeacon(base + '/api/send', new Blob([json], { type: 'text/plain' }))) return;
} catch (e) { /* fall through to fetch */ }
try {
fetch(base + '/api/send', { method: 'POST', body: json, headers: { 'Content-Type': 'text/plain' }, keepalive: true, mode: 'cors', credentials: 'omit' });
} catch (e) { /* analytics must never break the page */ }
}
function view() {
var path = location.pathname + (withHash ? location.hash : '');
if (path === last) return;
last = path;
send({ path: path, referrer: first ? document.referrer : '' });
first = false;
}
function hook(name) {
var orig = history[name];
if (!orig) return;
history[name] = function () {
var r = orig.apply(this, arguments);
setTimeout(view, 0);
return r;
};
}
hook('pushState');
hook('replaceState');
window.addEventListener('popstate', function () { setTimeout(view, 0); });
if (withHash) window.addEventListener('hashchange', function () { setTimeout(view, 0); });
window.sandbar = {
view: view,
track: function (name, data) {
if (!name) return;
send({ path: location.pathname, event: String(name).slice(0, 80), data: data });
}
};
if (tag.getAttribute('data-auto') === 'false') return;
if (document.readyState === 'complete') view();
else window.addEventListener('load', view);
})();