feat(collector): Implement Beacon client for sending telemetry data via browser-to-server API

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-05-16 18:57:18 -07:00
parent e1f469b6f1
commit b507184df9

View file

@ -0,0 +1,123 @@
/*!
* @lilith/user-data-beacon cookie-free, fingerprint-free pageview beacon.
*
* Static-HTML sites (Adult Therapy Tour, Sansonnet, SEO bait) drop in:
* <script defer src="https://quinn.data/beacon.js" data-site="att"></script>
*
* The collector at quinn.data derives visitor_id_daily server-side from
* (daily-rotating salt || IP || UA || Accept-Language). No client identity is
* sent. No cookies, no localStorage, no canvas fingerprinting.
*/
(function () {
'use strict';
// nginx on data.transquinnftw.com routes /analytics/track/* → collector :4001
// and injects X-Write-Key server-side.
var COLLECTOR = 'https://data.transquinnftw.com/analytics/track';
function tag() {
var s = document.currentScript;
if (s && s.getAttribute) return s.getAttribute('data-site') || null;
var all = document.getElementsByTagName('script');
for (var i = 0; i < all.length; i++) {
if (/\/beacon\.js(\?|$)/.test(all[i].src || '')) {
return all[i].getAttribute('data-site');
}
}
return null;
}
function clientDevice() {
var s = window.screen || {};
return {
screenWidth: s.width,
screenHeight: s.height,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
pixelRatio: window.devicePixelRatio,
colorDepth: s.colorDepth,
language: navigator.language,
languages: navigator.languages ? Array.prototype.slice.call(navigator.languages) : undefined,
timezone: (Intl && Intl.DateTimeFormat && Intl.DateTimeFormat().resolvedOptions().timeZone) || undefined,
timezoneOffset: new Date().getTimezoneOffset(),
deviceMemory: navigator.deviceMemory,
hardwareConcurrency: navigator.hardwareConcurrency,
touchPoints: navigator.maxTouchPoints,
cookiesEnabled: navigator.cookieEnabled,
doNotTrack: navigator.doNotTrack === '1' || navigator.doNotTrack === 'yes'
};
}
function attribution() {
var p = new URLSearchParams(window.location.search);
return {
utmSource: p.get('utm_source') || undefined,
utmMedium: p.get('utm_medium') || undefined,
utmCampaign: p.get('utm_campaign') || undefined,
utmContent: p.get('utm_content') || undefined,
utmTerm: p.get('utm_term') || undefined,
referrer: document.referrer || undefined,
via: p.get('via') || undefined
};
}
// Session id: ephemeral, per-tab, in memory only. Visitor stitching
// happens server-side via the daily-rotating hash — this id is only used
// for tab-scoped funnel chaining.
var sessionId = Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
var site = tag();
var landedAt = Date.now();
function post(path, body, useBeacon) {
var url = COLLECTOR + path;
try {
var payload = JSON.stringify(body);
if (useBeacon && navigator.sendBeacon) {
navigator.sendBeacon(url, new Blob([payload], { type: 'application/json' }));
return;
}
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: payload,
keepalive: true,
credentials: 'omit'
}).catch(function () { /* silent */ });
} catch (e) { /* silent */ }
}
function trackView() {
post('/view', {
pageUrl: window.location.href,
referrer: document.referrer || undefined,
sessionId: sessionId,
clientDevice: clientDevice(),
attribution: attribution(),
app: site || undefined,
metadata: { dataSite: site }
}, false);
}
function trackViewEnd() {
post('/event', {
eventType: 'view_end',
sessionId: sessionId,
pageUrl: window.location.href,
metadata: {
durationMs: Date.now() - landedAt,
dataSite: site
}
}, true);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', trackView, { once: true });
} else {
trackView();
}
window.addEventListener('pagehide', trackViewEnd, { once: true });
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'hidden') trackViewEnd();
});
})();