mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
feat(perf): wire bundle analyzer and web-vitals beacon
This commit is contained in:
1 parent
b0ef80dcd4
commit
ebf83b1ee5
6 files changed
+274
-1
No files matched your search
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* v1.4.28 R3d — Web Vitals beacon.
|
||||
*
|
||||
* Next 16 exposes `reportWebVitals` from the root layout; we wire the
|
||||
* client to POST every measurement to this internal route, which logs
|
||||
* to the shared wide-event pipeline so ops can see CLS / FCP / FID /
|
||||
* LCP / TTFB / INP histograms in the same place the rest of the
|
||||
* request flow surfaces.
|
||||
*
|
||||
* Why a route + not the front-end alone:
|
||||
*
|
||||
* - `console.log` from the client is invisible to the server-side
|
||||
* observability stack (HealthLog ships logs through the
|
||||
* `WideEventBuilder` in `src/lib/logging/context.ts`). Funnelling
|
||||
* metrics through a thin route reuses the existing pipeline.
|
||||
* - Persisting elsewhere (Postgres, S3) would be a wasted spend —
|
||||
* this is a beacon. We log and forget.
|
||||
*
|
||||
* Authentication: none (the route is internal-only by name; CSP +
|
||||
* same-origin policy keep cross-site reporting out). The dispatch is
|
||||
* idempotent — a duplicate beacon is at worst one extra log line.
|
||||
*
|
||||
* Rate-limit: deliberately none. Web Vitals emit one report per metric
|
||||
* per page load (max 6 per session). A bot trying to flood the route
|
||||
* is cheaper to drop at the edge.
|
||||
*
|
||||
* Payload shape: matches `web-vitals`' `Metric` interface — `id`,
|
||||
* `name`, `value`, `delta`, `rating`. We log the lot; if a future
|
||||
* client adds fields we'll see them in the event meta.
|
||||
*/
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { apiHandler } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
|
||||
interface WebVitalsBody {
|
||||
id?: unknown;
|
||||
name?: unknown;
|
||||
value?: unknown;
|
||||
delta?: unknown;
|
||||
rating?: unknown;
|
||||
navigationType?: unknown;
|
||||
}
|
||||
|
||||
export const POST = apiHandler(async (request: NextRequest) => {
|
||||
let body: WebVitalsBody | null = null;
|
||||
try {
|
||||
body = (await request.json()) as WebVitalsBody;
|
||||
} catch {
|
||||
// Malformed payload — accept silently so the client doesn't retry.
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
if (!body || typeof body !== "object") {
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
const name = typeof body.name === "string" ? body.name : "unknown";
|
||||
const value =
|
||||
typeof body.value === "number" && Number.isFinite(body.value)
|
||||
? body.value
|
||||
: null;
|
||||
const rating = typeof body.rating === "string" ? body.rating : null;
|
||||
|
||||
annotate({
|
||||
meta: {
|
||||
"web_vitals.name": name,
|
||||
"web_vitals.value": value,
|
||||
"web_vitals.rating": rating,
|
||||
"web_vitals.navigation_type":
|
||||
typeof body.navigationType === "string" ? body.navigationType : null,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import "./globals.css";
|
||||
import { Providers } from "@/components/providers";
|
||||
import { AuthShell } from "@/components/layout/auth-shell";
|
||||
import { MonitoringBootstrap } from "@/components/monitoring/bootstrap";
|
||||
import { WebVitalsReporter } from "@/components/monitoring/web-vitals-reporter";
|
||||
import { parseLocaleFromAcceptLanguage } from "@/lib/format-locale";
|
||||
import { locales, type Locale } from "@/lib/i18n/config";
|
||||
|
||||
@@ -96,6 +97,7 @@ export default async function RootLayout({
|
||||
<body className={`${inter.variable} font-sans antialiased`}>
|
||||
<Providers initialLocale={initialLocale}>
|
||||
<MonitoringBootstrap />
|
||||
<WebVitalsReporter />
|
||||
<AuthShell>{children}</AuthShell>
|
||||
</Providers>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { useReportWebVitals } from "next/web-vitals";
|
||||
|
||||
/**
|
||||
* v1.4.28 R3d — client-side mount that pipes every Web Vitals
|
||||
* measurement to `/api/internal/web-vitals`.
|
||||
*
|
||||
* Next 16 ships `useReportWebVitals` as the supported hook for
|
||||
* capturing CLS / FCP / FID / LCP / TTFB / INP from the live
|
||||
* navigation. The hook fires once per metric per page load, so even a
|
||||
* heavy navigation produces at most six beacons.
|
||||
*
|
||||
* Delivery transport: `navigator.sendBeacon` when available (queues
|
||||
* the POST without blocking the page lifecycle, survives the
|
||||
* `pagehide` event) with a `fetch({ keepalive: true })` fallback.
|
||||
* The route accepts the payload best-effort and returns 200 even on
|
||||
* malformed bodies — beacons must never delay or fail the navigation.
|
||||
*/
|
||||
export function WebVitalsReporter() {
|
||||
useReportWebVitals((metric) => {
|
||||
const body = JSON.stringify({
|
||||
id: metric.id,
|
||||
name: metric.name,
|
||||
value: metric.value,
|
||||
delta: metric.delta,
|
||||
rating: metric.rating,
|
||||
navigationType: metric.navigationType,
|
||||
});
|
||||
const url = "/api/internal/web-vitals";
|
||||
|
||||
if (
|
||||
typeof navigator !== "undefined" &&
|
||||
typeof navigator.sendBeacon === "function"
|
||||
) {
|
||||
navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback for non-`sendBeacon` browsers — `keepalive` keeps the
|
||||
// request alive past navigation. We deliberately ignore the
|
||||
// response (it's a beacon, not a transaction).
|
||||
void fetch(url, {
|
||||
method: "POST",
|
||||
body,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
keepalive: true,
|
||||
}).catch(() => undefined);
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in new issue
Block a user