- Guías
- Arma tu propio botón de reporte
Arma tu propio botón de reporte
Reporta bugs desde tu propia interfaz en lugar del botón flotante de Espejo. Apaga el botón, llama a report() desde tu formulario, graba la pantalla con tus propios controles y maneja errores y límites.
Espejo monta por defecto un botón flotante de reporte (un ícono de bug, con la etiqueta “Reportar un problema”; puedes cambiar el texto, ver Opciones del constructor). Es la forma más rápida de empezar, pero tu producto puede tener ya un lugar para esto: un menú de ayuda, un formulario de feedback, una página de soporte. Esta guía muestra cómo apagar nuestro botón y reportar desde tu propia interfaz con el SDK. La grabación es la misma: la red, la consola, los clics y la navegación (más el replay del DOM o un video de la pantalla cuando los usas) viajan con el reporte.
Úsalo cuando:
- Quieres que el reporte se vea y se lea como el resto de tu producto.
- Ya tienes un flujo de “reportar un problema” y quieres que la grabación viaje con él.
- Quieres decidir dónde y cuándo se puede reportar, por ejemplo solo para usuarios con sesión iniciada o solo en algunas páginas.
Instala con el botón apagado
Sección titulada «Instala con el botón apagado»Desde npm
Sección titulada «Desde npm»Instala el paquete. Funciona igual con pnpm o yarn:
npm install @espejo/browserCrea el grabador una sola vez, en el navegador, y arráncalo. Con
button: "off" no se monta el botón flotante de reporte. La captura funciona
exactamente igual. Otros widgets de Espejo pueden seguir apareciendo: ver
Otros widgets de Espejo.
import { Espejo } from "@espejo/browser";
export const espejo = new Espejo({ key: "pk_live_...", button: "off",});espejo.start();new Espejo()lanzaEspejo: missing data-key (the project's public key)sinkey.- No se graba nada hasta que llamas a
start(). Antes destart(), y después destop(),report()devuelvenull. - Llama a
start()una vez. Una segunda llamada mientras graba no hace nada. - Necesita un navegador: créalo del lado del cliente, después de que carga la página, no durante el renderizado en el servidor.
Los tipos de TypeScript vienen incluidos, así que no hay nada más que instalar.
Con la etiqueta script
Sección titulada «Con la etiqueta script»Si no usas un bundler, agrega data-button="off" al script:
<script src="https://app.espejo.dev/sdk/espejo.js" data-key="pk_live_..." data-button="off"></script>El script arranca solo y deja la instancia en window.espejo. Sin data-key
no graba nada y no define window.espejo. Cárgalo sin async ni defer si
un script inline justo después usa window.espejo, o espera al evento
load. Usa espejo.dom.js en lugar de espejo.js para sumar el replay del
DOM, como en el Quickstart.
Otros widgets de Espejo
Sección titulada «Otros widgets de Espejo»button: "off" solo quita el botón de reporte. Dos piezas más de Espejo
pueden seguir apareciendo en tu página, según cómo esté configurado el
proyecto en la consola:
- La encuesta de feedback. Aparece solo si el proyecto la activó. Para
suprimirla en una página, pasa
feedback: "off"o agregadata-feedback="off". Solo puede apagarla, nunca encenderla. Ver Feedback. - Los mensajes en vivo (el banner o el modal de un comportamiento). Aparecen solo si el proyecto configuró un mensaje. No hay una opción en la página para apagarlos: adminístralos en la consola. Ver Comportamientos.
const espejo = new Espejo({ key: "pk_live_...", button: "off", feedback: "off",});Reporta desde tu botón
Sección titulada «Reporta desde tu botón»espejo.report(description); // Promise<string | null>espejo.report(description, undefined, onProgress); // con progreso de subidadescription: lo que escribió la persona. Viaja con la grabación como texto plano. Un texto vacío se guarda como sin descripción.- El segundo argumento: omítelo siempre, o pasa
undefinedcuando necesites el tercero. Espejo lo usa para sus propios reportes. onProgress: progreso de subida opcional, de 0 a 1. Ver Progreso de subida.
Se resuelve con un texto que identifica el reporte, o con null cuando el
grabador no está corriendo. Es para tu equipo, no para la persona que
reportó: no se lo muestres. No cuentes con que sirva para abrir la grabación.
Después de un reporte que se resuelve, espejo.lastReportId tiene el id de
ese reporte (32 caracteres hexadecimales). Queda en null hasta el primer
reporte de la página. Guárdalo junto a tu propio ticket o fila para vincular
el reporte con tu propio sistema.
Cada reporte manda lo grabado desde el anterior y después empieza de nuevo, así que dos reportes seguidos no mandan los mismos eventos dos veces. Un reporte automático también cuenta como reporte anterior.
Se rechaza cuando la subida falla. Ver Errores y límites.
Un ejemplo completo con npm
Sección titulada «Un ejemplo completo con npm»Un formulario con su propio campo de texto y su propio botón. Desactiva el botón mientras envía, confirma con un mensaje corto y muestra un error genérico cuando la subida falla.
<form id="report-form"> <label for="report-text">¿Qué salió mal?</label> <textarea id="report-text" rows="4" required></textarea> <button id="report-send" type="submit">Enviar reporte</button> <p id="report-status" role="status"></p></form>import { espejo } from "./espejo"; // la instancia creada arriba
const form = document.querySelector<HTMLFormElement>("#report-form")!;const text = document.querySelector<HTMLTextAreaElement>("#report-text")!;const send = document.querySelector<HTMLButtonElement>("#report-send")!;const status = document.querySelector<HTMLParagraphElement>("#report-status")!;
form.addEventListener("submit", async (event) => { event.preventDefault(); const description = text.value.trim(); if (!description) return;
send.disabled = true; status.textContent = "Enviando..."; try { const result = await espejo.report(description); if (result === null) { status.textContent = "Ahora no se puede reportar."; return; } text.value = ""; status.textContent = "Gracias. Recibimos tu reporte."; // Opcional: guarda el id de tu lado, junto a tu propio ticket. await fetch("/api/support/reports", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ description, id: espejo.lastReportId }), }); } catch (error) { // console.warn y no console.error: ver la nota de abajo. console.warn("Espejo report failed", error); status.textContent = "No pudimos enviar tu reporte. Prueba de nuevo en un momento."; } finally { send.disabled = false; }});La llamada a /api/support/reports es tu propio backend, no el de Espejo.
Quítala si no la necesitas.
Registra un reporte fallido con console.warn, no con console.error.
Cuando los reportes automáticos están encendidos en el proyecto, un
console.error puede disparar uno. console.warn nunca lo hace.
El mismo ejemplo con la etiqueta script
Sección titulada «El mismo ejemplo con la etiqueta script»<script src="https://app.espejo.dev/sdk/espejo.js" data-key="pk_live_..." data-button="off"></script>
<form id="report-form"> <label for="report-text">¿Qué salió mal?</label> <textarea id="report-text" rows="4" required></textarea> <button id="report-send" type="submit">Enviar reporte</button> <p id="report-status" role="status"></p></form>
<script> const form = document.getElementById("report-form"); const text = document.getElementById("report-text"); const send = document.getElementById("report-send"); const status = document.getElementById("report-status");
form.addEventListener("submit", async (event) => { event.preventDefault(); const description = text.value.trim(); if (!description || !window.espejo) return;
send.disabled = true; status.textContent = "Enviando..."; try { const result = await window.espejo.report(description); if (result === null) { status.textContent = "Ahora no se puede reportar."; return; } text.value = ""; status.textContent = "Gracias. Recibimos tu reporte."; } catch (error) { console.warn("Espejo report failed", error); status.textContent = "No pudimos enviar tu reporte. Prueba de nuevo en un momento."; } finally { send.disabled = false; } });</script>Con el chat de soporte en la misma página
Sección titulada «Con el chat de soporte en la misma página»Cuando el agente de soporte abre un ticket, puede
adjuntar la última grabación que el visitante reportó en la página. El chat
la lee de window.espejo.lastReportId. La etiqueta script define
window.espejo por ti. Con npm, defínelo tú para que el chat la encuentre:
(window as any).espejo = espejo;Graba la pantalla con tus propios controles
Sección titulada «Graba la pantalla con tus propios controles»Un video de la pantalla muestra el bug mejor que cualquier descripción. Puedes ofrecerlo desde tu propia interfaz con estos métodos:
| Miembro | Qué hace |
|---|---|
startVideo(options?) | Abre el selector de pantalla del navegador y empieza a grabar. Se resuelve cuando la grabación arrancó. No hace nada si ya hay una en curso. |
stopVideo() | Detiene la grabación y guarda el video para el próximo report(). Se resuelve con el video como Blob, o con null cuando no se estaba grabando. |
setMicrophone(on) | Enciende o apaga el micrófono sin detener la grabación. Se resuelve con { on, denied }. |
discardVideo() | Descarta un video ya detenido para que el próximo reporte vaya sin él. |
setMarkup(on, onExit?) | Enciende o apaga el dibujo sobre la pantalla. Ver Dibujar sobre la pantalla. |
recordingVideo | true mientras hay una grabación de pantalla en curso. |
microphoneOn | true mientras el micrófono aporta audio. |
Desde npm también tienes videoSupported(), que dice si el navegador puede
grabar la pantalla, y MAX_VIDEO_SECONDS (240):
import { videoSupported, MAX_VIDEO_SECONDS } from "@espejo/browser";
recordButton.hidden = !videoSupported();Necesita un clic
Sección titulada «Necesita un clic»startVideo() y el primer setMicrophone(true) abren un aviso del
navegador, y los navegadores solo lo permiten desde un gesto del usuario.
Llámalos directamente dentro de un handler de clic. Si se llaman fuera de un
clic, por ejemplo desde un temporizador, el navegador los rechaza.
Opciones de startVideo
Sección titulada «Opciones de startVideo»| Opción | Default | Significado |
|---|---|---|
microphone | false | Empezar con el micrófono encendido. Puedes cambiarlo después con setMicrophone(). |
surface | la opción videoSurface, o el selector normal | Qué ofrece primero el selector: current-tab (la pestaña actual sin selector, en Chrome), browser, window o monitor. Salvo current-tab, es una preferencia: la persona igual puede elegir otra cosa. |
onUserStopped | ninguno | Se llama cuando la persona deja de compartir desde la barra propia del navegador. |
onLimitReached | ninguno | Se llama cuando la grabación llega a su tope de tiempo y se detiene sola. |
maxSeconds | 240 | Segundos antes de que la grabación se detenga sola. Puedes pedir menos, nunca más de 240. |
maxHeight | 1080 | Alto máximo de la captura, en píxeles. Las pantallas más grandes se reescalan. |
frameRate | 24 | Cuadros por segundo. |
videoBitsPerSecond | 600000 | Bitrate del video. Subirlo llena antes el tope de 25 MiB del video. |
startVideo() se rechaza cuando la persona cierra el selector o lo niega, y
con This browser can't record the screen. o
This browser doesn't support MediaRecorder for webm. cuando el navegador no
puede grabar.
Cuando la grabación se detiene sola (la persona usó la barra del navegador, o
se llegó al tope de tiempo), recordingVideo sigue en true hasta que llamas
a stopVideo() o a report(). Llama a stopVideo() desde onUserStopped y
onLimitReached para que tu interfaz salga del estado de grabación.
Un flujo completo
Sección titulada «Un flujo completo»import { videoSupported } from "@espejo/browser";import { espejo } from "./espejo";
recordButton.hidden = !videoSupported();
recordButton.addEventListener("click", async () => { try { await espejo.startVideo({ onUserStopped: () => void finishRecording(), onLimitReached: () => void finishRecording(), }); } catch (error) { console.warn("Screen recording did not start", error); return; // la persona cerró el selector, o el navegador no puede grabar } showRecordingControls(); // tu interfaz: detener, micrófono, dibujar});
micButton.addEventListener("click", async () => { const { on, denied } = await espejo.setMicrophone(!espejo.microphoneOn); micButton.setAttribute("aria-pressed", String(on)); // denied: el navegador bloquea el micrófono para este sitio. Pedirlo otra vez no muestra nada. if (denied) micButton.disabled = true;});
stopButton.addEventListener("click", () => void finishRecording());
async function finishRecording() { await espejo.setMarkup(false); const video = await espejo.stopVideo(); showReportForm(video); // el formulario de la sección anterior; el video va con el próximo report()}
cancelButton.addEventListener("click", async () => { await espejo.setMarkup(false); await espejo.stopVideo(); espejo.discardVideo(); // no se envía nada hideRecordingControls();});stopVideo() te da el video como Blob, así que puedes dejar que la persona
lo vea antes de enviarlo, por ejemplo con URL.createObjectURL(video).
- El próximo
report()adjunta el video, y después lo descarta. - Si todavía hay una grabación en curso,
report()la detiene primero y la adjunta. discardVideo()solo descarta un video que ya se detuvo. Para cancelar una grabación en curso, llama antes astopVideo(), como en el ejemplo.- Un video que no envías ni descartas espera al próximo
report()de la página. Descártalo cuando la persona cierra tu formulario sin enviar. setMicrophone()no hace nada sin una grabación en curso y se resuelve con{ on: false, denied: false }.
Progreso de subida
Sección titulada «Progreso de subida»Un video puede tardar unos segundos en subir. Pasa onProgress como tercer
argumento de report(), con undefined como segundo:
await espejo.report(description, undefined, (fraction) => { status.textContent = `Enviando... ${Math.round(fraction * 100)}%`;});Solo se llama cuando el reporte lleva un video. Tómalo como información extra y no como señal de que la subida sigue viva: algunos navegadores lo llaman una sola vez en 100%, y detrás de algunos proxies no se llama nunca. Muestra “Enviando…” desde el principio y agrega el número cuando llegue.
Dibujar sobre la pantalla
Sección titulada «Dibujar sobre la pantalla»setMarkup(true, onExit) pone un canvas a pantalla completa sobre la página
para que la persona señale el problema mientras graba. Se resuelve con true
cuando el dibujo quedó encendido, y con false cuando no pudo arrancar. En
ese caso no pasa nada más y la grabación sigue. setMarkup(false) lo apaga y
se resuelve con false.
- Úsalo mientras grabas. El dibujo solo existe en el video de la pantalla: no se guarda en ningún otro lado y no produce una imagen.
- El canvas cubre toda la página, tu interfaz incluida, así que los clics no llegan a la página mientras está encendido. Espejo muestra su propia barra de herramientas abajo a la derecha: lápiz, rectángulo, cinco colores y deshacer. Los trazos se desvanecen unos segundos después del último movimiento.
- La persona sale con Escape, que llama a
onExit. Para ofrecer tu propio botón de salida, daleposition: fixedyz-index: 2147483647para que quede por encima del canvas, y llama asetMarkup(false)desde él. - La primera llamada descarga
https://app.espejo.dev/sdk/espejo.draw.js. Si tu sitio tiene una Content Security Policy, permite scripts dehttps://app.espejo.dev, osetMarkupse resuelve confalse. - Apágalo antes de detener la grabación, como en el ejemplo: detener el video no quita el canvas.
Quién reportó, y en qué versión
Sección titulada «Quién reportó, y en qué versión»Pasa un id opaco de tu usuario para que la grabación se pueda rastrear hasta esa persona. Nunca un correo ni un nombre: viaja a la vista en el navegador.
// Al iniciar sesiónespejo.identify("user-123");// Al cerrar sesiónespejo.identify(null);También puedes fijarlo desde el principio con la opción userRef o el
atributo data-user-ref. Las reglas y dónde se ve están en
Identificar al usuario.
release y env etiquetan cada grabación con la versión y el entorno de tu
app, para que sepas de qué build vino un bug:
const espejo = new Espejo({ key: "pk_live_...", button: "off", release: "2026.09.30", env: "production",});Con la etiqueta script, data-release y data-env.
Opciones del constructor
Sección titulada «Opciones del constructor»Las opciones que importan cuando reportas desde tu propia interfaz. Cada una tiene su atributo del script.
| Opción | Atributo | Default | Significado |
|---|---|---|---|
key | data-key | ninguno | La clave pública del proyecto (pk_live_...). Obligatoria. |
button | data-button | on | off no monta el botón flotante de reporte. No afecta la encuesta de feedback ni los mensajes en vivo. |
feedback | data-feedback | on | off suprime la encuesta de feedback en esta página. No puede encenderla: lo decide el proyecto en la consola. Ver Feedback. |
capture | data-capture | events | dom suma el replay del DOM. Con la etiqueta script, carga espejo.dom.js en su lugar. |
release | data-release | ninguno | La versión de tu app, visible en la grabación. |
env | data-env | ninguno | Tu entorno, por ejemplo production o staging. |
userRef | data-user-ref | ninguno | Un id opaco de tu usuario. Cámbialo después con identify(). |
videoSurface | data-video-surface | el selector normal | El surface por defecto de startVideo(). Un valor fijado para el proyecto en la consola tiene prioridad. |
autoReport | data-auto-report | on | off apaga los reportes automáticos en esta página. No puede encenderlos. Ver Reportes automáticos. |
buttonText, buttonIcon y position dan estilo al botón de Espejo, así que
no hacen nada con button: "off". buttonText es solo una opción del
constructor (no tiene atributo del script) y reemplaza los textos campo por
campo. Los textos por defecto están en español, por ejemplo:
new Espejo({ key: "pk_live_...", buttonText: { triggerLabel: "Report a problem", title: "Report a problem" },});Otros miembros que te pueden servir:
| Miembro | Qué hace |
|---|---|
start() | Empieza a grabar. |
stop() | Deja de capturar y quita el botón de reporte, la encuesta de feedback y los mensajes en vivo. No detiene una grabación de pantalla en curso: llama antes a stopVideo(). Después de stop(), report() devuelve null. |
recording | true entre start() y stop(). |
lastReportId | El id del último reporte que una persona envió en esta página, o null. Los reportes automáticos no lo cambian. |
snapshot() | Lo grabado hasta ahora, sin subir nada, o null cuando no se está grabando. Sirve para revisar qué mandaría un reporte. |
Errores y límites
Sección titulada «Errores y límites»report() se rechaza en estos casos. Captura el error, muestra a la persona
un mensaje genérico y registra el detalle con console.warn.
Cuando report() se rechaza no se consume nada: los eventos grabados, el
video y lastReportId quedan como estaban. Si reintentas, se envían los
mismos eventos y el mismo video, más lo que se grabó entretanto.
| Mensaje | Cuándo |
|---|---|
ingest rejected the session: HTTP <status> <body> | Espejo contestó con un error. El status y los primeros 200 caracteres de la respuesta van en el mensaje. |
ingest unreachable: the upload could not be completed | La subida con video y onProgress no pudo llegar a Espejo. |
the upload was cancelled | La subida con video y onProgress se canceló. |
ingest returned a body that is not JSON | La subida con video y onProgress recibió una respuesta que no es JSON. |
| El error de red propio del navegador | El pedido no pudo llegar a Espejo, sin onProgress. El texto depende del navegador. |
Los status que puedes recibir:
| Status | Causa | Qué hacer |
|---|---|---|
403 | Clave ausente, inválida o revocada. | Revisa key o data-key. |
403 | El origen de la página no está entre los orígenes permitidos del proyecto. La respuesta dice Origin <origin> is not allowed for this project. | Agrega el origen en la consola. Un proyecto sin orígenes permitidos acepta cualquiera. |
403 | El proyecto llegó a su límite diario de sesiones (Daily limit of <n> sessions reached.) o de bytes (Daily byte limit reached.). | Los límites se reinician a la medianoche UTC. Los reportes automáticos cuentan para los mismos límites. |
403 | El proyecto está pausado, o la cuenta está desactivada. | Revisa el proyecto en la consola. |
413 | La grabación o el video son demasiado grandes. | Ver los tamaños más abajo. |
503 | Espejo está recibiendo más de lo que puede sostener en este momento (manda Retry-After), o el almacenamiento del proyecto no está configurado. | Pide a la persona que pruebe de nuevo en unos segundos. |
Límites:
- Grabación: hasta 8 MiB por reporte, sin contar el video. El grabador guarda un buffer con tope en memoria y descarta primero los eventos más viejos.
- Video: hasta 25 MiB y 240 segundos. Con el bitrate por defecto, 240
segundos normalmente entran en ese tamaño. Un
videoBitsPerSecondmás alto puede pasarse. - Límites diarios: sesiones y bytes por proyecto por día, los mismos para los reportes desde tu interfaz, desde nuestro botón y los automáticos.
- Orígenes permitidos: cuando el proyecto tiene una lista, solo las páginas de esos orígenes pueden reportar.
Lo que todavía no está disponible
Sección titulada «Lo que todavía no está disponible»- Datos de contacto de quien reporta. No hay un campo para un correo o un
nombre. Usa
identify()con un id opaco y guarda el contacto de tu lado. - Campos propios o metadata. Un reporte lleva su descripción,
release,envyuserRef, y nada más que elijas tú. - Severidad o categoría. No hay un campo para eso. Si lo necesitas,
guárdalo en tu propio sistema junto a
lastReportId. - Adjuntos o capturas de pantalla. Un reporte no puede llevar archivos ni imágenes. El dibujo solo aparece en el video de la pantalla.
- Source maps. Los stack traces de la grabación se muestran tal como los informó el navegador. El código minificado no se traduce a tus fuentes.
- Reportar por HTTP. El reporte se hace con el SDK. No hay un contrato HTTP público para subir una grabación desde tu propio cliente.