From ba1e5c1885176642779d5a6447cb23fce2e03bc0 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sun, 18 Jan 2026 17:52:53 +0100 Subject: [PATCH] feat: add web vitals tracking script and performance dashboard UI --- app/sites/[id]/page.tsx | 10 ++++ components/dashboard/PerformanceStats.tsx | 70 +++++++++++++++++++++++ lib/api/stats.ts | 7 +++ 3 files changed, 87 insertions(+) create mode 100644 components/dashboard/PerformanceStats.tsx diff --git a/app/sites/[id]/page.tsx b/app/sites/[id]/page.tsx index d0e4206..0e0e308 100644 --- a/app/sites/[id]/page.tsx +++ b/app/sites/[id]/page.tsx @@ -14,6 +14,7 @@ import TopReferrers from '@/components/dashboard/TopReferrers' import Locations from '@/components/dashboard/Locations' import TechSpecs from '@/components/dashboard/TechSpecs' import Chart from '@/components/dashboard/Chart' +import PerformanceStats from '@/components/dashboard/PerformanceStats' export default function SiteDashboardPage() { const params = useParams() @@ -38,6 +39,7 @@ export default function SiteDashboardPage() { const [os, setOS] = useState([]) const [devices, setDevices] = useState([]) const [screenResolutions, setScreenResolutions] = useState([]) + const [performance, setPerformance] = useState<{ lcp: number, cls: number, inp: number }>({ lcp: 0, cls: 0, inp: 0 }) const [dateRange, setDateRange] = useState(getDateRange(30)) const [isDatePickerOpen, setIsDatePickerOpen] = useState(false) @@ -109,6 +111,7 @@ export default function SiteDashboardPage() { setOS(Array.isArray(data.os) ? data.os : []) setDevices(Array.isArray(data.devices) ? data.devices : []) setScreenResolutions(Array.isArray(data.screen_resolutions) ? data.screen_resolutions : []) + setPerformance(data.performance || { lcp: 0, cls: 0, inp: 0 }) } catch (error: any) { toast.error('Failed to load data: ' + (error.message || 'Unknown error')) } finally { @@ -213,6 +216,13 @@ export default function SiteDashboardPage() { /> + {/* Performance Stats */} + {performance.lcp > 0 && ( +
+ +
+ )} +
+
{label}
+
+ {value} + {unit} +
+
+ ) +} + +export default function PerformanceStats({ stats }: Props) { + // * Scoring Logic (based on Google Web Vitals) + const getScore = (metric: 'lcp' | 'cls' | 'inp', value: number) => { + if (metric === 'lcp') return value <= 2500 ? 'good' : value <= 4000 ? 'needs-improvement' : 'poor' + if (metric === 'cls') return value <= 0.1 ? 'good' : value <= 0.25 ? 'needs-improvement' : 'poor' + if (metric === 'inp') return value <= 200 ? 'good' : value <= 500 ? 'needs-improvement' : 'poor' + return 'good' + } + + // * If no data, don't show or show placeholder? + // * Showing placeholder with 0s might be confusing if they actually have 0ms latency (impossible) + // * But we handle empty stats in parent or pass 0 here. + + return ( +
+

+ Core Web Vitals +

+
+ + + +
+
+ * Averages calculated from real user sessions. Lower is better. +
+
+ ) +} diff --git a/lib/api/stats.ts b/lib/api/stats.ts index 555455c..a4ea5d3 100644 --- a/lib/api/stats.ts +++ b/lib/api/stats.ts @@ -19,6 +19,12 @@ export interface ScreenResolutionStat { pageviews: number } +export interface PerformanceStats { + lcp: number + cls: number + inp: number +} + export interface TopReferrer { referrer: string pageviews: number @@ -190,6 +196,7 @@ export interface DashboardData { os: OSStat[] devices: DeviceStat[] screen_resolutions: ScreenResolutionStat[] + performance?: PerformanceStats } export async function getDashboard(siteId: string, startDate?: string, endDate?: string, limit = 10, interval?: string): Promise {