anky2002 commited on
Commit
9d380cf
·
verified ·
1 Parent(s): 9f93ce6

feat: add Web Vitals reporting for Core Web Vitals monitoring

Browse files
src/components/analytics/web-vitals.tsx ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useEffect } from "react"
4
+
5
+ /**
6
+ * Reports Core Web Vitals (LCP, FID, CLS, FCP, TTFB) to console in development
7
+ * and can be extended to send to analytics in production.
8
+ *
9
+ * Uses the web-vitals library pattern via PerformanceObserver.
10
+ */
11
+ export function WebVitals() {
12
+ useEffect(() => {
13
+ if (typeof window === "undefined") return
14
+
15
+ // Only report in development or if analytics endpoint is configured
16
+ const shouldReport = process.env.NODE_ENV === "development" || !!process.env.NEXT_PUBLIC_ANALYTICS_URL
17
+
18
+ if (!shouldReport) return
19
+
20
+ // Use Performance Observer for CLS, LCP, FID
21
+ try {
22
+ // Largest Contentful Paint
23
+ const lcpObserver = new PerformanceObserver((list) => {
24
+ const entries = list.getEntries()
25
+ const lastEntry = entries[entries.length - 1]
26
+ if (lastEntry) {
27
+ reportMetric("LCP", lastEntry.startTime)
28
+ }
29
+ })
30
+ lcpObserver.observe({ type: "largest-contentful-paint", buffered: true })
31
+
32
+ // First Input Delay
33
+ const fidObserver = new PerformanceObserver((list) => {
34
+ const entries = list.getEntries()
35
+ entries.forEach((entry) => {
36
+ const fidEntry = entry as PerformanceEventTiming
37
+ reportMetric("FID", fidEntry.processingStart - fidEntry.startTime)
38
+ })
39
+ })
40
+ fidObserver.observe({ type: "first-input", buffered: true })
41
+
42
+ // Cumulative Layout Shift
43
+ let clsValue = 0
44
+ const clsObserver = new PerformanceObserver((list) => {
45
+ const entries = list.getEntries()
46
+ entries.forEach((entry) => {
47
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
+ if (!(entry as any).hadRecentInput) {
49
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
+ clsValue += (entry as any).value
51
+ }
52
+ })
53
+ reportMetric("CLS", clsValue)
54
+ })
55
+ clsObserver.observe({ type: "layout-shift", buffered: true })
56
+
57
+ return () => {
58
+ lcpObserver.disconnect()
59
+ fidObserver.disconnect()
60
+ clsObserver.disconnect()
61
+ }
62
+ } catch {
63
+ // PerformanceObserver not supported
64
+ }
65
+ }, [])
66
+
67
+ return null
68
+ }
69
+
70
+ function reportMetric(name: string, value: number) {
71
+ if (process.env.NODE_ENV === "development") {
72
+ const color = getMetricColor(name, value)
73
+ console.log(
74
+ `%c[Web Vital] ${name}: ${value.toFixed(2)}ms`,
75
+ `color: ${color}; font-weight: bold;`
76
+ )
77
+ }
78
+
79
+ // Send to analytics endpoint if configured
80
+ const analyticsUrl = process.env.NEXT_PUBLIC_ANALYTICS_URL
81
+ if (analyticsUrl) {
82
+ const body = JSON.stringify({ name, value, page: window.location.pathname })
83
+ // Use sendBeacon for reliability during page unload
84
+ if (navigator.sendBeacon) {
85
+ navigator.sendBeacon(analyticsUrl, body)
86
+ } else {
87
+ fetch(analyticsUrl, { method: "POST", body, keepalive: true }).catch(() => {})
88
+ }
89
+ }
90
+ }
91
+
92
+ function getMetricColor(name: string, value: number): string {
93
+ switch (name) {
94
+ case "LCP":
95
+ return value <= 2500 ? "green" : value <= 4000 ? "orange" : "red"
96
+ case "FID":
97
+ return value <= 100 ? "green" : value <= 300 ? "orange" : "red"
98
+ case "CLS":
99
+ return value <= 0.1 ? "green" : value <= 0.25 ? "orange" : "red"
100
+ default:
101
+ return "blue"
102
+ }
103
+ }