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 paddy@acme.com 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.
GET /v1/projects/:project_id/users/paddy%40acme.com/summary
Authorization: Bearer $AGENTRY_PRIVATE_API_KEY
{
"distinct_id": "paddy@acme.com",
"event_stats": {
"count": 17,
"first_seen": "2026-05-16T16:50:00Z",
"last_seen": "2026-05-16T17:13:05Z"
},
"recent_events": [
{ "event": "page_view", "timestamp": "2026-05-16T17:12:00Z" },
{ "event": "checkout_started", "timestamp": "2026-05-16T17:12:20Z" },
{ "event": "checkout_failed", "timestamp": "2026-05-16T17:13:00Z" }
],
"recent_recordings": [
{ "id": "s_paddy_42", "started_at": "2026-05-16T17:09:45Z" }
]
}
Error at 17:13:00 in session s_paddy_42. Let me fetch the replay.
GET /v1/projects/:project_id/session-replays?distinct_id=paddy%40acme.com&date_from=2026-05-16T16:45:00Z&date_to=2026-05-16T17:20:00Z
Authorization: Bearer $AGENTRY_PRIVATE_API_KEY
{
"recordings": [
{
"id": "s_paddy_42",
"distinct_id": "paddy@acme.com",
"started_at": "2026-05-16T17:09:45Z"
}
],
"has_next": false
}
GET /v1/projects/:project_id/session-replays/s_paddy_42/snapshots
Authorization: Bearer $AGENTRY_PRIVATE_API_KEY
{
"snapshots": [
{ "type": 2, "timestamp": 1778951340000 },
{ "type": 3, "timestamp": 1778951356000, "data": { "source": 2 } },
{ "type": 3, "timestamp": 1778951379000, "data": { "source": 6 } }
],
"next_action": "Snapshots are rrweb-format DOM events..."
}
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 — paddy@acme.com — 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 two pieces:
- Turn on the project’s replay strategy in Agentry/PostHog.
- Load the browser recorder (
posthog-js) on the pages you want to inspect.
Default strategy is errors_only — replay only records sessions where an error occurred — which is the cheapest signal-to-cost ratio:
// Once, at setup
// Endpoint: POST /v1/projects/:project_id/posthog/session-replay/configure
// Exact request shape: fetch the PostHog tag from /v1/openapi.json before sending.
// Concept field: 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_PRIVATE_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/`, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.AGENTRY_PUBLIC_API_KEY}`,
"Content-Type": "application/json",
},
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)
For the Agentry website itself, the recorder is wired in
apps/web/src/lib/agentry-client.ts. The browser only needs the same
PUBLIC_AGENTRY_PUBLIC_API_KEY used for /v1/logs/ and /v1/analytics/; the helper
fetches replay bootstrap config from GET /v1/analytics/browser-config.
The WorkOS-hosted sign-in page cannot be replayed because Agentry’s JavaScript
does not run on api.workos.com / WorkOS-hosted domains. Record the Agentry
pages before and after the hosted redirect, then correlate by distinct_id,
$session_id, and the signup events.
Weekly onboarding drop-off review
For prompts like “fetch my session replays that get to onboarding but don’t finish it and analyze them once a week”, use a scheduled agent or cron job. Agentry provides the API; the schedule runs wherever you run automation.
For Agentry’s own prompt-to-signup funnel, use:
entry_event:install_prompt_copiedcompletion_event:signup_completed
signup_started fires immediately before redirecting to WorkOS; signup_completed
fires in the API after the device code is authorized. The browser helper mirrors
the anonymous id and session id into cookies so those server-side events can be
joined back to the pre-signup replay candidate.
First, get a bounded candidate set:
POST /v1/projects/:project_id/query-blueprints/funnel_dropoff_users/run
Authorization: Bearer $AGENTRY_PRIVATE_API_KEY
Content-Type: application/json
{
"params": {
"entry_event": "onboarding_started",
"completion_event": "onboarding_completed",
"days": 7,
"limit": 25
}
}
Then, for each returned distinct_id, fetch recordings around entry_at:
GET /v1/projects/:project_id/session-replays?distinct_id=user_123&date_from=2026-06-15T00:00:00Z&date_to=2026-06-22T00:00:00Z
Authorization: Bearer $AGENTRY_PRIVATE_API_KEY
If the drop-off row includes session_recording_id, fetch that replay directly:
GET /v1/projects/:project_id/session-replays/:replay_id/snapshots
Authorization: Bearer $AGENTRY_PRIVATE_API_KEY
The weekly job should summarize patterns across the sampled replays: repeated clicks, navigation loops, missing form validation, console errors, or places where users abandon after the same UI state. Agentry’s /v1/docs/automation documents the cron-style weekly digest pattern; no webhook is needed for a weekly pull.
Variations
- “Pull the replays for everyone who hit
f_npe_ain the last 7 days. Is the pattern always ‘address save 500’?” - “Every Monday, sample 25 users who fired
onboarding_startedbut notonboarding_completed, fetch their replays, and summarize the top three friction patterns.” - “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.”