Saltar al contenido
Telemetry
Explorar documentación

GuíasActualizado el 2 de octubre de 20267 min de lectura

Usa esta documentación con tu agente de programación

Abra un paquete de mensajes enfocados para Claude Code, Codex, Cursor u otro agente de codificación, luego adáptelo al flujo de trabajo que se describe aquí.

En esta página
  1. Arquitectura
  2. Definir el contrato del navegador
  3. Recopile Web Vitals en el navegador
  4. Validar y reenviar en el servidor
  5. Lista de verificación de seguridad y privacidad
  6. Verificar los datos

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:

  1. El paquete del navegador no contiene ninguna clave Telemetry.
  2. Se rechazan los nombres de eventos desconocidos, los campos adicionales, las URL sin formato y los cuerpos de gran tamaño.
  3. Los eventos CLS, INP y LCP válidos llegan con valores numéricos.
  4. Una solicitud de cobro bloqueada o fallida no afecta la navegación.
  5. Los valores de liberación y ruta son lo suficientemente estables como para agruparlos.
  6. 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.

Función relacionada

Registra nombres de eventos estables, campos con tipos definidos y contexto revisado para proteger la privacidad.

Autores de la página y referencias

El equipo editorial de Telemetry es responsable de esta explicación; el equipo de producto revisa el comportamiento, los ejemplos y las limitaciones.

Cómo revisamos nuestra documentación