From ec227d614f595223c9d1060097e1fb4032a951d0 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sat, 28 Feb 2026 19:35:06 +0100 Subject: [PATCH] feat: implement Umami page speed tracking via Web Vitals - Add WebVitalsTracker component using useReportWebVitals - Report LCP, CLS, FID, FCP, TTFB, and INP as Umami events - Include rating (good/needs-improvement/poor) for meaningful metrics --- components/analytics/AnalyticsShell.tsx | 4 ++ components/analytics/WebVitalsTracker.tsx | 54 +++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 components/analytics/WebVitalsTracker.tsx diff --git a/components/analytics/AnalyticsShell.tsx b/components/analytics/AnalyticsShell.tsx index cd441c6c..696cc881 100644 --- a/components/analytics/AnalyticsShell.tsx +++ b/components/analytics/AnalyticsShell.tsx @@ -9,6 +9,9 @@ const DynamicAnalyticsProvider = dynamic(() => import('./AnalyticsProvider'), { const DynamicScrollDepthTracker = dynamic(() => import('./ScrollDepthTracker'), { ssr: false, }); +const DynamicWebVitalsTracker = dynamic(() => import('./WebVitalsTracker'), { + ssr: false, +}); export default function AnalyticsShell() { const [shouldLoad, setShouldLoad] = useState(false); @@ -34,6 +37,7 @@ export default function AnalyticsShell() { + ); } diff --git a/components/analytics/WebVitalsTracker.tsx b/components/analytics/WebVitalsTracker.tsx new file mode 100644 index 00000000..3761a8d2 --- /dev/null +++ b/components/analytics/WebVitalsTracker.tsx @@ -0,0 +1,54 @@ +'use client'; + +import { useReportWebVitals } from 'next/web-vitals'; +import { useAnalytics } from './useAnalytics'; + +/** + * WebVitalsTracker component. + * + * Captures Next.js Web Vitals and reports them to Umami as custom events. + * This provides "meaningful" page speed tracking by measuring real user + * experiences (LCP, CLS, INP, etc.). + */ +export default function WebVitalsTracker() { + const { trackEvent } = useAnalytics(); + + useReportWebVitals((metric) => { + const { name, value, id, label } = metric; + + // Determine rating (simplified version of web-vitals standards) + let rating: 'good' | 'needs-improvement' | 'poor' = 'good'; + + if (name === 'LCP') { + if (value > 4000) rating = 'poor'; + else if (value > 2500) rating = 'needs-improvement'; + } else if (name === 'CLS') { + if (value > 0.25) rating = 'poor'; + else if (value > 0.1) rating = 'needs-improvement'; + } else if (name === 'FID') { + if (value > 300) rating = 'poor'; + else if (value > 100) rating = 'needs-improvement'; + } else if (name === 'FCP') { + if (value > 3000) rating = 'poor'; + else if (value > 1800) rating = 'needs-improvement'; + } else if (name === 'TTFB') { + if (value > 1500) rating = 'poor'; + else if (value > 800) rating = 'needs-improvement'; + } else if (name === 'INP') { + if (value > 500) rating = 'poor'; + else if (value > 200) rating = 'needs-improvement'; + } + + // Report to Umami + trackEvent('web-vital', { + metric: name, + value: Math.round(name === 'CLS' ? value * 1000 : value), // CLS is a score, multiply by 1000 to keep as integer if preferred + rating, + id, + label, + path: typeof window !== 'undefined' ? window.location.pathname : undefined, + }); + }); + + return null; +}