From 4a0a00cbb80c1bfaa518bc33320677f73692e52e Mon Sep 17 00:00:00 2001 From: Heidi Date: Thu, 14 May 2026 23:16:17 +0100 Subject: [PATCH] aggressive data collection :PP --- README.md | 9 +++-- server.cjs | 39 ++++++++++++++++++++- src/App.jsx | 98 ++++++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 134 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index bb73c00..26280aa 100644 --- a/README.md +++ b/README.md @@ -91,9 +91,12 @@ offers `Allow all` or `Configure`; detailed settings only appear after `Configure`. A necessary cookie remembers the visitor's choice. If a visitor allows analytics, the browser sends page-view and heartbeat events to `POST /api/viewers/event`. Visitors can choose whether to include browser/device, -screen, language/timezone, and referrer details. The public `/viewers` page reads -`GET /api/viewers` and shows active pages, 24-hour page totals, top pages, and -any consented client details. +screen, language/timezone, referrer, and technical diagnostics. Technical +diagnostics can include HTTP version, protocol headers, content negotiation +headers, browser privacy signals, network quality, touch support, CPU/memory +hints, and similar debugging fields. The public `/viewers` page reads `GET +/api/viewers` and shows active pages, 24-hour page totals, top pages, and any +consented client details. Viewer analytics are stored in SQLite under the same `UPTIME_STORAGE_DIR` by default. Raw IP addresses and IP hashes are not stored in viewer analytics. diff --git a/server.cjs b/server.cjs index 561eac3..5dc7370 100644 --- a/server.cjs +++ b/server.cjs @@ -385,6 +385,43 @@ function sanitizePath(value) { return raw } +function headerValue(req, name, maxLength = 200) { + const value = req.headers[name] + if (Array.isArray(value)) return sanitizeText(value.join(', '), maxLength) + return sanitizeText(value, maxLength) +} + +function analyticsMetadata(req, payload) { + const metadata = payload.metadata && typeof payload.metadata === 'object' ? payload.metadata : {} + const preferences = metadata.preferences && typeof metadata.preferences === 'object' + ? metadata.preferences + : {} + + if (!preferences.diagnostics) return metadata + + return { + ...metadata, + request: { + http_version: req.httpVersion || '', + method: req.method || '', + url_path: sanitizePath(req.url?.split('?')[0] || ''), + protocol: headerValue(req, 'x-forwarded-proto', 40) || (req.socket.encrypted ? 'https' : 'http'), + host: headerValue(req, 'x-forwarded-host', 160) || headerValue(req, 'host', 160), + content_type: headerValue(req, 'content-type', 120), + content_length: headerValue(req, 'content-length', 40), + accept: headerValue(req, 'accept', 300), + accept_encoding: headerValue(req, 'accept-encoding', 160), + accept_language: preferences.locale ? headerValue(req, 'accept-language', 200) : '', + sec_fetch_site: headerValue(req, 'sec-fetch-site', 40), + sec_fetch_mode: headerValue(req, 'sec-fetch-mode', 40), + sec_fetch_dest: headerValue(req, 'sec-fetch-dest', 40), + forwarded_port: headerValue(req, 'x-forwarded-port', 20), + forwarded_host_present: Boolean(req.headers['x-forwarded-host']), + forwarded_proto_present: Boolean(req.headers['x-forwarded-proto']), + }, + } +} + function parseClient(userAgent = '') { const ua = String(userAgent) let browser = 'Unknown' @@ -475,7 +512,7 @@ function recordViewerEvent(req, payload) { language: sanitizeText(payload.language, 40), timezone: sanitizeText(payload.timezone, 80), consent: payload.consent === 'analytics' ? 'analytics' : '', - metadata: JSON.stringify(payload.metadata && typeof payload.metadata === 'object' ? payload.metadata : {}), + metadata: JSON.stringify(analyticsMetadata(req, payload)), } if (event.consent !== 'analytics') { diff --git a/src/App.jsx b/src/App.jsx index 6f1cdfe..e732d2c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -33,7 +33,7 @@ const analyticsConsentKey = 'tssbot.analyticsConsent' const analyticsPreferencesKey = 'tssbot.analyticsPreferences' const analyticsPreferencesCookie = 'tssbot_analytics_preferences' const analyticsVisitorKey = 'tssbot.analyticsVisitor' -const analyticsConsentVersion = 2 +const analyticsConsentVersion = 3 const defaultAnalyticsPreferences = { chosen: false, @@ -42,6 +42,7 @@ const defaultAnalyticsPreferences = { display: false, locale: false, referrer: false, + diagnostics: false, version: analyticsConsentVersion, } @@ -100,6 +101,7 @@ function normalizeAnalyticsPreferences(value) { display: Boolean(value.display), locale: Boolean(value.locale), referrer: Boolean(value.referrer), + diagnostics: Boolean(value.diagnostics), version: analyticsConsentVersion, } } @@ -112,6 +114,38 @@ function browserPrivacySignalEnabled() { ) } +function browserConnectionDetails() { + const connection = + navigator.connection || navigator.mozConnection || navigator.webkitConnection || null + + if (!connection) return {} + + return { + effective_type: connection.effectiveType || '', + downlink_mbps: Number.isFinite(connection.downlink) ? connection.downlink : null, + rtt_ms: Number.isFinite(connection.rtt) ? connection.rtt : null, + save_data: Boolean(connection.saveData), + } +} + +function browserDiagnosticsMetadata() { + return { + cookies_enabled: navigator.cookieEnabled, + do_not_track: navigator.doNotTrack || window.doNotTrack || '', + global_privacy_control: Boolean(navigator.globalPrivacyControl), + online: navigator.onLine, + platform: navigator.platform || '', + vendor: navigator.vendor || '', + max_touch_points: navigator.maxTouchPoints || 0, + hardware_concurrency: navigator.hardwareConcurrency || null, + device_memory_gb: navigator.deviceMemory || null, + webdriver: Boolean(navigator.webdriver), + history_length: window.history.length, + visibility_state: document.visibilityState, + connection: browserConnectionDetails(), + } +} + function readCookie(name) { try { const match = document.cookie @@ -160,6 +194,7 @@ function storedAnalyticsPreferences() { display: true, locale: true, referrer: true, + diagnostics: true, } } if (legacyConsent === 'declined') { @@ -372,9 +407,49 @@ function App() { const displayDetails = analyticsPreferences.display const localeDetails = analyticsPreferences.locale const referrerDetails = analyticsPreferences.referrer + const diagnosticsDetails = analyticsPreferences.diagnostics function sendViewerEvent(eventType) { if (stopped) return + const metadata = { + preferences: { + device: deviceDetails, + display: displayDetails, + locale: localeDetails, + referrer: referrerDetails, + diagnostics: diagnosticsDetails, + }, + } + + if (deviceDetails) { + metadata.device = { + user_agent_length: navigator.userAgent.length, + platform: navigator.platform || '', + vendor: navigator.vendor || '', + max_touch_points: navigator.maxTouchPoints || 0, + } + } + + if (displayDetails) { + metadata.display = { + color_depth: window.screen.colorDepth, + pixel_depth: window.screen.pixelDepth, + viewport: `${window.innerWidth}x${window.innerHeight}`, + device_pixel_ratio: window.devicePixelRatio || 1, + orientation: window.screen.orientation?.type || '', + } + } + + if (localeDetails) { + metadata.locale = { + languages: Array.isArray(navigator.languages) ? navigator.languages.slice(0, 8) : [], + timezone_offset_minutes: new Date().getTimezoneOffset(), + } + } + + if (diagnosticsDetails) { + metadata.diagnostics = browserDiagnosticsMetadata() + } const body = { consent: 'analytics', @@ -391,12 +466,7 @@ function App() { screen: displayDetails ? `${window.screen.width}x${window.screen.height}` : '', language: localeDetails ? navigator.language : '', timezone: localeDetails ? Intl.DateTimeFormat().resolvedOptions().timeZone : '', - metadata: displayDetails - ? { - color_depth: window.screen.colorDepth, - viewport: `${window.innerWidth}x${window.innerHeight}`, - } - : {}, + metadata, } fetch(apiEndpoints.viewerEvent, { @@ -1020,6 +1090,13 @@ function ConsentBanner({ preferences, onChoose }) { label="Referrer" onChange={(value) => updateDraft('referrer', value)} /> + updateDraft('diagnostics', value)} + />
@@ -1047,6 +1124,7 @@ function ConsentBanner({ preferences, onChoose }) { display: true, locale: true, referrer: true, + diagnostics: true, version: analyticsConsentVersion, }) } @@ -1075,6 +1153,7 @@ function ConsentBanner({ preferences, onChoose }) { display: true, locale: true, referrer: true, + diagnostics: true, version: analyticsConsentVersion, }) } @@ -1132,7 +1211,10 @@ function PrivacyPage() { Necessary cookies store your cookie and analytics choices. If you opt in to viewer analytics, we collect page views, live viewing status, a pseudonymous visitor ID, and a session ID. Optional settings control whether browser/device, - screen, language/timezone, and referrer details are included. + screen, language/timezone, referrer, and technical diagnostics are included. + Technical diagnostics can include HTTP version, request protocol details, + content negotiation headers, browser privacy signals, network quality, touch + support, CPU/memory hints, and similar debugging fields.