update viewers page
This commit is contained in:
+51
-1
@@ -758,8 +758,18 @@ function viewerDashboard() {
|
|||||||
limit 12
|
limit 12
|
||||||
`).all(daySince)
|
`).all(daySince)
|
||||||
|
|
||||||
|
const topPages30d = db.prepare(`
|
||||||
|
select page_path, page_title, count(*) as views
|
||||||
|
from viewer_events
|
||||||
|
where event_type = 'page_view'
|
||||||
|
and occurred_at >= ?
|
||||||
|
group by page_path, page_title
|
||||||
|
order by views desc, page_path asc
|
||||||
|
limit 12
|
||||||
|
`).all(thirtyDaysSince)
|
||||||
|
|
||||||
const clients = db.prepare(`
|
const clients = db.prepare(`
|
||||||
select browser, os, device, count(*) as events
|
select browser, os, device, count(*) as events, count(distinct visitor_id) as visitors
|
||||||
from viewer_events
|
from viewer_events
|
||||||
where occurred_at >= ?
|
where occurred_at >= ?
|
||||||
group by browser, os, device
|
group by browser, os, device
|
||||||
@@ -932,6 +942,23 @@ function viewerDashboard() {
|
|||||||
location_labels: locationsByDate.get(row.date) || [],
|
location_labels: locationsByDate.get(row.date) || [],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const countries24h = db.prepare(`
|
||||||
|
select
|
||||||
|
country,
|
||||||
|
avg(latitude) as latitude,
|
||||||
|
avg(longitude) as longitude,
|
||||||
|
count(*) as events,
|
||||||
|
count(distinct visitor_id) as visitors
|
||||||
|
from viewer_events
|
||||||
|
where occurred_at >= ?
|
||||||
|
and country != ''
|
||||||
|
and latitude is not null
|
||||||
|
and longitude is not null
|
||||||
|
group by country
|
||||||
|
order by visitors desc, events desc
|
||||||
|
limit 80
|
||||||
|
`).all(daySince)
|
||||||
|
|
||||||
const countries = db.prepare(`
|
const countries = db.prepare(`
|
||||||
select
|
select
|
||||||
country,
|
country,
|
||||||
@@ -949,6 +976,26 @@ function viewerDashboard() {
|
|||||||
limit 80
|
limit 80
|
||||||
`).all(thirtyDaysSince)
|
`).all(thirtyDaysSince)
|
||||||
|
|
||||||
|
const locations24h = db.prepare(`
|
||||||
|
select
|
||||||
|
country,
|
||||||
|
region,
|
||||||
|
city,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
timezone,
|
||||||
|
language,
|
||||||
|
count(*) as events,
|
||||||
|
count(distinct visitor_id) as visitors
|
||||||
|
from viewer_events
|
||||||
|
where occurred_at >= ?
|
||||||
|
and latitude is not null
|
||||||
|
and longitude is not null
|
||||||
|
group by country, region, city, latitude, longitude, timezone, language
|
||||||
|
order by visitors desc, events desc
|
||||||
|
limit 32
|
||||||
|
`).all(daySince)
|
||||||
|
|
||||||
const locations = db.prepare(`
|
const locations = db.prepare(`
|
||||||
select
|
select
|
||||||
country,
|
country,
|
||||||
@@ -995,11 +1042,14 @@ function viewerDashboard() {
|
|||||||
active,
|
active,
|
||||||
active_pages: activePages,
|
active_pages: activePages,
|
||||||
top_pages: topPages,
|
top_pages: topPages,
|
||||||
|
top_pages_30d: topPages30d,
|
||||||
clients,
|
clients,
|
||||||
clients_30d: clients30d,
|
clients_30d: clients30d,
|
||||||
activity_24h: activity24hWithLabels,
|
activity_24h: activity24hWithLabels,
|
||||||
activity_30d: activityWithLocations,
|
activity_30d: activityWithLocations,
|
||||||
|
countries_24h: countries24h,
|
||||||
countries,
|
countries,
|
||||||
|
locations_24h: locations24h,
|
||||||
locations,
|
locations,
|
||||||
totals: {
|
totals: {
|
||||||
active_now: active.length,
|
active_now: active.length,
|
||||||
|
|||||||
+76
-64
@@ -2259,19 +2259,54 @@ function AnalyticsPeriodSection({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ViewersPage({ viewers }) {
|
function ViewersPage({ viewers }) {
|
||||||
|
const [analyticsWindow, setAnalyticsWindow] = useState('24h')
|
||||||
const data = viewers.data || {}
|
const data = viewers.data || {}
|
||||||
const active = data.active || []
|
const active = data.active || []
|
||||||
const activePages = data.active_pages || []
|
const activePages = data.active_pages || []
|
||||||
const topPages = data.top_pages || []
|
|
||||||
const clients = data.clients || []
|
|
||||||
const clients30d = data.clients_30d || []
|
|
||||||
const activity24h = filledLast24Hours(data.activity_24h || [])
|
const activity24h = filledLast24Hours(data.activity_24h || [])
|
||||||
const activity30d = filledLast30Days(data.activity_30d || [])
|
const activity30d = filledLast30Days(data.activity_30d || [])
|
||||||
const countries = data.countries || []
|
|
||||||
const locations = data.locations || []
|
|
||||||
const dataTypes = data.data_types || []
|
const dataTypes = data.data_types || []
|
||||||
const totals = data.totals || {}
|
const totals = data.totals || {}
|
||||||
const generatedAt = data.generated_at ? dateFormat.format(new Date(data.generated_at)) : 'Waiting for data'
|
const generatedAt = data.generated_at ? dateFormat.format(new Date(data.generated_at)) : 'Waiting for data'
|
||||||
|
const periods = {
|
||||||
|
'24h': {
|
||||||
|
label: '24hr',
|
||||||
|
title: 'Last 24 hours',
|
||||||
|
description: 'Consented analytics grouped by hour',
|
||||||
|
bucketLabel: 'Hourly count',
|
||||||
|
pointFormat: shortTimeFormat,
|
||||||
|
activity: activity24h,
|
||||||
|
topPages: data.top_pages || [],
|
||||||
|
clients: data.clients || [],
|
||||||
|
countries: data.countries_24h || [],
|
||||||
|
locations: data.locations_24h || [],
|
||||||
|
totals: {
|
||||||
|
events: totals.events_24h,
|
||||||
|
visitors: totals.visitors_24h,
|
||||||
|
sessions: totals.sessions_24h,
|
||||||
|
pageViews: totals.page_views_24h,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'30d': {
|
||||||
|
label: '30d',
|
||||||
|
title: 'Last 30 days',
|
||||||
|
description: 'Retained consented analytics over the current privacy window',
|
||||||
|
bucketLabel: 'Daily count',
|
||||||
|
pointFormat: shortDateFormat,
|
||||||
|
activity: activity30d,
|
||||||
|
topPages: data.top_pages_30d || data.top_pages || [],
|
||||||
|
clients: data.clients_30d || [],
|
||||||
|
countries: data.countries || [],
|
||||||
|
locations: data.locations || [],
|
||||||
|
totals: {
|
||||||
|
events: totals.events_30d,
|
||||||
|
visitors: totals.visitors_30d,
|
||||||
|
sessions: totals.sessions_30d,
|
||||||
|
pageViews: totals.page_views_30d,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const periodData = periods[analyticsWindow]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 pt-10 sm:pt-14">
|
<section className="space-y-6 pt-10 sm:pt-14">
|
||||||
@@ -2286,45 +2321,42 @@ function ViewersPage({ viewers }) {
|
|||||||
Live consented browser sessions and page activity. Last refreshed {generatedAt}.
|
Live consented browser sessions and page activity. Last refreshed {generatedAt}.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<div className="flex rounded-md border border-border bg-surface p-1 text-sm font-semibold" aria-label="Analytics time window">
|
||||||
|
{Object.entries(periods).map(([key, period]) => (
|
||||||
|
<button
|
||||||
|
className={`rounded px-3 py-1.5 transition ${
|
||||||
|
analyticsWindow === key ? 'bg-fury-red text-bg shadow-sm' : 'text-text-soft hover:text-text'
|
||||||
|
}`}
|
||||||
|
key={key}
|
||||||
|
onClick={() => setAnalyticsWindow(key)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{period.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
<span className="w-fit rounded-md bg-surface px-3 py-2 text-sm font-semibold text-fury-cyan">
|
<span className="w-fit rounded-md bg-surface px-3 py-2 text-sm font-semibold text-fury-cyan">
|
||||||
{viewers.status === 'loading' ? 'Loading' : `${formatNumber(totals.active_now)} active now`}
|
{viewers.status === 'loading' ? 'Loading' : `${formatNumber(totals.active_now)} active now`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
<Stat label="Active now" value={formatNumber(totals.active_now)} />
|
<Stat label="Active now" value={formatNumber(totals.active_now)} />
|
||||||
<Stat label="Visitors 24h" value={formatNumber(totals.visitors_24h)} />
|
<Stat label={`Visitors ${periodData.label}`} value={formatNumber(periodData.totals.visitors)} />
|
||||||
<Stat label="Page views 24h" value={formatNumber(totals.page_views_24h)} />
|
<Stat label={`Page views ${periodData.label}`} value={formatNumber(periodData.totals.pageViews)} />
|
||||||
<Stat label="Events 24h" value={formatNumber(totals.events_24h)} />
|
<Stat label={`Events ${periodData.label}`} value={formatNumber(periodData.totals.events)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AnalyticsPeriodSection
|
<AnalyticsPeriodSection
|
||||||
activity={activity24h}
|
activity={periodData.activity}
|
||||||
bucketLabel="Hourly count"
|
bucketLabel={periodData.bucketLabel}
|
||||||
description="Consented analytics grouped by hour"
|
description={periodData.description}
|
||||||
pointFormat={shortTimeFormat}
|
pointFormat={periodData.pointFormat}
|
||||||
title="Last 24 hours"
|
title={periodData.title}
|
||||||
totals={{
|
totals={periodData.totals}
|
||||||
events: totals.events_24h,
|
|
||||||
visitors: totals.visitors_24h,
|
|
||||||
sessions: totals.sessions_24h,
|
|
||||||
pageViews: totals.page_views_24h,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AnalyticsPeriodSection
|
|
||||||
activity={activity30d}
|
|
||||||
bucketLabel="Daily count"
|
|
||||||
description="Retained consented analytics over the current privacy window"
|
|
||||||
pointFormat={shortDateFormat}
|
|
||||||
title="Last 30 days"
|
|
||||||
totals={{
|
|
||||||
events: totals.events_30d,
|
|
||||||
visitors: totals.visitors_30d,
|
|
||||||
sessions: totals.sessions_30d,
|
|
||||||
pageViews: totals.page_views_30d,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="overflow-hidden rounded-lg border border-border bg-fury-white shadow-sm">
|
<div className="overflow-hidden rounded-lg border border-border bg-fury-white shadow-sm">
|
||||||
@@ -2418,9 +2450,9 @@ function ViewersPage({ viewers }) {
|
|||||||
<div className="rounded-lg border border-border bg-fury-white shadow-sm">
|
<div className="rounded-lg border border-border bg-fury-white shadow-sm">
|
||||||
<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">Top pages</h2>
|
<h2 className="text-lg font-semibold">Top pages</h2>
|
||||||
<p className="mt-1 text-sm text-text-soft">Page views over the last 24 hours</p>
|
<p className="mt-1 text-sm text-text-soft">Page views over {periodData.title.toLowerCase()}</p>
|
||||||
</div>
|
</div>
|
||||||
{topPages.map((page) => (
|
{periodData.topPages.map((page) => (
|
||||||
<div className="grid grid-cols-[1fr_auto] gap-3 border-b border-surface px-5 py-3 text-sm" key={page.page_path}>
|
<div className="grid grid-cols-[1fr_auto] gap-3 border-b border-surface px-5 py-3 text-sm" key={page.page_path}>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate font-semibold">{page.page_title || page.page_path}</p>
|
<p className="truncate font-semibold">{page.page_title || page.page_path}</p>
|
||||||
@@ -2429,27 +2461,28 @@ function ViewersPage({ viewers }) {
|
|||||||
<p className="font-semibold text-fury-cyan">{formatNumber(page.views)}</p>
|
<p className="font-semibold text-fury-cyan">{formatNumber(page.views)}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{!topPages.length ? <p className="px-5 py-10 text-sm text-text-soft">No page views recorded yet</p> : null}
|
{!periodData.topPages.length ? <p className="px-5 py-10 text-sm text-text-soft">No page views recorded yet</p> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-lg border border-border bg-fury-white shadow-sm">
|
<div className="rounded-lg border border-border bg-fury-white shadow-sm">
|
||||||
<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">Clients</h2>
|
<h2 className="text-lg font-semibold">Clients</h2>
|
||||||
<p className="mt-1 text-sm text-text-soft">Browsers, devices, and operating systems seen in 24 hours</p>
|
<p className="mt-1 text-sm text-text-soft">Browsers, devices, and operating systems seen over {periodData.title.toLowerCase()}</p>
|
||||||
</div>
|
</div>
|
||||||
{clients.map((client) => (
|
{periodData.clients.map((client) => (
|
||||||
<div
|
<div
|
||||||
className="grid grid-cols-[1fr_auto] gap-3 border-b border-surface px-5 py-3 text-sm"
|
className="grid grid-cols-[1fr_auto_auto] gap-3 border-b border-surface px-5 py-3 text-sm"
|
||||||
key={`${client.browser}-${client.os}-${client.device}`}
|
key={`${client.browser}-${client.os}-${client.device}`}
|
||||||
>
|
>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate font-semibold">{client.browser} on {client.os}</p>
|
<p className="truncate font-semibold">{client.browser} on {client.os}</p>
|
||||||
<p className="truncate text-xs text-text-soft">{client.device}</p>
|
<p className="truncate text-xs text-text-soft">{client.device}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-semibold text-fury-cyan">{formatNumber(client.events)}</p>
|
<p className="text-text-soft">{formatNumber(client.visitors || 0)} visitors</p>
|
||||||
|
<p className="font-semibold text-fury-cyan">{formatNumber(client.events)} events</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{!clients.length ? <p className="px-5 py-10 text-sm text-text-soft">No client data recorded yet</p> : null}
|
{!periodData.clients.length ? <p className="px-5 py-10 text-sm text-text-soft">No client data recorded yet</p> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2474,34 +2507,13 @@ 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">
|
||||||
Cloudflare edge geolocation from the last 30 days
|
Cloudflare edge geolocation from {periodData.title.toLowerCase()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<LocationSignalMap countries={countries} locations={locations} />
|
<LocationSignalMap countries={periodData.countries} locations={periodData.locations} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-lg border border-border bg-fury-white shadow-sm">
|
|
||||||
<div className="border-b border-surface px-5 py-4">
|
|
||||||
<h2 className="text-lg font-semibold">Clients over 30 days</h2>
|
|
||||||
<p className="mt-1 text-sm text-text-soft">Browsers, devices, and operating systems across retained data</p>
|
|
||||||
</div>
|
|
||||||
{clients30d.map((client) => (
|
|
||||||
<div
|
|
||||||
className="grid grid-cols-[1fr_auto_auto] gap-3 border-b border-surface px-5 py-3 text-sm"
|
|
||||||
key={`${client.browser}-${client.os}-${client.device}-30d`}
|
|
||||||
>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p className="truncate font-semibold">{client.browser} on {client.os}</p>
|
|
||||||
<p className="truncate text-xs text-text-soft">{client.device}</p>
|
|
||||||
</div>
|
|
||||||
<p className="text-text-soft">{formatNumber(client.visitors)} visitors</p>
|
|
||||||
<p className="font-semibold text-fury-cyan">{formatNumber(client.events)} events</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{!clients30d.length ? <p className="px-5 py-10 text-sm text-text-soft">No 30-day client data recorded yet</p> : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-xs text-text-soft">
|
<p className="text-xs text-text-soft">
|
||||||
Analytics are opt-in, retained for {formatNumber(data.privacy?.retention_days || 30)} days,
|
Analytics are opt-in, retained for {formatNumber(data.privacy?.retention_days || 30)} days,
|
||||||
and public output excludes raw IP addresses and precise location.
|
and public output excludes raw IP addresses and precise location.
|
||||||
|
|||||||
Reference in New Issue
Block a user