La red, con los cuerpos
fetch y XHR se parchean desde adentro de la página, así que los cuerpos de request y de response están de verdad. Sin barra amarilla del depurador, sin extensión, sin medio log.
Un script, y un reporte de bug deja de ser una frase. Llega con la sesión adentro: las llamadas de red con sus cuerpos, la consola, cada clic, y el replay del DOM o el video de la pantalla. Mirala vos, o apuntá un agente de IA por MCP y dejá que la lea entera.
Plan gratis, sin tarjeta. 18 KB gzip en tu página, y no sube nada hasta que alguien reporta un problema.
Paquete npm - un import, SDK completo
import { Espejo } from "@espejo/browser"; const espejo = new Espejo({ key: "pk_live_..." }); espejo.start();
Después, cuando un usuario se choca con algo:
await espejo.report('No me deja guardar el pedido') // → la URL de la sesión subida
El replay, la red, la consola y las interacciones comparten la línea de tiempo. Un 500 deja de ser una línea de log y pasa a ser el botón que alguien apretó un segundo antes.
Conectá las tools y el MCP de Espejo a Claude, Codex, LangGraph o tu propio agente. Puede guiar a tus usuarios, actuar por ellos en tu producto y reportar automáticamente un problema cuando una acción necesita asistencia. Así las preguntas de uso se resuelven en el momento y a tu equipo solo llegan los errores reales, los que requieren una corrección en la aplicación.
Miembros del equipo y sus roles.
Espejo corre un servidor MCP en el mismo proceso que la API. Claude Code, claude.ai o cualquier cliente MCP se conecta a POST /mcp con OAuth 2.1 y PKCE; una persona lo aprueba en una pantalla de consentimiento y elige qué proyectos puede ver; y de ahí en más el agente tiene cinco tools de lectura. Puede encontrar la sesión que falló y leer qué pasó adentro sin que nadie abra una consola.
Conectar un agente
$ claude mcp add --transport http espejo https://app.espejo.dev/mcp
Las cinco están detrás de un único scope mcp:read. Una tool sin entrada en la tabla de política no corre, así que agregar un handler nunca alcanza para exponerla: hay que declarar además qué autoridad necesita, en otro archivo, a propósito.
La clave de administrador de la plataforma ve todas las cuentas, así que se la rechaza en la puerta del MCP. Cada conexión tiene atrás una cuenta, un consentimiento y un alcance.
Cada conexión es un permiso que se ve y se revoca, con refresh rotativo y revocación RFC 7009. Se saca el consentimiento y el agente queda afuera.
Las cinco tools
list_recordings Las grabaciones que esta conexión puede ver, de la más nueva a la más vieja. Se filtra por proyecto, por rango de fechas, por texto libre, o sólo las que tienen errores.
get_recording Todo lo que Espejo sabe de una: cuándo corrió, cuánto duró, en qué página y en qué release, sus conteos de error, y qué objetos guardó.
recording_events Qué pasó adentro. Primero el resumen, requests, 4xx/5xx con el clic que los provocó, errores de consola, y después red, consola, interacciones y navegaciones.
recording_frames Links firmados y de vida corta a lo que se puede mirar: el video de pantalla, el replay del DOM de rrweb. Son URLs de Espejo, nunca del bucket, y vencen.
recording_transcript La transcripción guardada de la narración de una grabación, si tiene. Nunca dispara una transcripción. Espejo todavía no las produce.
Usá el paquete npm si tu app tiene bundler. El script alojado sigue disponible si querés instalar sin build.
Por defecto no sube nada. El script graba en un anillo en memoria con tope de bytes y manda ese pedazo recién cuando se llama a report(). data-mode="always" sube todas las sesiones, existe, pero no es el default. Es la diferencia entre algo que se puede dejar puesto en producción y algo que te quema el bucket en una semana.
Paquete npm - un import, SDK completo
import { Espejo } from "@espejo/browser"; const espejo = new Espejo({ key: "pk_live_..." }); espejo.start();
¿No tenés bundler? Usá el script alojado · Red con cuerpos, consola, clics y navegación, más el botón de reporte y la encuesta de feedback · 18 KB gzip
<script src="https://app.espejo.dev/sdk/espejo.js" data-key="pk_live_..."></script>
Todo lo anterior, más un replay visual completo del DOM · esa misma reconstrucción es la que le da al heatmap su fondo real
<script src="https://app.espejo.dev/sdk/espejo.dom.js" data-key="pk_live_..."></script>
Todo capturado desde la propia página: no hay nada que instalar ni nada que aprobar.
fetch y XHR se parchean desde adentro de la página, así que los cuerpos de request y de response están de verdad. Sin barra amarilla del depurador, sin extensión, sin medio log.
Cada log y cada error, cada interacción descrita, qué se clickeó, jamás qué se tipeó, y cada cambio de URL, en el mismo reloj que todo lo demás.
rrweb reconstruye la página tal como estaba. O grabás la pantalla real, con micrófono, desde un click handler. Necesita un gesto del usuario, así que es un botón de tu interfaz.
Cuentas, proyectos, y una clave pública por proyecto que va directo a tu HTML, igual que un DSN de Sentry. La ruta en el bucket la arma siempre el server, nunca la propone el navegador.
Por defecto el nuestro, para que un proyecto arranque en un clic. Que las grabaciones queden en tu propio bucket, con tu credencial, llega con el plan Team, para quien necesite que no salgan de su cuenta. R2 y S3.
Un botón flotante de “reportar un problema” se monta solo, salvo que lo apagues con data-button="off". O llamás a report() desde tu propia interfaz y te devuelve la URL de la sesión.
Cada clic se captura sin configurar nada y se pinta sobre la página reconstruida desde el DOM · no un screenshot, el markup real con los estilos que vio tu usuario. Dónde se aprieta una pantalla, y dónde no la toca nadie. Una ruta que todavía no tiene snapshot cae a un wireframe neutro.
Describís un patrón una vez: llegó al carrito y no compró, clickeó cinco veces el mismo lugar, o empezó un formulario y no lo envió. Espejo cuenta cada sesión que lo matchea y te linkea directo a esas grabaciones. Empezás desde un template o armás el tuyo; cada pantalla se agrupa por ruta normalizada, así /orden/123 y /orden/456 son la misma página.
Las señales se juntan en todos los planes, no se pierde nada mientras estás en el plan gratis. Definir behaviors y ver sus conteos y segmentos es Pro; el catálogo de templates lo ve cualquiera.
Una encuesta chica · caritas, una escala del 1 al 7 o pulgares · aparece una vez por visitante, y un solo toque deja un voto en un dashboard de Satisfacción. Apagada por defecto, y aparte del botón de reportar: esto es un voto, no una grabación.
Y atá cada voto a uno de tus usuarios. Un atributo o un solo identify() le dice a Espejo a qué usuario tuyo pertenece un voto o una sesión, así ves el feedback y las grabaciones por persona. Es un id opaco de tu propia base, nunca un email ni PII.
La captura es JavaScript corriendo adentro de tu página: fetch y XHR parcheados, la consola, los listeners de eventos, la History API. Corre donde corra la página, así que una sesión graba su red, su consola y cada toque igual en un iPhone que en Android. Los clics son toques, no cambia nada más: la misma grabación, las mismas cinco tools de MCP, los mismos reportes automáticos.
Lo único de escritorio es el video de pantalla. Grabarla necesita getDisplayMedia y un gesto del usuario, y ningún navegador de celular lo expone: ni Safari en iOS, ni Chrome en Android. En el teléfono queda todo lo demás: red con cuerpos, consola, toques, navegación y el replay del DOM.
El mejor reporte de bug es el que nadie tuvo que escribir. Se prende por proyecto y Espejo sube el buffer en el momento en que tu app se rompe, con el usuario todavía en la página, y sin depender de que te avise.
Qué pasa, en orden
Una excepción no capturada, una promesa rechazada que nadie atrapó, un 500 de tu API. El SDK ya está mirando todo eso: son los mismos hooks que llenan la grabación.
El mismo camino de subida que el botón de reportar, así hay una sola puerta y no dos. Nunca con video: grabar la pantalla necesita un gesto del usuario, y acá no hay nadie para darlo. El replay del DOM sí viaja, si cargaste el bundle dom.
El link de la grabación, el disparador, el extracto del error y los contadores, firmado con HMAC-SHA256 en X-Espejo-Signature para que tu receptor pueda probar que salió de acá. Timeout de 5s, reintentos con backoff, y el último resultado a la vista en la pantalla del proyecto.
La grabación ya es legible por un agente: red con cuerpos, consola, clics, DOM. Un webhook que entrega un link más un servidor MCP que lo sabe leer es la materia prima de un pipeline que se arregla solo, el pipeline lo armás vos. Espejo graba y avisa; no repara.
Qué lo puede disparar
auto:uncaught window.onerror, los que rompen una pantalla
auto:unhandled_rejection una promesa rechazada que nadie manejó
auto:console_error console.error, para los equipos que lo usan en serio
auto:network_5xx tu propia API contestó 500
auto:network_failed el request no salió: sin red, CORS, abortado
auto:network_4xx apagado por defecto, un 401 o un avatar que falta suele ser ruido
Por qué un render loop no te funde
Un componente en loop a 60fps tira 3.600 errores por minuto. El tope de fábrica es tres subidas por carga de página, hay 60 segundos de espera entre una y otra, y la misma firma de error no sube dos veces. Los tres candados, porque cada uno ataja una forma distinta de avalancha.
Los reportes automáticos consumen los topes diarios normales del proyecto. No hay canal privilegiado, así que el peor caso lo acota un número que ya pusiste vos. La pausa que lo protege es la misma de siempre.
El SDK le pregunta al server qué está prendido. El script puede apagarlo (data-auto-report="off"), nunca prenderlo, tu clave pública es pública, así que un atributo que pudiera activarlo dejaría a cualquiera gastarte la cuota. Si el pedido de configuración falla, los reportes automáticos quedan apagados y el SDK sigue andando.
Una grabación termina en un bucket y en la ventana de contexto de un modelo. Un secreto que se escapa acá no se “borra” después, por eso el redactor corre en el navegador, antes de que salga nada.
Authorization, Cookie, Set-Cookie, x-api-key y proxy-authorization, más cualquier header cuyo nombre traiga “token” o “secret”, que ataja los que nadie listó.
Valores de query y de fragmento que se llamen como un token, password, signature, code, key o credential; el user:pass@ del host; y cualquier cosa con forma de JWT, reconocida por su forma aunque esté sin nombre en medio de un path.
De un input se registra que cambió, jamás con qué. En el replay del DOM van enmascarados todos los inputs, no sólo los de contraseña: un número de tarjeta en un campo de texto común no es menos privado por no ser una contraseña.
data-espejo-block deja un hueco en el replay y además calla los clics de adentro. data-espejo-mask conserva la forma y tapa el texto.
Redactar corre antes de truncar, a propósito: un JSON cortado a la mitad ya no parsea, y un redactor que no puede parsear no puede redactar.
Un plan gratis con el producto entero adentro, y topes en vez de un reloj de prueba.
Gratis
$0
para siempre, sin tarjeta
Todo lo de esta página. El servidor MCP incluido.
El primer tope que se toca manda: ahí se borra la grabación más vieja. Son tres y no uno porque cada uno ataja un abuso distinto: los días atajan al que graba poco y deja todo para siempre, la cantidad al que graba mucho y corto, y los minutos al que graba pocas veces pero largo.
Team
$9
por mes, 2 asientos incluidos
Topes más altos, tu propio bucket, más gente en la cuenta.
Se cobra por asiento, mensual o anual: pagás por la gente que está en la cuenta, no por un escalón de plan que se te queda chico.
Creá un proyecto, pegá el script, reproducí el bug. Después preguntale a tu agente qué pasó.