Session replay your AI agent can read

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

One recording, four tracks, one clock

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 · session 4f2a…c81
Network Console Clicks
  • 0:04 click, “Save”
  • 0:04 POST /v1/orders, 500
  • 0:04 Uncaught TypeError
  • 0:06 GET /v1/session, 200
0:00 Replay 0:09
Schematic of the replay view, an illustration, not a recording.

Give your agent the tools to solve, not just answer

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.

The assistant is using this page
Workspace

Team

Team members and their roles.

+ Invite
☷ MemberRole ⌄StatusMember since
DennisOwneractive22/7/2026
Ana PérezAdminactive28/7/2026
Martín SilvaMemberactive13/8/2026
OwnerAdminMember
AI

Your agent opens the recording. Not you.

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

Read-only, by policy and not by habit

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.

No machine key gets in

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.

Revocable from the console

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.

How the MCP server works →

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.

Install with the SDK

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>

What lands in the recording

All of it captured from inside the page, so there is nothing to install and nothing to approve.

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.

Console, clicks and navigation

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.

DOM replay or screen video

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.

Multi-tenant from the first table

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.

Our bucket, or yours

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 report button, already there

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.

Clicks become a heatmap on the real page

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.

espejo · heatmap /checkout
fewer clicks more clicks
Schematic of the heatmap view · an illustration, not real click data.

Turn sessions into behaviors

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.

Ask your users how it went, in one tap

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.

espejo · feedback scale7
Satisfaction 4.6 / 7 How was it? 1 2 3 4 5 6 7 Thanks!
Schematic of the CSAT survey · an illustration, not real votes.

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.

It records on the phone, too

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.

Bugs that report themselves

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

  1. Your app throws

    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.

  2. The buffer goes up, with no video

    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.

  3. Your endpoint gets a signed POST

    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.

  4. Point your AI agent at it over MCP

    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.

Read the auto-report docs →

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

Three per page load, and that's the ceiling

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.

It spends the same budget as everything else

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 project decides, not the page

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.

What never reaches the bucket

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.

  • Auth headers

    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.

  • Secrets inside URLs

    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.

  • Whatever people type

    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.

  • Whole regions, when you say so

    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.

Pricing

One free plan with the whole product in it, and caps instead of a trial clock.

Team

$9

per month, 2 seats included

Higher caps, your own bucket, more people in the account.

  • Your own R2 or S3 bucket
  • Signed webhook for automatic reports
  • Raised caps and retention
  • $3/mo per extra seat, change it anytime
  • Annual billing, about two months free
  • PayPal or Mercado Pago
Read the docs

Billed per seat, monthly or annual: you pay for the people in the account, not for a plan tier you outgrow.

Put it on a page and break something

Create a project, paste the script tag, reproduce the bug. Then ask your agent what happened.