aggressive data collection :PP

This commit is contained in:
2026-05-14 23:16:17 +01:00
parent aacd9f202b
commit 4a0a00cbb8
3 changed files with 134 additions and 12 deletions
+6 -3
View File
@@ -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 `Configure`. A necessary cookie remembers the visitor's choice. If a visitor
allows analytics, the browser sends page-view and heartbeat events to allows analytics, the browser sends page-view and heartbeat events to
`POST /api/viewers/event`. Visitors can choose whether to include browser/device, `POST /api/viewers/event`. Visitors can choose whether to include browser/device,
screen, language/timezone, and referrer details. The public `/viewers` page reads screen, language/timezone, referrer, and technical diagnostics. Technical
`GET /api/viewers` and shows active pages, 24-hour page totals, top pages, and diagnostics can include HTTP version, protocol headers, content negotiation
any consented client details. 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 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. default. Raw IP addresses and IP hashes are not stored in viewer analytics.
+38 -1
View File
@@ -385,6 +385,43 @@ function sanitizePath(value) {
return raw 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 = '') { function parseClient(userAgent = '') {
const ua = String(userAgent) const ua = String(userAgent)
let browser = 'Unknown' let browser = 'Unknown'
@@ -475,7 +512,7 @@ function recordViewerEvent(req, payload) {
language: sanitizeText(payload.language, 40), language: sanitizeText(payload.language, 40),
timezone: sanitizeText(payload.timezone, 80), timezone: sanitizeText(payload.timezone, 80),
consent: payload.consent === 'analytics' ? 'analytics' : '', 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') { if (event.consent !== 'analytics') {
+90 -8
View File
@@ -33,7 +33,7 @@ const analyticsConsentKey = 'tssbot.analyticsConsent'
const analyticsPreferencesKey = 'tssbot.analyticsPreferences' const analyticsPreferencesKey = 'tssbot.analyticsPreferences'
const analyticsPreferencesCookie = 'tssbot_analytics_preferences' const analyticsPreferencesCookie = 'tssbot_analytics_preferences'
const analyticsVisitorKey = 'tssbot.analyticsVisitor' const analyticsVisitorKey = 'tssbot.analyticsVisitor'
const analyticsConsentVersion = 2 const analyticsConsentVersion = 3
const defaultAnalyticsPreferences = { const defaultAnalyticsPreferences = {
chosen: false, chosen: false,
@@ -42,6 +42,7 @@ const defaultAnalyticsPreferences = {
display: false, display: false,
locale: false, locale: false,
referrer: false, referrer: false,
diagnostics: false,
version: analyticsConsentVersion, version: analyticsConsentVersion,
} }
@@ -100,6 +101,7 @@ function normalizeAnalyticsPreferences(value) {
display: Boolean(value.display), display: Boolean(value.display),
locale: Boolean(value.locale), locale: Boolean(value.locale),
referrer: Boolean(value.referrer), referrer: Boolean(value.referrer),
diagnostics: Boolean(value.diagnostics),
version: analyticsConsentVersion, 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) { function readCookie(name) {
try { try {
const match = document.cookie const match = document.cookie
@@ -160,6 +194,7 @@ function storedAnalyticsPreferences() {
display: true, display: true,
locale: true, locale: true,
referrer: true, referrer: true,
diagnostics: true,
} }
} }
if (legacyConsent === 'declined') { if (legacyConsent === 'declined') {
@@ -372,9 +407,49 @@ function App() {
const displayDetails = analyticsPreferences.display const displayDetails = analyticsPreferences.display
const localeDetails = analyticsPreferences.locale const localeDetails = analyticsPreferences.locale
const referrerDetails = analyticsPreferences.referrer const referrerDetails = analyticsPreferences.referrer
const diagnosticsDetails = analyticsPreferences.diagnostics
function sendViewerEvent(eventType) { function sendViewerEvent(eventType) {
if (stopped) return 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 = { const body = {
consent: 'analytics', consent: 'analytics',
@@ -391,12 +466,7 @@ function App() {
screen: displayDetails ? `${window.screen.width}x${window.screen.height}` : '', screen: displayDetails ? `${window.screen.width}x${window.screen.height}` : '',
language: localeDetails ? navigator.language : '', language: localeDetails ? navigator.language : '',
timezone: localeDetails ? Intl.DateTimeFormat().resolvedOptions().timeZone : '', timezone: localeDetails ? Intl.DateTimeFormat().resolvedOptions().timeZone : '',
metadata: displayDetails metadata,
? {
color_depth: window.screen.colorDepth,
viewport: `${window.innerWidth}x${window.innerHeight}`,
}
: {},
} }
fetch(apiEndpoints.viewerEvent, { fetch(apiEndpoints.viewerEvent, {
@@ -1020,6 +1090,13 @@ function ConsentBanner({ preferences, onChoose }) {
label="Referrer" label="Referrer"
onChange={(value) => updateDraft('referrer', value)} onChange={(value) => updateDraft('referrer', value)}
/> />
<PreferenceToggle
checked={draft.diagnostics}
description="Includes HTTP version, protocol headers, cache/debug headers, network quality, privacy signals, touch support, CPU/memory hints, and other browser diagnostics."
disabled={!draft.analytics}
label="Technical diagnostics"
onChange={(value) => updateDraft('diagnostics', value)}
/>
</div> </div>
<div className="mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"> <div className="mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
@@ -1047,6 +1124,7 @@ function ConsentBanner({ preferences, onChoose }) {
display: true, display: true,
locale: true, locale: true,
referrer: true, referrer: true,
diagnostics: true,
version: analyticsConsentVersion, version: analyticsConsentVersion,
}) })
} }
@@ -1075,6 +1153,7 @@ function ConsentBanner({ preferences, onChoose }) {
display: true, display: true,
locale: true, locale: true,
referrer: true, referrer: true,
diagnostics: true,
version: analyticsConsentVersion, version: analyticsConsentVersion,
}) })
} }
@@ -1132,7 +1211,10 @@ function PrivacyPage() {
Necessary cookies store your cookie and analytics choices. If you opt in to Necessary cookies store your cookie and analytics choices. If you opt in to
viewer analytics, we collect page views, live viewing status, a pseudonymous viewer analytics, we collect page views, live viewing status, a pseudonymous
visitor ID, and a session ID. Optional settings control whether browser/device, 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.
</PrivacySection> </PrivacySection>
<PrivacySection title="Why we collect it"> <PrivacySection title="Why we collect it">