aggressive data collection :PP

This commit is contained in:
2026-05-14 23:07:30 +01:00
parent ef10da8b0b
commit fe2e2751d5
4 changed files with 503 additions and 73 deletions
+54 -11
View File
@@ -46,7 +46,6 @@ const UPTIME_HISTORY_LIMIT = Number(process.env.UPTIME_HISTORY_LIMIT || 336)
const ANALYTICS_DATABASE_FILE = process.env.ANALYTICS_DATABASE_FILE || 'viewers.sqlite'
const ANALYTICS_RETENTION_DAYS = Number(process.env.ANALYTICS_RETENTION_DAYS || 30)
const ANALYTICS_ACTIVE_WINDOW_SECONDS = Number(process.env.ANALYTICS_ACTIVE_WINDOW_SECONDS || 75)
const ANALYTICS_SALT = process.env.ANALYTICS_SALT || 'change-me-viewer-salt'
const API_CACHE_TTL_MS = Number(process.env.API_CACHE_TTL_MS || 15000)
const API_RATE_LIMIT_WINDOW_MS = Number(process.env.API_RATE_LIMIT_WINDOW_MS || 60000)
const API_RATE_LIMIT_MAX = Number(process.env.API_RATE_LIMIT_MAX || 120)
@@ -376,10 +375,6 @@ function clientIp(req) {
return req.socket.remoteAddress || 'unknown'
}
function hashIp(ip) {
return crypto.createHash('sha256').update(`${ANALYTICS_SALT}:${ip}`).digest('hex')
}
function sanitizeText(value, maxLength = 200) {
return String(value || '').replace(/[\u0000-\u001f\u007f]/g, '').trim().slice(0, maxLength)
}
@@ -458,20 +453,24 @@ function recordViewerEvent(req, payload) {
purgeOldAnalytics(db)
const serverClient = parseClient(req.headers['user-agent'] || '')
const shareUserAgent = payload.user_agent !== 'Not shared'
const event = {
visitor_id: sanitizeText(payload.visitor_id, 80) || crypto.randomUUID(),
session_id: sanitizeText(payload.session_id, 80) || crypto.randomUUID(),
ip_hash: hashIp(clientIp(req)),
ip_hash: '',
event_type: ['page_view', 'heartbeat', 'consent'].includes(payload.event_type)
? payload.event_type
: 'heartbeat',
page_path: sanitizePath(payload.page_path),
page_title: sanitizeText(payload.page_title, 160),
referrer: sanitizeText(payload.referrer, 300),
user_agent: sanitizeText(req.headers['user-agent'] || payload.user_agent, 500),
browser: sanitizeText(payload.browser || serverClient.browser, 80),
os: sanitizeText(payload.os || serverClient.os, 80),
device: sanitizeText(payload.device || serverClient.device, 80),
user_agent: sanitizeText(
shareUserAgent ? payload.user_agent || req.headers['user-agent'] : 'Not shared',
500,
),
browser: sanitizeText(payload.browser || (shareUserAgent ? serverClient.browser : 'Not shared'), 80),
os: sanitizeText(payload.os || (shareUserAgent ? serverClient.os : 'Not shared'), 80),
device: sanitizeText(payload.device || (shareUserAgent ? serverClient.device : 'Not shared'), 80),
screen: sanitizeText(payload.screen, 40),
language: sanitizeText(payload.language, 40),
timezone: sanitizeText(payload.timezone, 80),
@@ -515,6 +514,35 @@ function recordViewerEvent(req, payload) {
`).run({ ...event, now })
}
function deleteViewerData(payload) {
const db = ensureAnalyticsDb()
const visitorId = sanitizeText(payload.visitor_id, 80)
const sessionId = sanitizeText(payload.session_id, 80)
if (!visitorId && !sessionId) {
throw new Error('A visitor or session identifier is required')
}
const result = db.transaction(() => {
let eventsDeleted = 0
let sessionsDeleted = 0
if (visitorId) {
eventsDeleted += db.prepare('delete from viewer_events where visitor_id = ?').run(visitorId).changes
sessionsDeleted += db.prepare('delete from active_viewers where visitor_id = ?').run(visitorId).changes
}
if (sessionId) {
eventsDeleted += db.prepare('delete from viewer_events where session_id = ?').run(sessionId).changes
sessionsDeleted += db.prepare('delete from active_viewers where session_id = ?').run(sessionId).changes
}
return { events_deleted: eventsDeleted, sessions_deleted: sessionsDeleted }
})()
return result
}
function viewerDashboard() {
const db = ensureAnalyticsDb()
purgeOldAnalytics(db)
@@ -585,7 +613,7 @@ function viewerDashboard() {
},
privacy: {
retention_days: ANALYTICS_RETENTION_DAYS,
stores_ip_hashes: true,
stores_ip_hashes: false,
exposes_raw_ip: false,
},
}
@@ -801,6 +829,21 @@ const server = http.createServer((req, res) => {
return
}
if (req.method === 'POST' && req.url === '/api/viewers/delete') {
if (!isSameOriginRequest(req)) {
sendJson(res, 403, { error: 'Analytics deletion is restricted to this site' })
return
}
readJsonBody(req)
.then((payload) => {
const result = deleteViewerData(payload)
sendJson(res, 200, result)
})
.catch((error) => sendJson(res, 400, { error: error.message }))
return
}
if (req.method === 'OPTIONS' && req.url.startsWith('/api/')) {
sendJson(res, 403, { error: 'CORS requests are not allowed' })
return