This commit is contained in:
2026-05-16 12:20:14 +01:00
parent 4feac9a1fc
commit 1cc500e428
3 changed files with 706 additions and 33 deletions
+39 -4
View File
@@ -259,6 +259,7 @@ function ensureAnalyticsDb() {
os text not null default 'Unknown',
device text not null default 'Desktop',
screen text not null default '',
theme text not null default 'light',
language text not null default '',
timezone text not null default '',
country text not null default '',
@@ -284,6 +285,7 @@ function ensureAnalyticsDb() {
os text not null default 'Unknown',
device text not null default 'Desktop',
screen text not null default '',
theme text not null default 'light',
language text not null default '',
timezone text not null default '',
country text not null default '',
@@ -314,6 +316,8 @@ function ensureAnalyticsDb() {
`alter table active_viewers add column latitude real`,
`alter table viewer_events add column longitude real`,
`alter table active_viewers add column longitude real`,
`alter table viewer_events add column theme text not null default 'light'`,
`alter table active_viewers add column theme text not null default 'light'`,
]) {
try {
analyticsDb.exec(statement)
@@ -578,6 +582,10 @@ function numberHeader(req, name, min, max) {
return value
}
function sanitizeTheme(value) {
return value === 'dark' ? 'dark' : 'light'
}
const CITY_COORDINATE_OVERRIDES = new Map([
['GB|ENGLAND|MILTON KEYNES', { latitude: 52.0406, longitude: -0.7594 }],
])
@@ -969,6 +977,7 @@ function recordViewerEvent(req, payload) {
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),
theme: sanitizeTheme(payload.theme),
language: sanitizeText(payload.language, 40),
timezone: sanitizeText(payload.timezone, 80),
country: location.country,
@@ -989,22 +998,22 @@ function recordViewerEvent(req, payload) {
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,
referrer, user_agent, browser, os, device, screen, theme, 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,
@referrer, @user_agent, @browser, @os, @device, @screen, @theme, @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,
referrer, user_agent, browser, os, device, screen, theme, 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,
@referrer, @user_agent, @browser, @os, @device, @screen, @theme, @language, @timezone,
@country, @region, @city, @latitude, @longitude)
on conflict(session_id) do update set
last_seen_at = excluded.last_seen_at,
@@ -1016,6 +1025,7 @@ function recordViewerEvent(req, payload) {
os = excluded.os,
device = excluded.device,
screen = excluded.screen,
theme = excluded.theme,
language = excluded.language,
timezone = excluded.timezone,
country = excluded.country,
@@ -1077,6 +1087,7 @@ function viewerDashboard() {
max(os) as os,
max(device) as device,
max(screen) as screen,
max(theme) as theme,
max(language) as language,
max(timezone) as timezone,
max(country) as country,
@@ -1102,6 +1113,7 @@ function viewerDashboard() {
os: row.os,
device: row.device,
screen: row.screen,
theme: sanitizeTheme(row.theme),
language: row.language,
timezone: row.timezone,
country: row.country,
@@ -1121,12 +1133,14 @@ function viewerDashboard() {
visitors: new Set(),
clients: new Map(),
countries: new Set(),
themes: new Map(),
last_seen_at: viewer.last_seen_at,
}
existing.viewers += viewer.sessions || 1
existing.visitors.add(viewer.visitor)
if (viewer.country) existing.countries.add(viewer.country)
existing.themes.set(viewer.theme, (existing.themes.get(viewer.theme) || 0) + (viewer.sessions || 1))
const clientKey = `${viewer.browser} on ${viewer.os}`
existing.clients.set(clientKey, (existing.clients.get(clientKey) || 0) + 1)
if (new Date(viewer.last_seen_at).getTime() > new Date(existing.last_seen_at).getTime()) {
@@ -1142,6 +1156,9 @@ function viewerDashboard() {
viewers: page.viewers,
visitors: page.visitors.size,
countries: Array.from(page.countries).sort(),
themes: Array.from(page.themes.entries())
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
.map(([theme, count]) => ({ theme, count })),
clients: Array.from(page.clients.entries())
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
.slice(0, 4)
@@ -1188,6 +1205,22 @@ function viewerDashboard() {
limit 12
`).all(thirtyDaysSince)
const themes = db.prepare(`
select theme, count(*) as events, count(distinct visitor_id) as visitors
from viewer_events
where occurred_at >= ?
group by theme
order by events desc
`).all(daySince).map((row) => ({ ...row, theme: sanitizeTheme(row.theme) }))
const themes30d = db.prepare(`
select theme, count(*) as events, count(distinct visitor_id) as visitors
from viewer_events
where occurred_at >= ?
group by theme
order by events desc
`).all(thirtyDaysSince).map((row) => ({ ...row, theme: sanitizeTheme(row.theme) }))
const activity24h = db.prepare(`
select
substr(occurred_at, 1, 13) || ':00:00.000Z' as date,
@@ -1447,6 +1480,8 @@ function viewerDashboard() {
top_pages_30d: topPages30d,
clients,
clients_30d: clients30d,
themes,
themes_30d: themes30d,
activity_24h: activity24hWithLabels,
activity_30d: activityWithLocations,
countries_24h: countries24h,