update viewers page
This commit is contained in:
+80
-13
@@ -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
@@ -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} />
|
||||||
|
|||||||
Reference in New Issue
Block a user