Conceptos fundamentales
Arquitectura
Sección titulada «Arquitectura» recorder API (apps/api) storage / consumers┌──────────────┐ HTTPS POST ┌───────────────────────┐ S3 (R2) ┌─────────────┐│ <script> │──────────────▶│ /v1/ingest/sessions │───────────▶│ bucket ││ (packages/ │ x-espejo-key │ IngestKeyGuard │ │ (per-project ││ browser) │ │ + quota + budget │ │ or shared) │└──────────────┘ └───────────┬────────────┘ └──────┬──────┘ Chrome extension │ indexes │ signed (multi-tab, legacy) ▼ │ HMAC links ┌───────────────────────┐ │ │ Postgres: Session, │ │ │ Project, IngestKey │ │ └───────┬───────┬────────┘ │ JWT / admin key │ │ OAuth 2.1 + PKCE │ ┌──────────────┘ └───────────────┐ │ ▼ ▼ │ ┌───────────────────┐ ┌───────────────────┐ │ │ apps/console (SPA)│ │ MCP server (/mcp) │───┘ │ v1/projects, v1/ │ │ 5 read-only tools │ │ sessions │ │ scoped per grant │ └───────────────────┘ └───────────────────┘Hay dos productores de grabaciones: el SDK <script> (packages/browser,
que es lo que cubre esta documentación) y una extensión de Chrome, que es
anterior al SDK y sigue existiendo para lo único que un script de página no
puede hacer: seguir varias pestañas a la vez. Ambos escriben la misma forma de
bundle, definida una sola vez en packages/core para que los dos
consumidores (la consola y el servidor MCP) nunca necesiten saber cuál produjo
una sesión determinada.
Todo cuelga de un único host (dbuger.dnh.ar en producción): el endpoint de
ingesta, la API de la consola, el servidor MCP y los bundles del SDK. Un solo
host significa que la consola habla con la API en el mismo origen — sin CORS
que configurar — y un único certificado en lugar de cuatro.
Tenants, proyectos y claves
Sección titulada «Tenants, proyectos y claves»Espejo es multi-tenant. La jerarquía, de arriba hacia abajo:
- Tenant — una cuenta. Es dueña de proyectos y usuarios.
- Proyecto — una cosa que se está grabando (un sitio, una app). Tiene un
slug, un modo de almacenamiento, límites de retención y una o más claves de ingesta. - Clave de ingesta (
pk_live_…) — lo que lleva el tag<script>. Es pública por diseño, de la misma manera que un DSN de Sentry: solo indica a qué proyecto pertenece una grabación y no otorga acceso de lectura. Las claves se pueden rotar (emitir una nueva) o revocar (marcarla como inactiva; la fila se conserva para que lo que llegó bajo ella pueda auditarse).
Hay dos formas de autenticarse contra la API de la consola: una sesión de
usuario (Authorization: Bearer <jwt>), que es lo que envía la propia
consola y que automáticamente acota cada consulta al tenant de ese usuario; o
la clave de administrador (x-espejo-admin-key), una credencial de máquina
para operar y diagnosticar sin una cuenta — ve todos los tenants, por lo que se
mantiene deliberadamente fuera de cualquier cosa que otorgue acceso en nombre de
una persona (crear un proyecto, aprobar una conexión MCP).
Almacenamiento: tu bucket o el nuestro
Sección titulada «Almacenamiento: tu bucket o el nuestro»El almacenamiento es un adaptador con dos modos, por proyecto: ours escribe
en el bucket propio de Espejo con credenciales del entorno — el valor por
defecto, para que un proyecto nuevo pueda empezar a grabar con un clic, sin
formulario de credenciales cloud por adelantado. tenant (planificado)
escribiría en el bucket propio del cliente. De cualquier manera, el índice
— qué sesiones existen, sus metadatos — vive en el Postgres de Espejo; sin eso,
listar sesiones implicaría escanear un bucket entero en cada carga de página.
El cliente tiene forma R2 (region: 'auto', forcePathStyle: true) porque eso
es lo que necesita R2; el propio S3 funciona igual a través del mismo adaptador.
El bundle
Sección titulada «El bundle»Una grabación es un directorio en el bucket:
espejo/<projectId>/<yyyy-mm>/<sessionId>/ manifest.json what this session is and what objects compose it events.json summary + network + console + interactions + navigation dom.jsonl.gz DOM replay events (dom mode) video.webm (video mode)La ruta siempre la construye el servidor, nunca la propone el navegador — de lo contrario, un cliente podría sobreescribir la sesión de otra persona.
events.json abre con un summary a propósito — cantidad de requests,
4xx/5xx con el clic que los causó, cantidad de errores de consola — porque eso
es lo primero que lee cualquiera al diagnosticar un bug, sea humano o modelo:
interface Summary { request_count: number; interaction_count: number; error_4xx_count: number; error_5xx_count: number; errors: { t_ms: number | null; method: string | null; url: string | null; status: number | null; after: { t_ms: number | null; kind: string; label: string | null } | null }[]; console_error_count: number;}seguido de network, console, interactions y navigations — el mismo
orden en que los devuelven la API y las herramientas MCP.
Redacción
Sección titulada «Redacción»La redacción corre antes del truncado — un cuerpo JSON cortado a la mitad ya no parsea, así que redactarlo después es demasiado tarde. Lo que nunca se escribe:
- Headers:
Authorization,Cookie,Set-Cookie,x-api-key, y cualquier header cuyo nombre coincida con/token|secret/i— defensa en profundidad para headers comox-auth-tokenque no están en la lista exacta. - URLs: parámetros de query y fragmento que coincidan con
/token|secret|password|signature|^code$|^key$|credential/i, userinfo de autenticación básica HTTP, y cualquier segmento de ruta o valor suelto con forma de JWT (tres segmentos base64url unidos por puntos) — reconocido por forma, ya que un token en una ruta URL o en un fragmento de SPA con hash routing no tiene clave que lo identifique. - Cuerpos JSON: cualquier clave cuyo último segmento camelCase/
snake_caseseatoken,secret,password,credential(s),authorization,jwt,bearer,auth,otpopwd(más el caso especial de dos segmentosapi_key/apiKey) — verificado por segmento, no por subcadena, asíauthor_idypwd_reset_requested_atsobreviven mientras queaccess_tokenno. - Valores de inputs: de un campo de formulario, solo se registra que cambió — nunca lo que se escribió.
- Replay del DOM: todos los inputs se enmascaran, no solo los campos de
contraseña. Medido con el enmascaramiento desactivado: una contraseña aparecía
como
********(rrweb ya maneja eso), pero un campo de tarjeta de crédito en texto plano aparecía completo. Un número de documento o el tag de un animal no es menos privado por no ser una contraseña.
Dos atributos le dan control manual a quien integra: data-espejo-block abre
un hueco en el replay y descarta los clics dentro de él; data-espejo-mask
conserva la forma pero oculta el texto.
Retención
Sección titulada «Retención»Cada proyecto tiene tres límites independientes — retention_days,
max_recordings, max_video_seconds (acumulado, no por grabación) — y una
sesión se elimina en cuanto se supera cualquiera de ellos, empezando por la
más antigua. Las tres verificaciones fallan de forma cerrada: un límite inválido
o ausente bloquea la eliminación en lugar de defaultear a “borrar todo”, y el
planificador se niega a correr si su lista de candidatos no está ordenada de más
antigua a más nueva o si sus totales no cuadran.
Qué sigue
Sección titulada «Qué sigue»- Quickstart — obtené una clave de proyecto y enviá una grabación.
- API de la consola — la superficie HTTP completa.
- Servidor MCP — permitile a un agente leer grabaciones directamente.
- Integración de tenant — cómo un producto real conecta Espejo, usando Angirú como ejemplo.