Ir al contenido
Consola

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 │
└────────────────┘
  1. Abrí la configuración del proyecto en la consola y activá Auto-report.
  2. Elegí los triggers. Cinco están activados por defecto; auto:network_4xx no lo está (ver más abajo).
  3. 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.

TriggerSe dispara cuandoPor defecto
auto:uncaughtwindow.onerror — una excepción que nadie capturóactivado
auto:unhandled_rejectionuna promesa rechazada sin handleractivado
auto:console_errorse llamó a console.error(...)activado
auto:network_5xxuna respuesta llegó con status ≥ 500activado
auto:network_failedel request nunca se completó: sin red, CORS, abortadoactivado
auto:network_4xxuna respuesta llegó con status 400–499desactivado

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.

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.

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 decidiendo

espejo.autoReportActive te dice si un error en este momento subiría algo — útil para verificar tu configuración sin provocar un error.

Un reporte automático es una grabación normal. En la API y por MCP lleva dos campos extra:

  • report_source — manual o auto. Quién lo pidió.
  • trigger — manual, always, o uno de los valores auto:* 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ó).

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 a 10.0.0.5 hoy.
  • 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; un 4xx de tu endpoint no, porque va a decir lo mismo tres veces más.
  • No se siguen redirects. Un 302 hacia 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.

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, os
from 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 "", 200

Respondé 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).

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.

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.

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.