Why this matters
Some bugs only make sense once you see the user’s screen. The stack tells you what crashed. The replay tells you what the user was trying to do. The latter is often the diagnostic difference:
- Stack:
TypeError: Cannot read property 'address' of null at BillingForm.tsx:118 - Replay: The customer clicked “Add address” 3 times, each time the modal closed without saving. They eventually gave up and clicked “Pay anyway” — boom, null address.
Now you know it’s a modal bug, not a billing-form bug. The fix isn’t a null-guard on line 118 — it’s making the address modal actually save.
Agentry’s replay strategies (off, errors_only, sampled, url_scoped, all) plus the per-distinct_id retrieval means you can leave replay off most of the time, only triggered around errors, and still get full context for the cases that matter.
What you get
- The customer’s most recent session that contained an error
- The 60-second pre-error timeline (clicks, navigations, form changes)
- A diagnostic narrative — agent reads the snapshots, tells you what likely went wrong
- A reproducible test case for the engineer to fix against
Walk through it
Customer [email protected] reported a checkout error this morning. Pull their session replay. Tell me what they did in the 60 seconds before the error and what likely caused it.
Three steps: find their recent error, find the replay that contains it, read the snapshots around the error timestamp.
agentry_get_distinct_id_summary {
project_id: "default",
distinct_id: "[email protected]",
window_hours: 24
}
{
"user": { "distinct_id": "[email protected]", "last_seen": 1747400400 },
"recent_events": [
{ "event": "page_view", "ts": 1747400100, "properties": { "path": "/checkout" } },
{ "event": "checkout_started", "ts": 1747400120 },
{ "event": "checkout_failed", "ts": 1747400180, "properties": { "reason": "address_missing" } }
],
"errors": [
{ "fingerprint": "f_npe_a", "message": "Cannot read properties of null (reading 'address')",
"occurred_at": 1747400180, "session_id": "s_paddy_42" }
]
}
Error at 17:13:00 in session s_paddy_42. Let me fetch the replay.
agentry_list_session_replays {
project_id: "default",
distinct_id: "[email protected]",
window_hours: 24
}
{
"replays": [
{
"session_id": "s_paddy_42",
"started_at": 1747399980,
"ended_at": 1747400195,
"duration_seconds": 215,
"events_count": 84,
"errors_count": 1
}
]
}
agentry_get_replay_snapshots {
session_id: "s_paddy_42",
from_ts: 1747400120,
to_ts: 1747400180
}
{
"snapshots": [
{ "ts": 1747400120, "type": "click", "target": "button[data-test=checkout]", "url": "/cart" },
{ "ts": 1747400122, "type": "navigation", "url": "/checkout" },
{ "ts": 1747400128, "type": "click", "target": "button[data-test=add-address]" },
{ "ts": 1747400129, "type": "dom_mutation", "summary": "modal AddressForm opened" },
{ "ts": 1747400141, "type": "input", "target": "input[name=line1]", "value_redacted": true },
{ "ts": 1747400156, "type": "click", "target": "button[data-test=save-address]" },
{ "ts": 1747400157, "type": "dom_mutation", "summary": "modal AddressForm closed" },
{ "ts": 1747400159, "type": "console", "level": "error", "text": "POST /api/address 500" },
{ "ts": 1747400164, "type": "click", "target": "button[data-test=add-address]" },
{ "ts": 1747400168, "type": "dom_mutation", "summary": "modal AddressForm opened (empty)" },
{ "ts": 1747400172, "type": "click", "target": "button[data-test=close-modal]" },
{ "ts": 1747400175, "type": "click", "target": "button[data-test=pay-now]" },
{ "ts": 1747400180, "type": "console", "level": "error", "text": "TypeError: Cannot read properties of null (reading 'address')" }
]
}
Here’s what really happened:
- 17:12:00 — paddy clicked checkout
- 17:12:08 — clicked “Add address,” modal opened
- 17:12:21 — filled in the address (line1 was the only field shown — that’s a clue)
- 17:12:36 — clicked “Save address.” Modal closed without confirmation.
- 17:12:39 — but a
console.errorfired:POST /api/address 500— the save actually failed silently - 17:12:44 — paddy thought “huh, did it save?” and clicked “Add address” again
- 17:12:48 — modal opened again, but empty (because nothing was saved)
- 17:12:52 — paddy closed the modal and clicked “Pay now,” figuring he’d handle billing after
- 17:13:00 — TypeError, because billing relies on address being set
The bug is NOT in BillingForm.tsx:118. It’s that the POST /api/address endpoint returned 500 and the front-end swallowed it — closed the modal anyway, no user-visible error. The TypeError that fired 21 seconds later is a downstream symptom.
Real fix: in the modal save handler, check the response status and show an error toast on non-2xx. Bonus: figure out why /api/address 500’d (separate investigation — probably a validation issue with paddy’s specific input).
Want me to write a regression test that exercises the “address save fails + user proceeds to pay” path?
The output
Session replay diagnosis — [email protected] — session s_paddy_42
Pre-error timeline (60s):
17:12:00 click [checkout]
17:12:08 click [add-address]
17:12:08 modal AddressForm opened
17:12:21 input address line1 filled
17:12:36 click [save-address]
17:12:36 modal AddressForm closed
17:12:39 CONSOLE POST /api/address 500 ★ silent failure!
17:12:44 click [add-address] (user retried)
17:12:48 modal AddressForm opened (empty) (because nothing saved)
17:12:52 click [close-modal]
17:12:55 click [pay-now] (gave up)
17:13:00 ERROR TypeError null.address ← stack tells you this
← replay tells you it's a SYMPTOM
True root cause: AddressForm.tsx silently swallows the 500 from /api/address.
BillingForm.tsx:118 null-check is the wrong fix.
The real fix is in the save handler.
Suggested:
1. AddressForm: show error toast on non-2xx response, don't close modal
2. Investigate why /api/address returned 500 (separate case)
3. Write a regression test: "save fails → user proceeds → expect blocking error"
Setting it up
Replay needs to be on. Default strategy is errors_only — replay only records sessions where an error occurred — which is the cheapest signal-to-cost ratio:
// Via MCP — once, at setup
agentry_configure_session_replay { strategy: "errors_only" }
Or via raw HTTP:
await fetch(
`https://api.agentry.sh/v1/projects/${PROJECT_ID}/posthog/session-replay/configure`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.AGENTRY_API_KEY}`,
"Content-Type": "application/json",
"User-Agent": "config/1.0", // REQUIRED — Cloudflare 403s default UAs
},
body: JSON.stringify({ strategy: "errors_only" }),
},
);
On the client side, your fetch helper sends session_id alongside each event:
// At app boot
const sessionId = crypto.randomUUID();
window.AGENTRY_SESSION_ID = sessionId;
// Each event includes it
await fetch(`https://api.agentry.sh/v1/analytics/${PROJECT_ID}/`, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.AGENTRY_DSN}`,
"Content-Type": "application/json",
"User-Agent": "myapp/1.0", // REQUIRED
},
body: JSON.stringify({
event: "checkout_started",
distinct_id: user.email,
session_id: window.AGENTRY_SESSION_ID,
properties: { /* ... */ },
}),
});
When the recording strategy is errors_only, the client buffers DOM events locally and only ships them up after a logged error in the same session. Privacy-friendly + bandwidth-friendly.
Other strategies:
off— no replaysampled— fixed % of all sessionsurl_scoped— only specific URL patterns (e.g./checkout/*)all— record everything (use during a specific bug hunt, then revert)
Variations
- “Pull the replays for everyone who hit
f_npe_ain the last 7 days. Is the pattern always ‘address save 500’?” - “Filter to mobile-Safari only — replays for the iOS users hitting this. Is the error platform-specific?”
- “Find sessions where the user clicked the same button 3+ times in 10s — those are usually rage clicks indicating broken UX.”
- “Compare 2 replays from different users hitting the same error — find the common DOM state.”