update viewers page

This commit is contained in:
2026-05-16 07:49:55 +01:00
parent 30cc816aa0
commit b1e3f764a3
2 changed files with 81 additions and 55 deletions
+80 -13
View File
@@ -173,6 +173,10 @@ function ensureAnalyticsDb() {
language text not null default '', language text not null default '',
timezone text not null default '', timezone text not null default '',
country text not null default '', country text not null default '',
region text not null default '',
city text not null default '',
latitude real,
longitude real,
consent text not null default 'analytics', consent text not null default 'analytics',
metadata text not null default '{}' metadata text not null default '{}'
); );
@@ -193,7 +197,11 @@ function ensureAnalyticsDb() {
screen text not null default '', screen text not null default '',
language text not null default '', language text not null default '',
timezone text not null default '', timezone text not null default '',
country text not null default '' country text not null default '',
region text not null default '',
city text not null default '',
latitude real,
longitude real
); );
create index if not exists viewer_events_occurred_at_idx create index if not exists viewer_events_occurred_at_idx
@@ -209,6 +217,14 @@ function ensureAnalyticsDb() {
for (const statement of [ for (const statement of [
`alter table viewer_events add column country text not null default ''`, `alter table viewer_events add column country text not null default ''`,
`alter table active_viewers add column country text not null default ''`, `alter table active_viewers add column country text not null default ''`,
`alter table viewer_events add column region text not null default ''`,
`alter table active_viewers add column region text not null default ''`,
`alter table viewer_events add column city text not null default ''`,
`alter table active_viewers add column city text not null default ''`,
`alter table viewer_events add column latitude real`,
`alter table active_viewers add column latitude real`,
`alter table viewer_events add column longitude real`,
`alter table active_viewers add column longitude real`,
]) { ]) {
try { try {
analyticsDb.exec(statement) analyticsDb.exec(statement)
@@ -417,6 +433,22 @@ function countryFromHeaders(req) {
return country return country
} }
function numberHeader(req, name, min, max) {
const value = Number(headerValue(req, name, 40))
if (!Number.isFinite(value) || value < min || value > max) return null
return value
}
function locationFromHeaders(req) {
return {
country: countryFromHeaders(req),
region: headerValue(req, 'cf-region', 120),
city: headerValue(req, 'cf-ipcity', 120),
latitude: numberHeader(req, 'cf-iplatitude', -90, 90),
longitude: numberHeader(req, 'cf-iplongitude', -180, 180),
}
}
function analyticsMetadata(req, payload) { function analyticsMetadata(req, payload) {
const metadata = payload.metadata && typeof payload.metadata === 'object' ? payload.metadata : {} const metadata = payload.metadata && typeof payload.metadata === 'object' ? payload.metadata : {}
const preferences = metadata.preferences && typeof metadata.preferences === 'object' const preferences = metadata.preferences && typeof metadata.preferences === 'object'
@@ -519,6 +551,7 @@ function recordViewerEvent(req, payload) {
purgeOldAnalytics(db) purgeOldAnalytics(db)
const serverClient = parseClient(req.headers['user-agent'] || '') const serverClient = parseClient(req.headers['user-agent'] || '')
const location = locationFromHeaders(req)
const shareUserAgent = payload.user_agent !== 'Not shared' const shareUserAgent = payload.user_agent !== 'Not shared'
const event = { const event = {
visitor_id: sanitizeText(payload.visitor_id, 80) || crypto.randomUUID(), visitor_id: sanitizeText(payload.visitor_id, 80) || crypto.randomUUID(),
@@ -540,7 +573,11 @@ function recordViewerEvent(req, payload) {
screen: sanitizeText(payload.screen, 40), screen: sanitizeText(payload.screen, 40),
language: sanitizeText(payload.language, 40), language: sanitizeText(payload.language, 40),
timezone: sanitizeText(payload.timezone, 80), timezone: sanitizeText(payload.timezone, 80),
country: countryFromHeaders(req), country: location.country,
region: location.region,
city: location.city,
latitude: location.latitude,
longitude: location.longitude,
consent: payload.consent === 'analytics' ? 'analytics' : '', consent: payload.consent === 'analytics' ? 'analytics' : '',
metadata: JSON.stringify(analyticsMetadata(req, payload)), metadata: JSON.stringify(analyticsMetadata(req, payload)),
} }
@@ -553,19 +590,23 @@ function recordViewerEvent(req, payload) {
db.prepare(` db.prepare(`
insert into viewer_events insert into viewer_events
(occurred_at, visitor_id, session_id, ip_hash, event_type, page_path, page_title, (occurred_at, visitor_id, session_id, ip_hash, event_type, page_path, page_title,
referrer, user_agent, browser, os, device, screen, language, timezone, country, consent, metadata) referrer, user_agent, browser, os, device, screen, language, timezone,
country, region, city, latitude, longitude, consent, metadata)
values values
(@occurred_at, @visitor_id, @session_id, @ip_hash, @event_type, @page_path, @page_title, (@occurred_at, @visitor_id, @session_id, @ip_hash, @event_type, @page_path, @page_title,
@referrer, @user_agent, @browser, @os, @device, @screen, @language, @timezone, @country, @consent, @metadata) @referrer, @user_agent, @browser, @os, @device, @screen, @language, @timezone,
@country, @region, @city, @latitude, @longitude, @consent, @metadata)
`).run({ ...event, occurred_at: now }) `).run({ ...event, occurred_at: now })
db.prepare(` db.prepare(`
insert into active_viewers insert into active_viewers
(session_id, visitor_id, ip_hash, first_seen_at, last_seen_at, page_path, page_title, (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) referrer, user_agent, browser, os, device, screen, language, timezone,
country, region, city, latitude, longitude)
values values
(@session_id, @visitor_id, @ip_hash, @now, @now, @page_path, @page_title, (@session_id, @visitor_id, @ip_hash, @now, @now, @page_path, @page_title,
@referrer, @user_agent, @browser, @os, @device, @screen, @language, @timezone, @country) @referrer, @user_agent, @browser, @os, @device, @screen, @language, @timezone,
@country, @region, @city, @latitude, @longitude)
on conflict(session_id) do update set on conflict(session_id) do update set
last_seen_at = excluded.last_seen_at, last_seen_at = excluded.last_seen_at,
page_path = excluded.page_path, page_path = excluded.page_path,
@@ -578,7 +619,11 @@ function recordViewerEvent(req, payload) {
screen = excluded.screen, screen = excluded.screen,
language = excluded.language, language = excluded.language,
timezone = excluded.timezone, timezone = excluded.timezone,
country = excluded.country country = excluded.country,
region = excluded.region,
city = excluded.city,
latitude = excluded.latitude,
longitude = excluded.longitude
`).run({ ...event, now }) `).run({ ...event, now })
} }
@@ -620,7 +665,8 @@ function viewerDashboard() {
const thirtyDaysSince = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString() const thirtyDaysSince = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
const active = db.prepare(` const active = db.prepare(`
select session_id, visitor_id, first_seen_at, last_seen_at, page_path, page_title, select session_id, visitor_id, first_seen_at, last_seen_at, page_path, page_title,
referrer, browser, os, device, screen, language, timezone, country referrer, browser, os, device, screen, language, timezone,
country, region, city, latitude, longitude
from active_viewers from active_viewers
where last_seen_at >= ? where last_seen_at >= ?
order by last_seen_at desc order by last_seen_at desc
@@ -640,6 +686,10 @@ function viewerDashboard() {
language: row.language, language: row.language,
timezone: row.timezone, timezone: row.timezone,
country: row.country, country: row.country,
region: row.region,
city: row.city,
latitude: row.latitude,
longitude: row.longitude,
})) }))
const activePageMap = new Map() const activePageMap = new Map()
@@ -728,21 +778,38 @@ function viewerDashboard() {
`).all(thirtyDaysSince) `).all(thirtyDaysSince)
const countries = db.prepare(` const countries = db.prepare(`
select country, count(*) as events, count(distinct visitor_id) as visitors select
country,
avg(latitude) as latitude,
avg(longitude) as longitude,
count(*) as events,
count(distinct visitor_id) as visitors
from viewer_events from viewer_events
where occurred_at >= ? where occurred_at >= ?
and country != '' and country != ''
and latitude is not null
and longitude is not null
group by country group by country
order by visitors desc, events desc order by visitors desc, events desc
limit 80 limit 80
`).all(thirtyDaysSince) `).all(thirtyDaysSince)
const locations = db.prepare(` const locations = db.prepare(`
select country, timezone, language, count(*) as events, count(distinct visitor_id) as visitors select
country,
region,
city,
latitude,
longitude,
timezone,
language,
count(*) as events,
count(distinct visitor_id) as visitors
from viewer_events from viewer_events
where occurred_at >= ? where occurred_at >= ?
and (country != '' or (timezone != '' and timezone != 'Not shared')) and latitude is not null
group by country, timezone, language and longitude is not null
group by country, region, city, latitude, longitude, timezone, language
order by visitors desc, events desc order by visitors desc, events desc
limit 32 limit 32
`).all(thirtyDaysSince) `).all(thirtyDaysSince)
@@ -791,7 +858,7 @@ function viewerDashboard() {
{ key: 'page', label: 'Page activity', detail: 'Page path, title, page views, and heartbeat state' }, { 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: '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: '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: 'locale', label: 'Language and coarse location', detail: 'Browser language and timezone when allowed, plus Cloudflare country/city coordinates when supplied by the edge' },
{ key: 'referrer', label: 'Referrer', detail: 'The referring page when allowed and provided by the browser' }, { 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' }, { key: 'diagnostics', label: 'Diagnostics', detail: 'Privacy signals, network hints, request headers, and browser capability details when allowed' },
], ],
+1 -42
View File
@@ -2011,36 +2011,6 @@ const shortDateFormat = new Intl.DateTimeFormat('en-GB', {
month: 'short', month: 'short',
}) })
const timezoneMapPoints = {
'America/Los_Angeles': [34.05, -118.24],
'America/Denver': [39.74, -104.99],
'America/Chicago': [41.88, -87.63],
'America/New_York': [40.71, -74.01],
'America/Toronto': [43.65, -79.38],
'America/Phoenix': [33.45, -112.07],
'America/Sao_Paulo': [-23.55, -46.63],
'America/Mexico_City': [19.43, -99.13],
'Europe/London': [51.51, -0.13],
'Europe/Paris': [48.86, 2.35],
'Europe/Berlin': [52.52, 13.41],
'Europe/Madrid': [40.42, -3.7],
'Europe/Rome': [41.9, 12.5],
'Europe/Amsterdam': [52.37, 4.9],
'Europe/Stockholm': [59.33, 18.07],
'Europe/Warsaw': [52.23, 21.01],
'Europe/Moscow': [55.76, 37.62],
'Africa/Cairo': [30.04, 31.24],
'Africa/Johannesburg': [-26.2, 28.04],
'Asia/Dubai': [25.2, 55.27],
'Asia/Kolkata': [22.57, 88.36],
'Asia/Singapore': [1.35, 103.82],
'Asia/Tokyo': [35.68, 139.65],
'Asia/Seoul': [37.57, 126.98],
'Asia/Shanghai': [31.23, 121.47],
'Australia/Sydney': [-33.87, 151.21],
'Pacific/Auckland': [-36.85, 174.76],
}
const countryNames = { const countryNames = {
AR: 'Argentina', AR: 'Argentina',
AU: 'Australia', AU: 'Australia',
@@ -2093,17 +2063,6 @@ const countryMapPoints = {
ZA: [24, -29], ZA: [24, -29],
} }
function timezonePoint(timezone = '') {
if (timezoneMapPoints[timezone]) return timezoneMapPoints[timezone]
if (timezone.startsWith('America/')) return [39, -96]
if (timezone.startsWith('Europe/')) return [50, 10]
if (timezone.startsWith('Africa/')) return [0, 20]
if (timezone.startsWith('Asia/')) return [34, 100]
if (timezone.startsWith('Australia/')) return [-25, 134]
if (timezone.startsWith('Pacific/')) return [-15, 170]
return [20, 0]
}
function filledLast30Days(rows) { function filledLast30Days(rows) {
const byDate = new Map(rows.map((row) => [row.date, row])) const byDate = new Map(rows.map((row) => [row.date, row]))
return Array.from({ length: 30 }, (_, index) => { return Array.from({ length: 30 }, (_, index) => {
@@ -2432,7 +2391,7 @@ function ViewersPage({ viewers }) {
<div className="border-b border-surface px-5 py-4"> <div className="border-b border-surface px-5 py-4">
<h2 className="text-lg font-semibold">Location signals</h2> <h2 className="text-lg font-semibold">Location signals</h2>
<p className="mt-1 text-sm text-text-soft"> <p className="mt-1 text-sm text-text-soft">
Country traffic when available, with timezone fallback from the last 30 days Cloudflare edge geolocation from the last 30 days
</p> </p>
</div> </div>
<LocationSignalMap countries={countries} locations={locations} /> <LocationSignalMap countries={countries} locations={locations} />