Browser Telemetry Proxy and Core Web Vitals
The Telemetry JavaScript SDK uses a secret API key and belongs in trusted server code. Do not bundle that key into a browser application. To collect Core Web Vitals or bounded frontend reliability events, send a small allowlisted payload to your own same-origin endpoint and let that endpoint forward it to Telemetry.
This design keeps credentials server-side and gives the application one place to enforce consent, origin checks, rate limits, field types, payload size, and privacy rules.
Architecture
browser measurement
-> POST /api/browser-telemetry
-> origin, size, rate, and schema checks
-> server-side telemetry-sh client
-> browser_performance_measured table
The proxy is not a general log endpoint. Accept only known event names and known fields. Never accept a client-supplied table name or Telemetry API key.
Define the browser contract
For Core Web Vitals, one event per metric is easy to validate:
type BrowserMetric = {
event_name: "browser_performance_measured";
metric_name: "CLS" | "INP" | "LCP";
metric_value: number;
metric_id: string;
route_template: string;
release: string;
navigation_type: string;
};
Use a route template such as /projects/:id, not location.href. Do not send query strings, DOM text, form values, cookies, authorization data, referrers containing identifiers, or arbitrary error messages. A random metric ID can help deduplicate retransmission, but it should not become a cross-site user identifier.
Collect Web Vitals in the browser
The web-vitals package reports the current Core Web Vitals. Send after consent where consent is required by your policy and jurisdiction:
import { onCLS, onINP, onLCP, type Metric } from "web-vitals";
function reportMetric(metric: Metric) {
const body = JSON.stringify({
event_name: "browser_performance_measured",
metric_name: metric.name,
metric_value: metric.value,
metric_id: metric.id,
route_template: routeTemplateFor(location.pathname),
release: window.__APP_RELEASE__,
navigation_type: metric.navigationType,
});
if (navigator.sendBeacon) {
navigator.sendBeacon(
"/api/browser-telemetry",
new Blob([body], { type: "application/json" }),
);
return;
}
void fetch("/api/browser-telemetry", {
method: "POST",
headers: { "content-type": "application/json" },
body,
keepalive: true,
credentials: "same-origin",
});
}
onCLS(reportMetric);
onINP(reportMetric);
onLCP(reportMetric);
sendBeacon is appropriate for small best-effort payloads during page termination. Delivery is not guaranteed, browser extensions may block the request, and a user may close the page before transmission. Treat browser measurements as sampled experience data, not an exact billing or audit ledger.
Validate and forward on the server
The endpoint should reject unknown origins, event names, metric names, non-finite values, oversized bodies, and unexpected fields. The example below shows the boundary; adapt the request and response primitives to the server framework:
import { Telemetry } from "telemetry-sh";
const telemetry = new Telemetry(process.env.TELEMETRY_API_KEY);
const allowedMetrics = new Set(["CLS", "INP", "LCP"]);
export async function POST(request: Request) {
if (!isAllowedSameOrigin(request)) {
return new Response("forbidden", { status: 403 });
}
const contentLength = Number(request.headers.get("content-length") || 0);
if (contentLength > 4096 || !(await rateLimit(request))) {
return new Response("rejected", { status: 429 });
}
const input = await request.json();
if (
input.event_name !== "browser_performance_measured" ||
!allowedMetrics.has(input.metric_name) ||
!Number.isFinite(input.metric_value) ||
!isAllowedRouteTemplate(input.route_template)
) {
return new Response("invalid event", { status: 400 });
}
await telemetry.log("browser_performance_measured", {
metric_name: input.metric_name,
metric_value: input.metric_value,
metric_id: boundedString(input.metric_id, 80),
route_template: input.route_template,
release: boundedString(input.release, 80),
navigation_type: boundedCategory(input.navigation_type),
});
return new Response(null, { status: 202 });
}
In production, initialize the client once per server process, use a bounded upstream timeout, and decide whether telemetry failures return 202, 204, or a retryable response. Avoid browser retry loops. A small rate limit by network and session boundary can reduce abuse, but do not store a raw IP address in the event.
Security and privacy checklist
- Keep
TELEMETRY_API_KEYin the server runtime only. - Restrict the endpoint to same-origin requests and the methods and content types you expect.
- Apply a small body limit before parsing JSON.
- Allowlist event names, fields, categories, route templates, and numeric ranges.
- Rate-limit before calling the upstream API.
- Do not use permissive CORS unless cross-origin collection is an intentional, reviewed product.
- Apply your consent and opt-out rules before collection.
- Define retention and deletion behavior for any identifier.
- Monitor rejected and rate-limited requests without copying rejected payloads.
CSRF defenses still matter when the endpoint accepts credentials or mutates user-linked state. For an anonymous, same-origin measurement endpoint, origin validation, strict content types, and a non-user-specific payload may be the appropriate boundary; confirm that choice with the application's security model.
Verify the data
Deploy first to a non-production environment. Confirm that:
- The browser bundle contains no Telemetry key.
- Unknown event names, extra fields, raw URLs, and oversized bodies are rejected.
- Valid CLS, INP, and LCP events arrive with numeric values.
- A blocked or failed collection request does not affect navigation.
- Release and route values are stable enough to group.
- Sparse routes are not used for noisy alerts.
Use the Core Web Vitals by route and release recipe to keep sample count beside the measurements. Continue with Frontend Reliability Monitoring with SQL, the JavaScript SDK guide, and Redacting Sensitive Data.