Proxy de telemetría del navegador y Core Web Vitals
El Telemetry JavaScript SDK utiliza una clave secreta API y pertenece al código de servidor confiable. No incluya esa clave en una aplicación de navegador. Para recopilar Core Web Vitals o eventos de confiabilidad de frontend limitados, envíe una pequeña carga útil incluida en la lista permitida a su propio punto final del mismo origen y deje que ese punto final la reenvíe a Telemetry.
Este diseño mantiene las credenciales en el lado del servidor y le brinda a la aplicación un lugar para hacer cumplir el consentimiento, las verificaciones de origen, los límites de velocidad, los tipos de campos, el tamaño de la carga útil y las reglas de privacidad.
Arquitectura
browser measurement
-> POST /api/browser-telemetry
-> origin, size, rate, and schema checks
-> server-side telemetry-sh client
-> browser_performance_measured table
El proxy no es un punto final de registro general. Acepte sólo nombres de eventos conocidos y campos conocidos. Nunca acepte un nombre de tabla proporcionado por el cliente o una clave Telemetry API.
Definir el contrato del navegador
Para Core Web Vitals, un evento por métrica es fácil de validar:
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;
};
Utilice una plantilla de ruta como /projects/:id, no location.href. No envíe cadenas de consulta, texto DOM, valores de formulario, cookies, datos de autorización, referencias que contengan identificadores ni mensajes de error arbitrarios. Una ID de métrica aleatoria puede ayudar a deduplicar la retransmisión, pero no debería convertirse en un identificador de usuario entre sitios.
Recopile Web Vitals en el navegador
El paquete web-vitals informa los Core Web Vitals actuales. Enviar después del consentimiento cuando su política y jurisdicción requieran el consentimiento:
import { onCLS, onINP, onLCP, type Metric } from "web-vitals";
function reportMetric(metric: Metric) {
// Adapt these app-owned route/release helpers; never send a raw URL.
try {
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 ?? "unknown",
});
if (navigator.sendBeacon) {
try {
const queued = navigator.sendBeacon(
"/api/browser-telemetry",
new Blob([body], { type: "application/json" }),
);
if (queued) return;
} catch {
// A refused beacon gets one best-effort fallback, not a retry loop.
}
}
void fetch("/api/browser-telemetry", {
method: "POST",
headers: { "content-type": "application/json" },
body,
keepalive: true,
credentials: "same-origin",
}).catch(() => {});
} catch {
// Collection or metadata failure must not interrupt the page.
}
}
onCLS(reportMetric);
onINP(reportMetric);
onLCP(reportMetric);
sendBeacon es apropiado para cargas útiles pequeñas de mejor esfuerzo durante la terminación de la página. La entrega no está garantizada, las extensiones del navegador pueden bloquear la solicitud y un usuario puede cerrar la página antes de la transmisión. Trate las mediciones del navegador como datos de experiencia de muestra, no como un libro de auditoría o facturación exacto.
Validar y reenviar en el servidor
El punto final debe rechazar orígenes desconocidos, nombres de eventos, nombres de métricas, valores no finitos, cuerpos de gran tamaño y campos inesperados. El siguiente ejemplo muestra el límite; Adapte las primitivas de solicitud y respuesta al marco del servidor:
import telemetry from "telemetry-sh";
telemetry.init(process.env.TELEMETRY_API_KEY!);
const maxBodyBytes = 4096;
const allowedMetrics = new Set(["CLS", "INP", "LCP"]);
const allowedFields = new Set([
"event_name", "metric_name", "metric_value", "metric_id",
"route_template", "release", "navigation_type",
]);
const allowedNavigationTypes = new Set([
"navigate", "reload", "back-forward", "back-forward-cache",
"prerender", "restore", "soft-navigation", "unknown",
]);
// Count actual stream bytes: Content-Length can be absent or incorrect.
async function readBoundedJson(request: Request) {
if (!request.body) return { ok: false as const, status: 400 };
const reader = request.body.getReader();
const chunks: Uint8Array[] = [];
let bytes = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
bytes += value.byteLength;
if (bytes > maxBodyBytes) {
void reader.cancel().catch(() => {});
return { ok: false as const, status: 413 };
}
chunks.push(value);
}
const body = new Uint8Array(bytes);
let offset = 0;
for (const chunk of chunks) {
body.set(chunk, offset);
offset += chunk.byteLength;
}
const text = new TextDecoder("utf-8", { fatal: true }).decode(body);
return { ok: true as const, value: JSON.parse(text) as unknown };
} catch {
return { ok: false as const, status: 400 };
} finally {
reader.releaseLock();
}
}
function isBoundedString(value: unknown, max: number): value is string {
return typeof value === "string" && value.length > 0 && value.length <= max;
}
export async function POST(request: Request) {
// Supply these app-owned origin, rate-limit and route allowlist adapters.
if (!isAllowedSameOrigin(request)) {
return new Response("forbidden", { status: 403 });
}
const contentType = request.headers.get("content-type")?.split(";")[0].trim().toLowerCase();
if (contentType !== "application/json") {
return new Response("unsupported content type", { status: 415 });
}
if (!(await rateLimit(request))) {
return new Response("rate limited", { status: 429 });
}
const parsed = await readBoundedJson(request);
if (!parsed.ok) return new Response("invalid body", { status: parsed.status });
const input = parsed.value;
if (input === null || typeof input !== "object" || Array.isArray(input)) {
return new Response("invalid event", { status: 400 });
}
const event = input as Record<string, unknown>;
if (
Object.keys(event).some(key => !allowedFields.has(key)) ||
event.event_name !== "browser_performance_measured" ||
typeof event.metric_name !== "string" || !allowedMetrics.has(event.metric_name) ||
typeof event.metric_value !== "number" || !Number.isFinite(event.metric_value) || event.metric_value < 0 ||
!isBoundedString(event.metric_id, 80) ||
!isBoundedString(event.route_template, 160) || !isAllowedRouteTemplate(event.route_template) ||
!isBoundedString(event.release, 80) ||
typeof event.navigation_type !== "string" || !allowedNavigationTypes.has(event.navigation_type)
) {
return new Response("invalid event", { status: 400 });
}
try {
await telemetry.log("browser_performance_measured", {
metric_name: event.metric_name,
metric_value: event.metric_value,
metric_id: event.metric_id,
route_template: event.route_template,
release: event.release,
navigation_type: event.navigation_type,
});
} catch {
return new Response("collection unavailable", { status: 502 });
}
return new Response(null, { status: 202 });
}
El navegador hace como máximo una petición alternativa si se rechaza el beacon; encolado no significa entregado. Proporciona routeTemplateFor y la versión desde tu aplicación, y registra la captura solo tras el consentimiento requerido. En el servidor, implementa isAllowedSameOrigin, rateLimit e isAllowedRouteTemplate según tu política. El ejemplo limita los bytes reales del JSON y rechaza campos desconocidos, sin recortarlos silenciosamente. Un 202 es la confirmación de este handler, no una prueba de entrega persistente. La lectura del cuerpo y la espera al SDK necesitan límites de tiempo de la aplicación; no se ofrece un plazo total ni una cola persistente. Revisa la atribución de rutas SPA y la deduplicación antes de activar métricas de navegación suave.
En producción, inicialice el cliente una vez por proceso del servidor, utilice un tiempo de espera ascendente limitado y decida si los errores de telemetría devuelven 202, 204 o una respuesta reintentable. Evite los bucles de reintento del navegador. Un pequeño límite de velocidad por red y límite de sesión puede reducir el abuso, pero no almacene una dirección IP sin procesar en ese caso.
Lista de verificación de seguridad y privacidad
- Mantenga
TELEMETRY_API_KEYúnicamente en el tiempo de ejecución del servidor. - Restrinja el punto final a solicitudes del mismo origen y los métodos y tipos de contenido que espera.
- Aplique un límite de cuerpo pequeño antes de analizar JSON.
- Lista de permitidos nombres de eventos, campos, categorías, plantillas de ruta y rangos numéricos.
- Límite de velocidad antes de llamar al API ascendente.
- No utilice CORS permisivo a menos que la recopilación de orígenes cruzados sea un producto revisado intencional.
- Aplique sus reglas de consentimiento y exclusión voluntaria antes de la recolección.
- Defina el comportamiento de retención y eliminación de cualquier identificador.
- Supervise las solicitudes rechazadas y de velocidad limitada sin copiar las cargas útiles rechazadas.
Las defensas CSRF siguen siendo importantes cuando el punto final acepta credenciales o cambia el estado vinculado al usuario. Para un punto final de medición anónimo del mismo origen, la validación del origen, los tipos de contenido estrictos y una carga útil no específica del usuario pueden ser el límite apropiado; confirme esa elección con el modelo de seguridad de la aplicación.
Verificar los datos
Implemente primero en un entorno que no sea de producción. Confirma que:
- El paquete del navegador no contiene ninguna clave Telemetry.
- Se rechazan los nombres de eventos desconocidos, los campos adicionales, las URL sin formato y los cuerpos de gran tamaño.
- Los eventos CLS, INP y LCP válidos llegan con valores numéricos.
- Una solicitud de cobro bloqueada o fallida no afecta la navegación.
- Los valores de liberación y ruta son lo suficientemente estables como para agruparlos.
- Las rutas dispersas no se utilizan para alertas ruidosas.
Utilice el Core Web Vitals por ruta y receta de lanzamiento para mantener el recuento de muestras junto a las mediciones. Continúe con Monitoreo de confiabilidad frontend con SQL, Guía JavaScript SDK y Eliminación de datos sensibles confidenciales.