update viewers page
This commit is contained in:
+102
-7
@@ -172,6 +172,7 @@ function ensureAnalyticsDb() {
|
||||
screen text not null default '',
|
||||
language text not null default '',
|
||||
timezone text not null default '',
|
||||
country text not null default '',
|
||||
consent text not null default 'analytics',
|
||||
metadata text not null default '{}'
|
||||
);
|
||||
@@ -191,7 +192,8 @@ function ensureAnalyticsDb() {
|
||||
device text not null default 'Desktop',
|
||||
screen text not null default '',
|
||||
language text not null default '',
|
||||
timezone text not null default ''
|
||||
timezone text not null default '',
|
||||
country text not null default ''
|
||||
);
|
||||
|
||||
create index if not exists viewer_events_occurred_at_idx
|
||||
@@ -204,6 +206,17 @@ function ensureAnalyticsDb() {
|
||||
on active_viewers (last_seen_at desc);
|
||||
`)
|
||||
|
||||
for (const statement of [
|
||||
`alter table viewer_events add column country text not null default ''`,
|
||||
`alter table active_viewers add column country text not null default ''`,
|
||||
]) {
|
||||
try {
|
||||
analyticsDb.exec(statement)
|
||||
} catch (error) {
|
||||
if (!String(error.message || '').includes('duplicate column name')) throw error
|
||||
}
|
||||
}
|
||||
|
||||
return analyticsDb
|
||||
}
|
||||
|
||||
@@ -393,6 +406,17 @@ function headerValue(req, name, maxLength = 200) {
|
||||
return sanitizeText(value, maxLength)
|
||||
}
|
||||
|
||||
function countryFromHeaders(req) {
|
||||
const raw =
|
||||
headerValue(req, 'cf-ipcountry', 12) ||
|
||||
headerValue(req, 'x-vercel-ip-country', 12) ||
|
||||
headerValue(req, 'x-appengine-country', 12) ||
|
||||
headerValue(req, 'cloudfront-viewer-country', 12)
|
||||
const country = raw.toUpperCase()
|
||||
if (!/^[A-Z]{2}$/.test(country) || country === 'XX') return ''
|
||||
return country
|
||||
}
|
||||
|
||||
function analyticsMetadata(req, payload) {
|
||||
const metadata = payload.metadata && typeof payload.metadata === 'object' ? payload.metadata : {}
|
||||
const preferences = metadata.preferences && typeof metadata.preferences === 'object'
|
||||
@@ -513,6 +537,7 @@ function recordViewerEvent(req, payload) {
|
||||
screen: sanitizeText(payload.screen, 40),
|
||||
language: sanitizeText(payload.language, 40),
|
||||
timezone: sanitizeText(payload.timezone, 80),
|
||||
country: countryFromHeaders(req),
|
||||
consent: payload.consent === 'analytics' ? 'analytics' : '',
|
||||
metadata: JSON.stringify(analyticsMetadata(req, payload)),
|
||||
}
|
||||
@@ -525,19 +550,19 @@ 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, consent, metadata)
|
||||
referrer, user_agent, browser, os, device, screen, language, timezone, country, 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, @consent, @metadata)
|
||||
@referrer, @user_agent, @browser, @os, @device, @screen, @language, @timezone, @country, @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, language, timezone, country)
|
||||
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, @language, @timezone, @country)
|
||||
on conflict(session_id) do update set
|
||||
last_seen_at = excluded.last_seen_at,
|
||||
page_path = excluded.page_path,
|
||||
@@ -549,7 +574,8 @@ function recordViewerEvent(req, payload) {
|
||||
device = excluded.device,
|
||||
screen = excluded.screen,
|
||||
language = excluded.language,
|
||||
timezone = excluded.timezone
|
||||
timezone = excluded.timezone,
|
||||
country = excluded.country
|
||||
`).run({ ...event, now })
|
||||
}
|
||||
|
||||
@@ -589,7 +615,7 @@ function viewerDashboard() {
|
||||
const activeSince = `-${ANALYTICS_ACTIVE_WINDOW_SECONDS} seconds`
|
||||
const active = db.prepare(`
|
||||
select session_id, visitor_id, first_seen_at, last_seen_at, page_path, page_title,
|
||||
referrer, browser, os, device, screen, language, timezone
|
||||
referrer, browser, os, device, screen, language, timezone, country
|
||||
from active_viewers
|
||||
where last_seen_at >= datetime('now', ?)
|
||||
order by last_seen_at desc
|
||||
@@ -608,6 +634,7 @@ function viewerDashboard() {
|
||||
screen: row.screen,
|
||||
language: row.language,
|
||||
timezone: row.timezone,
|
||||
country: row.country,
|
||||
}))
|
||||
|
||||
const topPages = db.prepare(`
|
||||
@@ -629,6 +656,47 @@ function viewerDashboard() {
|
||||
limit 12
|
||||
`).all()
|
||||
|
||||
const clients30d = db.prepare(`
|
||||
select browser, os, device, count(*) as events, count(distinct visitor_id) as visitors
|
||||
from viewer_events
|
||||
where occurred_at >= datetime('now', '-30 days')
|
||||
group by browser, os, device
|
||||
order by events desc
|
||||
limit 12
|
||||
`).all()
|
||||
|
||||
const activity30d = db.prepare(`
|
||||
select
|
||||
date(occurred_at) as date,
|
||||
count(*) as events,
|
||||
count(distinct visitor_id) as visitors,
|
||||
sum(case when event_type = 'page_view' then 1 else 0 end) as page_views
|
||||
from viewer_events
|
||||
where occurred_at >= datetime('now', '-30 days')
|
||||
group by date(occurred_at)
|
||||
order by date asc
|
||||
`).all()
|
||||
|
||||
const countries = db.prepare(`
|
||||
select country, count(*) as events, count(distinct visitor_id) as visitors
|
||||
from viewer_events
|
||||
where occurred_at >= datetime('now', '-30 days')
|
||||
and country != ''
|
||||
group by country
|
||||
order by visitors desc, events desc
|
||||
limit 80
|
||||
`).all()
|
||||
|
||||
const locations = db.prepare(`
|
||||
select country, timezone, language, count(*) as events, count(distinct visitor_id) as visitors
|
||||
from viewer_events
|
||||
where occurred_at >= datetime('now', '-30 days')
|
||||
and (country != '' or (timezone != '' and timezone != 'Not shared'))
|
||||
group by country, timezone, language
|
||||
order by visitors desc, events desc
|
||||
limit 32
|
||||
`).all()
|
||||
|
||||
const totals = db.prepare(`
|
||||
select
|
||||
count(*) as events_24h,
|
||||
@@ -638,22 +706,49 @@ function viewerDashboard() {
|
||||
where occurred_at >= datetime('now', '-24 hours')
|
||||
`).get()
|
||||
|
||||
const totals30d = db.prepare(`
|
||||
select
|
||||
count(*) as events_30d,
|
||||
count(distinct visitor_id) as visitors_30d,
|
||||
count(distinct session_id) as sessions_30d,
|
||||
sum(case when event_type = 'page_view' then 1 else 0 end) as page_views_30d
|
||||
from viewer_events
|
||||
where occurred_at >= datetime('now', '-30 days')
|
||||
`).get()
|
||||
|
||||
return {
|
||||
active_window_seconds: ANALYTICS_ACTIVE_WINDOW_SECONDS,
|
||||
generated_at: new Date().toISOString(),
|
||||
active,
|
||||
top_pages: topPages,
|
||||
clients,
|
||||
clients_30d: clients30d,
|
||||
activity_30d: activity30d,
|
||||
countries,
|
||||
locations,
|
||||
totals: {
|
||||
active_now: active.length,
|
||||
events_24h: totals?.events_24h || 0,
|
||||
visitors_24h: totals?.visitors_24h || 0,
|
||||
page_views_24h: totals?.page_views_24h || 0,
|
||||
events_30d: totals30d?.events_30d || 0,
|
||||
visitors_30d: totals30d?.visitors_30d || 0,
|
||||
sessions_30d: totals30d?.sessions_30d || 0,
|
||||
page_views_30d: totals30d?.page_views_30d || 0,
|
||||
},
|
||||
data_types: [
|
||||
{ key: 'page', label: 'Page activity', detail: 'Page path, title, page views, and heartbeat state' },
|
||||
{ key: 'browser', label: 'Browser and device', detail: 'Browser, operating system, broad device type, and user-agent only when allowed' },
|
||||
{ key: 'display', label: 'Display', detail: 'Screen size, viewport size, pixel ratio, and colour depth when allowed' },
|
||||
{ key: 'locale', label: 'Language and coarse location', detail: 'Browser language and timezone when allowed, plus country code when supplied by the hosting edge' },
|
||||
{ key: 'referrer', label: 'Referrer', detail: 'The referring page when allowed and provided by the browser' },
|
||||
{ key: 'diagnostics', label: 'Diagnostics', detail: 'Privacy signals, network hints, request headers, and browser capability details when allowed' },
|
||||
],
|
||||
privacy: {
|
||||
retention_days: ANALYTICS_RETENTION_DAYS,
|
||||
stores_ip_hashes: false,
|
||||
exposes_raw_ip: false,
|
||||
exposes_precise_location: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user