Session replay for / Reproducing unreproducible bugs
Session replay for reproducing bugs you can't trigger
Someone reported a bug. You open the page and it works. You try another browser, another account, an incognito window. It still works. The report is a plot summary of what they think happened, and it is not enough to reproduce anything — you need what actually happened, not their memory of it.
What to watch for
- JS errors with no visible reaction An exception thrown and caught by an empty catch block produces zero telemetry and a user staring at a screen that silently did nothing. Error monitoring only sees what throws uncaught; this catches what a developer already decided not to handle.
- Requests that fail only for some sessions The same endpoint succeeding in 995 sessions and failing in 5 is not noise, it is the entire bug — the difference between those two groups (state, locale, a stale cache, an expired token) is the root cause, and it is invisible until you can see the 5 side by side.
- Long stalls immediately before a failure A race condition that loses under real-world latency shows up as a pause with no interaction, then an error, on a machine that is not yours. Your local network never produces this stall, which is exactly why the bug never reproduces at your desk.
- Sessions arriving by an unusual route A deep link, a back button after a redirect, a bookmarked URL from three releases ago — these put the app in a state your own manual testing never generates, because you always start from the front door.
- Repeated identical actions with no state change Someone doing the same thing five times in a row, each time expecting a different result, is the clearest transcript signature of “I don’t understand why this isn’t working” — and unlike a support ticket, it comes with an exact timestamp and the five attempts recorded.
What a session looks like
- 0:00 session start · /account/billing · viewport 1440×900 · Safari 17.4 - 0:03 click button "Update card" - 0:04 typed in a field (value masked at source) - 0:11 click button "Save" - ⚠ 0:12 POST /api/billing/card · 500 - 0:19 click button "Save" - ⚠ 0:20 POST /api/billing/card · 500 - 0:27 click button "Save" - ⚠ 0:28 POST /api/billing/card · 500 - ⚠ 0:34 rage click ×3 · button "Save" - 1:02 session end · total 1:02
“It works on my machine” is not a diagnosis, and neither is a bug report, because a bug report is a plot summary written by someone reconstructing what they remember doing, not a log of what they actually did. The gap between those two things is where irreproducible bugs live, and closing it is a matter of reading rather than guessing.
The six reasons a bug won’t reproduce
Almost every “works on my machine” bug comes from one of a handful of differences between your environment and theirs: state (their account has data or history yours doesn’t), timing (a race that only loses under real latency), environment (a browser, locale, or timezone your own testing never covers), interference (an ad blocker or extension rewriting the page), path (a deep link or a resubmitted form that put the app somewhere your manual testing never starts from), and scale (a query that’s fine with ten rows and times out at ten thousand).
Four of those six are invisible in conventional logs. A 500 shows up in error monitoring. A button that silently did nothing shows up nowhere, and neither does the sequence of actions that led someone to it.
Reconstruct the state, not the steps
The instinct when a bug report arrives is to follow the steps described. This usually fails, because a description is a reconstruction — people remember what they intended to do, not the exact sequence, and the two diverge exactly at the moment something went wrong for them.
A session recording sidesteps this entirely, because it isn’t a description — it’s the actual sequence, with the actual timing, on the actual browser. The transcript above is a real shape: three identical attempts to save a card, all failing the same way, on Safari specifically. Nobody had to remember that it was Safari. The recording already knew.
Reading across sessions, not just one
A single tape tells you what happened once. The more useful move, when you have several reports of “the same” bug, is comparing tapes side by side — what do the failing sessions share that the working ones don’t? In the example above, the answer was the browser. It’s just as often a plan tier, a feature flag, or an account created before a specific migration ran. The pattern only becomes visible once you can line several transcripts up next to each other, which is a search problem, not a single-recording problem.
Letting an agent do the comparing
This is where the exercise stops being manual. A coding agent connected over MCP can query tapes matching the report’s rough symptoms, read several transcripts, and correlate what’s common between the ones that failed — the kind of comparison that’s tedious for a person and mechanical for a model that’s already good at spotting a shared attribute across a list. It can then go open the file the bug is actually in, because it already has the repository loaded in the same conversation.
The deeper process — what to check before you even open a recording, and when the answer is “improve observability, not keep guessing” — is in how to reproduce a bug a user reported.
Asking your agent
$ claude "a user on Safari can't update their card — find out why" ▸ usertapes list_tapes(url_contains: "/billing", has_errors: true) ← 4 tapes this week · all POST /api/billing/card · 500 ▸ usertapes read_transcript(9f21c6e0) ← browser: Safari 17.4 · same 500 on every attempt, 3 attempts ▸ all 4 sessions on Safari; 0 on Chrome or Firefox with this error ▸ cause: card form uses a Chrome-only Payment Request API method, Safari falls through to a code path that never sets a required header
Over MCP, in Claude Code or Cursor — see session replay over MCP.
Installing it here
Add the snippet once, site-wide:
<script src="https://usertapes.com/tapes.js"
data-token="YOUR_SITE_TOKEN" async></script>
Two habits make this use case actually work, rather than just being theoretically possible:
Ask for the time and the page, not the story. When a bug report comes in, the two facts that narrow a search fast are when it happened and what URL it happened on. Both are things a reporter can actually answer, unlike “what did you click” — memory of steps is a reconstruction, and reconstructions are usually wrong in the one detail that mattered.
Set an alert on your highest-value flows — checkout, billing, signup — so the next irreproducible report already has a matching tape waiting before you go looking for it. See Alerts.
When this is the wrong tool
- Bugs you can already reproduce. If it fails locally, you don’t need a recording — you need a debugger, and reaching for replay first is slower than just running the repro steps you already have.
- Bugs about exact input values. Inputs are masked at the source, so if the failure depends on a specific string someone typed — a particular email format, a specific character — the transcript shows the field was filled and nothing more.
- Performance regressions across a whole user base. One tape shows you one slow session; it won’t tell you whether load times degraded 40% overall. Use real user monitoring for that question and replay for the individual case it flags.