80 lines
2.5 KiB
TypeScript
80 lines
2.5 KiB
TypeScript
/**
|
|
* Real-User-Monitoring (RUM) bridge for Core Web Vitals → Sentry.
|
|
*
|
|
* The browser's web-vitals library reports CLS / FID / LCP / INP / TTFB
|
|
* once per page load (or per change for CLS). We forward those values to
|
|
* Sentry via `captureMessage` with a structured tag-set so the metrics
|
|
* surface as searchable events. The dashboard query is documented in
|
|
* docs/architecture/lighthouse.md.
|
|
*
|
|
* Spec: docs/improvement-plan/10-compliance-polish/lighthouse.md (Step (f)).
|
|
*/
|
|
import type { Metric } from "web-vitals";
|
|
|
|
/**
|
|
* Reporter signature is exposed for tests + the runtime injection. The
|
|
* default reporter forwards to Sentry; tests pass a vi.fn() spy.
|
|
*/
|
|
export type WebVitalsReporter = (metric: Metric) => void;
|
|
|
|
/**
|
|
* Sends a web-vitals Metric to the Sentry sink. We keep the integration
|
|
* lazy and tolerant — when Sentry isn't initialised the call is a no-op.
|
|
*/
|
|
export const sentryReporter: WebVitalsReporter = (metric) => {
|
|
void (async () => {
|
|
try {
|
|
const Sentry = await import("@sentry/react-router");
|
|
Sentry.captureMessage(`web-vitals.${metric.name}`, {
|
|
level: "info",
|
|
tags: {
|
|
"web.vital.name": metric.name,
|
|
"web.vital.rating": metric.rating,
|
|
},
|
|
extra: {
|
|
value: metric.value,
|
|
delta: metric.delta,
|
|
id: metric.id,
|
|
navigationType: metric.navigationType,
|
|
},
|
|
});
|
|
} catch {
|
|
// Sentry not loaded or import failed — silent drop.
|
|
}
|
|
})();
|
|
};
|
|
|
|
export interface ReportWebVitalsOptions {
|
|
reporter?: WebVitalsReporter;
|
|
/**
|
|
* Allows injecting the web-vitals module (e.g. a mock) in tests.
|
|
* Production callers should let this default to dynamic import.
|
|
*/
|
|
loader?: () => Promise<typeof import("web-vitals")>;
|
|
}
|
|
|
|
/**
|
|
* Wires the web-vitals library callbacks. Safe to call on SSR (it bails
|
|
* out when `window` is absent). Returns a promise that resolves once the
|
|
* underlying library has been registered, useful for tests.
|
|
*/
|
|
export async function reportWebVitals(options: ReportWebVitalsOptions = {}): Promise<void> {
|
|
if (typeof window === "undefined") return;
|
|
|
|
const reporter = options.reporter ?? sentryReporter;
|
|
const loader = options.loader ?? (() => import("web-vitals"));
|
|
|
|
try {
|
|
const mod = await loader();
|
|
mod.onCLS(reporter);
|
|
mod.onLCP(reporter);
|
|
mod.onINP(reporter);
|
|
mod.onTTFB(reporter);
|
|
if (typeof mod.onFCP === "function") {
|
|
mod.onFCP(reporter);
|
|
}
|
|
} catch {
|
|
// Library missing or load error — never break the page.
|
|
}
|
|
}
|