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. 6 KB gzip on your page, and nothing uploads until someone reports a problem.
network (with bodies), console, clicks, navigation — 6 KB gzip
<script src="https://app.espejo.dev/sdk/espejo.js" data-key="pk_live_..."></script>
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.
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 first — requests, 4xx/5xx with the click that caused them, console errors — then 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.
Two bundles exist. Pick one — that is the whole integration.
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.
network (with bodies), console, clicks, navigation — 6 KB gzip
<script src="https://app.espejo.dev/sdk/espejo.js" data-key="pk_live_..."></script>
everything above, plus DOM replay via rrweb — 86 KB gzip
<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. Pointing it at your own bucket with your own credential is a setting, for whoever needs recordings never to leave their account. R2 today, S3 next.
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.
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
Coming soon
Higher caps, your own bucket, more people in the account.
Not priced yet. We would rather say nothing than put a number here we have not decided.
Create a project, paste the script tag, reproduce the bug. Then ask your agent what happened.