From b507184df96a2038d337a05ef6d44c29992b6055 Mon Sep 17 00:00:00 2001 From: autocommit Date: Sat, 16 May 2026 18:57:18 -0700 Subject: [PATCH] =?UTF-8?q?feat(collector):=20=E2=9C=A8=20Implement=20Beac?= =?UTF-8?q?on=20client=20for=20sending=20telemetry=20data=20via=20browser-?= =?UTF-8?q?to-server=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- services/collector/public/beacon.js | 123 ++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 services/collector/public/beacon.js diff --git a/services/collector/public/beacon.js b/services/collector/public/beacon.js new file mode 100644 index 0000000..57f296a --- /dev/null +++ b/services/collector/public/beacon.js @@ -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: + * + * + * 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(); + }); +})();