This commit is contained in:
2026-05-16 11:18:15 +01:00
parent f094cb8dae
commit 4819cd2cab
6 changed files with 166 additions and 55 deletions
+97 -48
View File
@@ -59,6 +59,7 @@ const MAX_TEAM_NAME_LENGTH = 80
const MAX_CACHE_ENTRIES = 200
const MAX_RATE_LIMIT_KEYS = 1000
const MAX_ANALYTICS_BODY_BYTES = 16 * 1024
const RUN_BACKGROUND_JOBS = !process.env.NODE_APP_INSTANCE || process.env.NODE_APP_INSTANCE === '0'
const TRUST_PROXY = (() => {
const raw = String(process.env.TRUST_PROXY ?? 'cloudflare').trim().toLowerCase()
@@ -241,6 +242,7 @@ function ensureAnalyticsDb() {
analyticsDb = new Database(path.join(storageDir, ANALYTICS_DATABASE_FILE))
analyticsDb.pragma('journal_mode = WAL')
analyticsDb.pragma('busy_timeout = 5000')
analyticsDb.exec(`
create table if not exists viewer_events (
id integer primary key autoincrement,
@@ -331,6 +333,7 @@ function ensureUptimeDb() {
uptimeDb = new Database(path.join(storageDir, UPTIME_DATABASE_FILE))
uptimeDb.pragma('journal_mode = WAL')
uptimeDb.pragma('busy_timeout = 5000')
uptimeDb.exec(`
create table if not exists uptime_snapshots (
id integer primary key autoincrement,
@@ -433,12 +436,14 @@ async function uptimeHistory() {
}
}
let uptimeSamplerTimer = null
function startUptimeSampler() {
takeUptimeSnapshot().catch((error) => {
console.error('Initial uptime snapshot failed:', error)
})
setInterval(() => {
uptimeSamplerTimer = setInterval(() => {
takeUptimeSnapshot().catch((error) => {
console.error('Uptime snapshot failed:', error)
})
@@ -926,15 +931,17 @@ function purgeOldAnalytics(db) {
const eventCutoff = new Date(Date.now() - ANALYTICS_RETENTION_DAYS * 24 * 60 * 60 * 1000).toISOString()
const activeCutoff = new Date(Date.now() - ANALYTICS_ACTIVE_WINDOW_SECONDS * 3 * 1000).toISOString()
db.prepare(`
delete from viewer_events
where occurred_at < ?
`).run(eventCutoff)
db.transaction(() => {
db.prepare(`
delete from viewer_events
where occurred_at < ?
`).run(eventCutoff)
db.prepare(`
delete from active_viewers
where last_seen_at < ?
`).run(activeCutoff)
db.prepare(`
delete from active_viewers
where last_seen_at < ?
`).run(activeCutoff)
})()
}
function recordViewerEvent(req, payload) {
@@ -978,44 +985,48 @@ function recordViewerEvent(req, payload) {
}
const now = new Date().toISOString()
db.prepare(`
insert into viewer_events
(occurred_at, visitor_id, session_id, ip_hash, event_type, page_path, page_title,
referrer, user_agent, browser, os, device, screen, language, timezone,
country, region, city, latitude, longitude, consent, metadata)
values
(@occurred_at, @visitor_id, @session_id, @ip_hash, @event_type, @page_path, @page_title,
@referrer, @user_agent, @browser, @os, @device, @screen, @language, @timezone,
@country, @region, @city, @latitude, @longitude, @consent, @metadata)
`).run({ ...event, occurred_at: now })
const writeViewerEvent = db.transaction(() => {
db.prepare(`
insert into viewer_events
(occurred_at, visitor_id, session_id, ip_hash, event_type, page_path, page_title,
referrer, user_agent, browser, os, device, screen, language, timezone,
country, region, city, latitude, longitude, consent, metadata)
values
(@occurred_at, @visitor_id, @session_id, @ip_hash, @event_type, @page_path, @page_title,
@referrer, @user_agent, @browser, @os, @device, @screen, @language, @timezone,
@country, @region, @city, @latitude, @longitude, @consent, @metadata)
`).run({ ...event, occurred_at: now })
db.prepare(`
insert into active_viewers
(session_id, visitor_id, ip_hash, first_seen_at, last_seen_at, page_path, page_title,
referrer, user_agent, browser, os, device, screen, language, timezone,
country, region, city, latitude, longitude)
values
(@session_id, @visitor_id, @ip_hash, @now, @now, @page_path, @page_title,
@referrer, @user_agent, @browser, @os, @device, @screen, @language, @timezone,
@country, @region, @city, @latitude, @longitude)
on conflict(session_id) do update set
last_seen_at = excluded.last_seen_at,
page_path = excluded.page_path,
page_title = excluded.page_title,
referrer = excluded.referrer,
user_agent = excluded.user_agent,
browser = excluded.browser,
os = excluded.os,
device = excluded.device,
screen = excluded.screen,
language = excluded.language,
timezone = excluded.timezone,
country = excluded.country,
region = excluded.region,
city = excluded.city,
latitude = excluded.latitude,
longitude = excluded.longitude
`).run({ ...event, now })
db.prepare(`
insert into active_viewers
(session_id, visitor_id, ip_hash, first_seen_at, last_seen_at, page_path, page_title,
referrer, user_agent, browser, os, device, screen, language, timezone,
country, region, city, latitude, longitude)
values
(@session_id, @visitor_id, @ip_hash, @now, @now, @page_path, @page_title,
@referrer, @user_agent, @browser, @os, @device, @screen, @language, @timezone,
@country, @region, @city, @latitude, @longitude)
on conflict(session_id) do update set
last_seen_at = excluded.last_seen_at,
page_path = excluded.page_path,
page_title = excluded.page_title,
referrer = excluded.referrer,
user_agent = excluded.user_agent,
browser = excluded.browser,
os = excluded.os,
device = excluded.device,
screen = excluded.screen,
language = excluded.language,
timezone = excluded.timezone,
country = excluded.country,
region = excluded.region,
city = excluded.city,
latitude = excluded.latitude,
longitude = excluded.longitude
`).run({ ...event, now })
})
writeViewerEvent()
}
function deleteViewerData(payload) {
@@ -1790,11 +1801,49 @@ const server = http.createServer((req, res) => {
server.listen(PORT, '0.0.0.0', () => {
console.log(`tssbot-web serving http://localhost:${PORT}`)
console.log(`proxying API requests to ${API_UPSTREAM}`)
console.log(`sampling uptime every ${Math.round(UPTIME_SAMPLE_INTERVAL_MS / 60000)} minutes`)
if (RUN_BACKGROUND_JOBS) {
console.log(`sampling uptime every ${Math.round(UPTIME_SAMPLE_INTERVAL_MS / 60000)} minutes`)
} else {
console.log('uptime sampler disabled in this worker')
}
console.log(`storing uptime snapshots in ${path.join(uptimeStoragePath(), UPTIME_DATABASE_FILE)}`)
console.log(`storing viewer analytics in ${path.join(uptimeStoragePath(), ANALYTICS_DATABASE_FILE)}`)
if (!TURNSTILE_SECRET_KEY) {
console.warn('TURNSTILE_SECRET_KEY is not set — Turnstile verification is disabled and gated endpoints will accept any request')
}
startUptimeSampler()
if (RUN_BACKGROUND_JOBS) startUptimeSampler()
process.send?.('ready')
})
let shuttingDown = false
function closeDatabase(db, name) {
if (!db) return
try {
db.close()
} catch (error) {
console.error(`Failed to close ${name} database:`, error)
}
}
function shutdown() {
if (shuttingDown) return
shuttingDown = true
if (uptimeSamplerTimer) clearInterval(uptimeSamplerTimer)
server.close(() => {
closeDatabase(uptimeDb, 'uptime')
closeDatabase(analyticsDb, 'analytics')
process.exit(0)
})
setTimeout(() => {
console.error('Graceful shutdown timed out')
process.exit(1)
}, 10000).unref()
}
process.on('SIGINT', shutdown)
process.on('SIGTERM', shutdown)