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