|
1 | 1 | import { type Metric, onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals"; |
2 | 2 | import type { BaseTracker } from "../core/tracker"; |
3 | | -import { generateUUIDv4, logger } from "../core/utils"; |
| 3 | +import { logger } from "../core/utils"; |
| 4 | + |
| 5 | +type WebVitalMetricName = "FCP" | "LCP" | "CLS" | "INP" | "TTFB"; |
| 6 | + |
| 7 | +type WebVitalSpan = { |
| 8 | + sessionId: string; |
| 9 | + timestamp: number; |
| 10 | + path: string; |
| 11 | + metricName: WebVitalMetricName; |
| 12 | + metricValue: number; |
| 13 | +}; |
4 | 14 |
|
5 | 15 | export function initWebVitalsTracking(tracker: BaseTracker) { |
6 | 16 | if (tracker.isServer()) { |
7 | 17 | return; |
8 | 18 | } |
9 | 19 |
|
10 | | - const metrics: { |
11 | | - fcp: number | undefined; |
12 | | - lcp: number | undefined; |
13 | | - cls: number | undefined; |
14 | | - inp: number | undefined; |
15 | | - ttfb: number | undefined; |
16 | | - } = { |
17 | | - fcp: undefined, |
18 | | - lcp: undefined, |
19 | | - cls: undefined, |
20 | | - inp: undefined, |
21 | | - ttfb: undefined, |
22 | | - }; |
| 20 | + const sentMetrics = new Set<WebVitalMetricName>(); |
23 | 21 |
|
24 | | - const sendVitals = () => { |
25 | | - if (!Object.values(metrics).some((m) => m !== undefined)) { |
| 22 | + const sendVitalSpan = (metricName: WebVitalMetricName, metricValue: number) => { |
| 23 | + if (sentMetrics.has(metricName)) { |
26 | 24 | return; |
27 | 25 | } |
| 26 | + sentMetrics.add(metricName); |
28 | 27 |
|
29 | | - const clamp = (v: number | undefined) => |
30 | | - typeof v === "number" ? Math.min(60_000, Math.max(0, v)) : v; |
31 | | - |
32 | | - const payload = { |
33 | | - eventId: generateUUIDv4(), |
34 | | - anonymousId: tracker.anonymousId, |
35 | | - sessionId: tracker.sessionId, |
| 28 | + const span: WebVitalSpan = { |
| 29 | + sessionId: tracker.sessionId ?? "", |
36 | 30 | timestamp: Date.now(), |
37 | | - fcp: clamp(metrics.fcp), |
38 | | - lcp: clamp(metrics.lcp), |
39 | | - cls: clamp(metrics.cls), |
40 | | - inp: metrics.inp, |
41 | | - ttfb: clamp(metrics.ttfb), |
42 | | - url: window.location.href, |
| 31 | + path: window.location.pathname, |
| 32 | + metricName, |
| 33 | + metricValue, |
43 | 34 | }; |
44 | 35 |
|
45 | | - logger.log("Sending web vitals", payload); |
46 | | - |
47 | | - tracker.sendBeacon(payload); |
| 36 | + logger.log(`Sending web vital span: ${metricName}`, span); |
| 37 | + tracker.sendBeacon(span); |
48 | 38 | }; |
49 | 39 |
|
50 | 40 | const handleMetric = (metric: Metric) => { |
51 | | - switch (metric.name) { |
52 | | - case "FCP": |
53 | | - metrics.fcp = Math.round(metric.value); |
54 | | - break; |
55 | | - case "LCP": |
56 | | - metrics.lcp = Math.round(metric.value); |
57 | | - break; |
58 | | - case "CLS": |
59 | | - metrics.cls = metric.value; // CLS is a score, not ms, so keep decimals if needed, but usually small |
60 | | - break; |
61 | | - case "INP": |
62 | | - metrics.inp = Math.round(metric.value); |
63 | | - break; |
64 | | - case "TTFB": |
65 | | - metrics.ttfb = Math.round(metric.value); |
66 | | - break; |
67 | | - default: |
68 | | - break; |
69 | | - } |
70 | | - logger.log(`Web Vitals Metric: ${metric.name}`, metric.value); |
| 41 | + const name = metric.name as WebVitalMetricName; |
| 42 | + const value = name === "CLS" ? metric.value : Math.round(metric.value); |
| 43 | + |
| 44 | + logger.log(`Web Vital captured: ${name}`, value); |
| 45 | + sendVitalSpan(name, value); |
71 | 46 | }; |
72 | 47 |
|
73 | 48 | onFCP(handleMetric); |
74 | 49 | onLCP(handleMetric); |
75 | 50 | onCLS(handleMetric); |
76 | 51 | onINP(handleMetric); |
77 | 52 | onTTFB(handleMetric); |
78 | | - |
79 | | - setTimeout(() => { |
80 | | - sendVitals(); |
81 | | - }, 4000); |
82 | | - |
83 | | - const report = () => { |
84 | | - sendVitals(); |
85 | | - }; |
86 | | - |
87 | | - let reportTimeout: number | undefined; |
88 | | - const debouncedReport = (immediate = false) => { |
89 | | - if (reportTimeout) { |
90 | | - window.clearTimeout(reportTimeout); |
91 | | - } |
92 | | - if (immediate) { |
93 | | - report(); |
94 | | - } else { |
95 | | - reportTimeout = window.setTimeout(report, 1000); |
96 | | - } |
97 | | - }; |
98 | | - |
99 | | - document.addEventListener("visibilitychange", () => { |
100 | | - if (document.visibilityState === "hidden") { |
101 | | - debouncedReport(true); |
102 | | - } |
103 | | - }); |
104 | | - |
105 | | - window.addEventListener("pagehide", () => debouncedReport(true)); |
106 | 53 | } |
0 commit comments