- Guías
- Reportes automáticos
Reportes automáticos
Bugs que se reportan solos: los seis triggers, los guardarraíles contra tormentas, la config server-side que obedece el SDK y cómo verificar la firma HMAC.
El mejor reporte de bug es el que nadie tuvo que escribir. Con los reportes automáticos activados, el SDK sube su ring buffer en el momento en que tu app se rompe — mientras el usuario todavía está en la página, y sin importar si alguna vez te lo dice.
Está desactivado por defecto en todos los proyectos, y activarlo es una decisión que se toma
en la consola, en el servidor. Ese es todo el diseño: tu clave pk_live_... es pública por diseño
(vive en tu HTML, como un DSN de Sentry), así que si un atributo del <script> pudiera habilitar
las subidas, cualquiera que copiara la clave podría gastar tu cuota.
your app the SDK Espejo API ┌────────────────┐ ┌──────────────────────────┐ ┌──────────────────────────┐ │ page loads ───▶│──▶│ GET /v1/ingest/config ──▶ │──▶│ project says on/off, │ │ │ │ (fails ⇒ stays OFF) │◀──│ triggers, caps │ │ throws a 500 ─▶│──▶│ trigger matches? │ │ │ │ │ │ storm guardrails pass? │ │ │ │ │ │ POST /v1/ingest/sessions ─│──▶│ session stored, │ │ │ │ report_source=auto │ │ report_source=auto │ └────────────────┘ └──────────────────────────┘ │ │ │ │ ▼ │ your receiver ◀──── signed POST ───────────────│ webhook (if enabled) │ ┌────────────────┐ X-Espejo-Signature └──────────────────────────┘ │ verify HMAC │ │ hand to agent │ └────────────────┘Cómo activarlo
Sección titulada «Cómo activarlo»- Abrí la configuración del proyecto en la consola y activá Auto-report.
- Elegí los triggers. Cinco están activados por defecto;
auto:network_4xxno lo está (ver más abajo). - Opcionalmente configurá el webhook — una URL y un secreto de firma — para que algo
distinto de una persona se entere. Sin él, los reportes automáticos simplemente aparecen en
tu lista de grabaciones, etiquetados como
Auto.
Nada cambia en tu página. El tag <script> queda exactamente igual: el SDK
le pregunta al servidor qué está activado cada vez que carga una página.
Los seis triggers
Sección titulada «Los seis triggers»| Trigger | Se dispara cuando | Por defecto |
|---|---|---|
auto:uncaught | window.onerror — una excepción que nadie capturó | activado |
auto:unhandled_rejection | una promesa rechazada sin handler | activado |
auto:console_error | se llamó a console.error(...) | activado |
auto:network_5xx | una respuesta llegó con status ≥ 500 | activado |
auto:network_failed | el request nunca se completó: sin red, CORS, abortado | activado |
auto:network_4xx | una respuesta llegó con status 400–499 | desactivado |
auto:network_4xx viene desactivado porque un 401 al revalidar una sesión y un 404
por un avatar faltante son ruido normal en la mayoría de las apps: con él activado, una
pantalla de login con un typo gasta todo el presupuesto por página. Existe para equipos
donde un 4xx significa algo.
console.warn nunca dispara nada. Igual se captura en la grabación —
simplemente no es un bug.
Por qué un render loop no puede arruinarte
Sección titulada «Por qué un render loop no puede arruinarte»Un componente en loop a 60fps lanza 3.600 errores por minuto. Tres guardarraíles corren en el navegador, porque ahí es donde un request puede no hacerse:
- Un límite por carga de página. Tres por defecto, configurable hasta 20. Este es el que detiene el loop puro.
- Un cooldown. 60 segundos entre dos reportes automáticos por defecto. Esto detiene el goteo: un error cada dos segundos nunca alcanza el límite pero igual subiría treinta grabaciones.
- Deduplicación por firma del error. El mismo mensaje y stack nunca se sube dos veces dentro de la sesión de página. Sin esto, un bug ruidoso consume los tres slots y el segundo bug, diferente, nunca pasa.
Un intento rechazado no gasta un slot — de lo contrario, un goteo agotaría el límite sin haber subido nada.
Además de eso, los reportes automáticos consumen el presupuesto diario normal del proyecto
(daily_session_budget y daily_byte_budget). No hay un canal privilegiado,
así que el peor caso está acotado por un número que vos ya elegiste, y la misma
pausa automática lo protege.
El kill switch
Sección titulada «El kill switch»Dos formas de desactivarlo desde la página. Ninguna puede activarlo:
<script src="https://app.espejo.dev/sdk/espejo.js" data-key="pk_live_..." data-auto-report="off"></script>// Para SPAs: silenciá una pantalla que sabés que es ruidosa, y después volvé a activarlo.espejo.setAutoReport(false);espejo.setAutoReport(true); // solo levanta el switch local; el proyecto sigue decidiendoespejo.autoReportActive te dice si un error en este momento subiría algo —
útil para verificar tu configuración sin provocar un error.
Qué llega en la grabación
Sección titulada «Qué llega en la grabación»Un reporte automático es una grabación normal. En la API y por MCP lleva dos campos extra:
report_source—manualoauto. Quién lo pidió.trigger—manual,always, o uno de los valoresauto:*de arriba. Qué pasó.
En la consola la fila está etiquetada como Auto y tiene como título su trigger (no hay
descripción, porque nadie la escribió).
El webhook
Sección titulada «El webhook»Cuando una sesión con report_source=auto termina de ingestarse, y el webhook está
habilitado, Espejo envía un POST con este body:
{ "project_id": "b3f1c0a2-1d4e-4f8a-9c2b-7e5d0a1f3b6c", "project_slug": "acme-app", "session_id": "9f2c4a7b1e8d035face6b2470d18c9a5", "replay_url": "https://app.espejo.dev/s/9f2c4a7b1e8d035face6b2470d18c9a5", "trigger": "auto:network_5xx", "error_excerpt": "TypeError: cannot read properties of undefined (reading 'id')", "counts": { "errors_4xx": 0, "errors_5xx": 2, "console_errors": 1 }, "occurred_at": "2026-08-17T13:41:02.514Z"}Lo que no está ahí: los bodies de request y response, la consola completa, el DOM.
Este JSON viaja a una URL que vos escribiste y va a terminar en un canal de Slack o en el
log de un agente; el detalle queda detrás de autenticación, donde replay_url y las
herramientas MCP pueden alcanzarlo.
Reglas de entrega:
- Solo
https, y el host no debe resolver a una dirección privada, de loopback o link-local. Se verifica cuando lo guardás y de nuevo antes de cada envío — un dominio que era público ayer puede apuntar a10.0.0.5hoy. - Timeout de 5 segundos. Un intento más tres reintentos con backoff exponencial
(1s, 2s, 4s). Un
5xx, un timeout o un error de red se reintenta; un4xxde tu endpoint no, porque va a decir lo mismo tres veces más. - No se siguen redirects. Un
302hacia una dirección interna saltearía directamente la verificación de arriba. - Nunca se envía nada sin firma. Si falta el secreto, la entrega se omite y se registra el motivo.
- El resultado de la última entrega real (incluidas las de prueba) se muestra en la página del proyecto. Un webhook que falla silenciosamente es peor que ninguno.
Verificar la firma
Sección titulada «Verificar la firma»El header es X-Espejo-Signature, con formato sha256=<hex>, y es el
HMAC-SHA256 del body crudo del request con tu secreto.
“Crudo” es la mitad del contrato: calculá el HMAC antes de parsear el JSON. Si parseás y re-serializás, la menor diferencia en orden de claves o espacios en blanco da un digest diferente y cada entrega parece falsificada.
Node / Express:
import express from "express";import { createHmac, timingSafeEqual } from "node:crypto";
const app = express();const SECRET = process.env.ESPEJO_WEBHOOK_SECRET;
// `express.raw` y no `express.json`: necesitamos los bytes exactos que se firmaron.app.post("/espejo", express.raw({ type: "application/json" }), (req, res) => { const expected = "sha256=" + createHmac("sha256", SECRET).update(req.body).digest("hex"); const given = req.get("X-Espejo-Signature") ?? "";
// Comparar en tiempo constante, y verificar el largo primero — timingSafeEqual // lanza en buffers de distinto tamaño, y el largo no es un secreto. const a = Buffer.from(expected); const b = Buffer.from(given); if (a.length !== b.length || !timingSafeEqual(a, b)) { return res.status(401).send("bad signature"); }
const event = JSON.parse(req.body.toString("utf-8")); console.log(event.trigger, event.replay_url); res.sendStatus(200);});Python / Flask:
import hmac, hashlib, osfrom flask import Flask, request
app = Flask(__name__)SECRET = os.environ["ESPEJO_WEBHOOK_SECRET"].encode()
@app.post("/espejo")def espejo(): # request.get_data() is the raw body — do not use request.json here. expected = "sha256=" + hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest() given = request.headers.get("X-Espejo-Signature", "") if not hmac.compare_digest(expected, given): return "bad signature", 401
event = request.get_json() print(event["trigger"], event["replay_url"]) return "", 200Respondé 2xx para confirmar la recepción. Cualquier otra cosa se trata como un fallo y se reintenta
(excepto 4xx, que se toma como un no definitivo).
Probarlo
Sección titulada «Probarlo»El botón Send test en la página del proyecto envía un payload de muestra por el mismo camino que una entrega real: misma firma, mismo timeout, mismos reintentos, misma validación de URL, y registra el resultado. Una prueba que usara un camino separado solo probaría que ese camino separado funciona.
Apuntarle un agente
Sección titulada «Apuntarle un agente»Un webhook que entrega un link de replay, más un servidor MCP que puede leer esa grabación — red con bodies, consola, clicks, DOM — es el material crudo para un pipeline de auto-reparación. El pipeline es tuyo para construir: Espejo graba y reporta, no repara.
Leer la configuración vos mismo
Sección titulada «Leer la configuración vos mismo»El endpoint que llama el SDK es público y devuelve solo lo que un navegador necesita:
GET /v1/ingest/config?key=pk_live_...{ "success": true, "data": { "enabled": true, "triggers": ["auto:uncaught", "auto:network_5xx"], "max_per_page": 3, "cooldown_ms": 60000 }}La clave va en el query string, no en un header, para que esto siga siendo un GET simple: un
header personalizado agregaría un preflight CORS al inicio de cada carga de página. La
respuesta es cacheable por cinco minutos.
La URL del webhook y su secreto nunca están en esta respuesta, y no pueden estarlo:
esto es legible por cualquiera que tenga la clave pública. Cuando auto-report está desactivado — o
la clave es desconocida o fue revocada, o el proyecto está pausado, o la cuenta está
desactivada — la respuesta es el mismo enabled: false, así que el endpoint no puede
usarse para sondear si una clave es válida.