Network, with the bodies
fetch and XHR are patched from inside the page, so request and response bodies are actually there. No debugger banner, no extension, no half a log.
One script tag, and a bug report stops being a sentence. It arrives with the session attached: the network calls with their bodies, the console, every click, and the DOM replay or the screen video. Watch it yourself, or point an AI agent at it over MCP and let it read the whole thing.
Free plan, no card. 18 KB gzip on your page, and nothing uploads until someone reports a problem.
npm package - one import, full SDK
import { Espejo } from "@espejo/browser"; const espejo = new Espejo({ key: "pk_live_..." }); espejo.start();
Then, whenever a user hits something:
await espejo.report('Can’t save the order') // → the URL of the uploaded session
The replay, the network log, the console and the interactions share a timeline. A 500 stops being a line in a log and becomes the button that was pressed a second before it.
Connect Espejo tools and MCP to Claude, Codex, LangGraph or your own agent. It can guide users, act for them inside your product and automatically report a problem when an action needs assistance. Questions get solved in the moment, and your team only receives real issues that require a fix in the application.
Team members and their roles.
Espejo runs an MCP server in the same process as the API. Claude Code, claude.ai or any MCP client connects to POST /mcp over OAuth 2.1 with PKCE; a person approves it on a consent screen and picks which projects it may see; and from then on the agent has five read-only tools. It can find the session that failed and read what happened inside it without anyone opening a console.
Connect an agent
$ claude mcp add --transport http espejo https://app.espejo.dev/mcp
All five sit behind a single mcp:read scope. A tool with no entry in the policy table does not run, so adding a handler is never enough to expose one. The authority has to be declared too, in another file, on purpose.
The platform admin key sees every account, so it is refused at the MCP door. Every connection carries an account, a consent and a scope behind it.
Each connection is a grant you can see and revoke, with rotating refresh tokens and RFC 7009 revocation. Take the consent away and the agent is out.
The five tools
list_recordings The recordings this connection can see, newest first. Filter by project, by date range, by free text, or only the ones with errors.
get_recording Everything Espejo knows about one: when it ran, how long, on what page and release, its error counts, and what it stored.
recording_events What happened inside. The summary comes first: requests, 4xx/5xx with the click that caused them, and console errors. Then come network, console, interactions and navigations.
recording_frames Signed, short-lived links to what can be watched: the screen video, the rrweb DOM replay. Espejo URLs, never bucket URLs, and they expire.
recording_transcript The stored narration transcript of a recording, when it has one. It never starts a transcription. Espejo does not produce them yet.
Use the npm package when your app has a bundler. The hosted script remains available when you want a zero-build install.
By default nothing is uploaded. The script keeps a capped, in-memory ring buffer and sends that slice only when report() is called. data-mode="always" uploads every session, it exists, but it is not the default. That is the difference between something you can leave running in production and something that burns your bucket in a week.
npm package - one import, full SDK
import { Espejo } from "@espejo/browser"; const espejo = new Espejo({ key: "pk_live_..." }); espejo.start();
No bundler? Use the hosted script · Network with bodies, console, clicks and navigation, plus the report button and the feedback survey · 18 KB gzip
<script src="https://app.espejo.dev/sdk/espejo.js" data-key="pk_live_..."></script>
Everything above, plus a full visual replay of the DOM · that same DOM rebuild is what gives the heatmap its real-page background
<script src="https://app.espejo.dev/sdk/espejo.dom.js" data-key="pk_live_..."></script>
All of it captured from inside the page, so there is nothing to install and nothing to approve.
fetch and XHR are patched from inside the page, so request and response bodies are actually there. No debugger banner, no extension, no half a log.
Every log and error, every interaction described: what was clicked, never what was typed, and every URL change, on the same clock as everything else.
rrweb reconstructs the page exactly as it was. Or record the real screen, with the microphone, from a click handler. A user gesture is required, so it is a button in your UI.
Accounts, projects, and a public key per project that goes straight into your HTML, the way a Sentry DSN does. The bucket path is always built by the server, never proposed by the browser.
The default is our bucket, so a project starts in one click. Keeping recordings in your own bucket, with your own credential, is coming with the Team plan. It is for whoever needs recordings never to leave their account. R2 and S3.
A floating “report a problem” button mounts itself unless you turn it off with data-button="off". Or call report() from your own UI and get the session URL back.
Every click is captured with no config and painted over the page rebuilt from the DOM · not a screenshot, the real markup styled the way your user saw it. Where a screen gets pressed, and where nobody ever touches it. A route with no snapshot yet falls back to a neutral wireframe.
Describe a pattern once: reached the cart and never bought, clicked the same spot five times, or started a form and never sent it. Espejo counts every session that matches and links you straight to those recordings. Start from a template or build your own; each screen is grouped by normalized route, so /order/123 and /order/456 are the same page.
The signals are gathered on every plan, nothing is lost while you're free. Defining behaviors and seeing their counts and segments is Pro; the template catalog is visible to everyone.
A tiny survey · faces, a 1 to 7 scale or thumbs · pops once per visitor, and a single tap lands a vote in a Satisfaction dashboard. Off by default, and a separate thing from the report button: this is a vote, not a recording.
And tie every vote to one of your users. One attribute or a single identify() call tells Espejo which of your users a vote or a session belongs to, so you read feedback and recordings per person. It is an opaque id from your own database, never an email or PII.
The capture is plain JavaScript running inside your page: patched fetch and XHR, the console, the event listeners, the History API. It runs wherever the page runs, so a session records its network, its console and every tap the same on an iPhone as on Android. Clicks are taps, nothing else changes: the same recording, the same five MCP tools, the same automatic reports.
The one desktop-only piece is the screen video. Recording a screen needs getDisplayMedia and a user gesture, and no mobile browser exposes it: not Safari on iOS, not Chrome on Android. On a phone you keep everything else: network with bodies, console, taps, navigation and the DOM replay.
The best bug report is the one nobody had to write. Turn it on per project and Espejo uploads the buffer the moment your app breaks, while the user is still on the page, whether or not they ever tell you.
What happens, in order
An uncaught exception, a rejected promise nobody caught, a 500 from your API. The SDK is already watching all of it. The hooks are the same ones that fill the recording.
Same upload path as the report button, so there is one way in and not two. Never with video: recording a screen needs a user gesture, and there is nobody here to give one. DOM replay does travel, if you loaded the dom bundle.
The replay link, the trigger, the error excerpt and the counts, signed with HMAC-SHA256 in X-Espejo-Signature so your receiver can prove it came from us. 5s timeout, retried with backoff, and the last result is on the project page.
The recording is already readable by an agent: network with bodies, console, clicks, DOM. A webhook that hands over a replay link plus an MCP server that can read it is the raw material for self-healing pipelines. The pipeline is yours to build. Espejo records and reports; it does not repair.
What can trip it
auto:uncaught window.onerror, the ones that break a screen
auto:unhandled_rejection a rejected promise nobody handled
auto:console_error console.error, for teams that use it to mean something
auto:network_5xx your own API answered 500
auto:network_failed the request never left: offline, CORS, aborted
auto:network_4xx off by default. A 401 or a missing avatar is usually noise
Why a render loop can't bankrupt you
A component looping at 60fps throws 3,600 errors a minute. Three uploads per page load is the default cap, there is a 60-second cooldown between them, and the same error signature never uploads twice. All three, because each one stops a different shape of flood.
Automatic reports draw on the project's normal daily caps. There is no privileged channel, so the worst case is bounded by a number you already set. The pause that protects it is the same one.
The SDK asks the server what is on. A script tag can turn it off (data-auto-report="off"), never on. Your public key is public, so an attribute that could enable it would let anyone spend your quota. If the config request fails, automatic reports stay off and the SDK keeps working.
A recording ends up in object storage and in the context window of a model. A secret that leaks here is not something you delete later, so the redactor runs in the browser, before anything is sent.
Authorization, Cookie, Set-Cookie, x-api-key and proxy-authorization, plus any header whose name carries “token” or “secret”, which catches the ones nobody listed.
Query and fragment values named like a token, password, signature, code, key or credential; user:pass@ in the host; and anything JWT-shaped, recognised by its form even sitting unnamed in the middle of a path.
An input records that it changed, never to what. In the DOM replay every input is masked, not just the password ones. A card number in a plain text field is not less private for not being a password.
data-espejo-block leaves a hole in the replay and silences the clicks inside it. data-espejo-mask keeps the shape and hides the text.
Redaction runs before truncation, on purpose: a JSON cut in half no longer parses, and a redactor that cannot parse cannot redact.
One free plan with the whole product in it, and caps instead of a trial clock.
Free
$0
forever, no card
Everything on this page. The MCP server included.
Whichever cap you hit first evicts the oldest recording. Three caps and not one because each one stops a different kind of overflow: days catch whoever records little and keeps it forever, count catches whoever records a lot and short, minutes catch whoever records rarely and long.
Team
$9
per month, 2 seats included
Higher caps, your own bucket, more people in the account.
Billed per seat, monthly or annual: you pay for the people in the account, not for a plan tier you outgrow.
Create a project, paste the script tag, reproduce the bug. Then ask your agent what happened.