aggressive data collection :PP
This commit is contained in:
@@ -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
@@ -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
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user