Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/lib/observability/web-vitals.ts
T

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.
}
}