Skip to content

fix(replay): Recover when rrweb recording silently stops#21409

Draft
billyvg wants to merge 1 commit into
developfrom
fix/replay-recording-resilience
Draft

fix(replay): Recover when rrweb recording silently stops#21409
billyvg wants to merge 1 commit into
developfrom
fix/replay-recording-resilience

Conversation

@billyvg

@billyvg billyvg commented Jun 9, 2026

Copy link
Copy Markdown
Member

Summary

rrweb session-replay recording could stop entirely while the rest of the Replay integration kept working — breadcrumbs, network, and console events continued and segments kept flushing, so it looked like "recording just froze." This makes recording resilient so a single transient failure can no longer permanently kill it.

Root cause

Every rrweb event type flows through the single getHandleRecordingEmit callback, which had no try/catch. Because Sentry's rrweb errorHandler returns undefined, rrweb re-throws any error that escapes the callback (callbackWrapper), which can tear down recording or leave the mutation buffer permanently locked.

Separately, the buffer→session conversion (sendBufferedReplayOrFlush) tears down rrweb and restarts it. If that restart failed — rrweb's own record() swallows internal errors and returns undefined, and startRecording() swallowed throws — _stopRecording was left unset with no recovery while _isEnabled stayed true. Result: all rrweb stops, breadcrumbs keep flowing.

Changes

  • handleRecordingEmit.ts: wrap the emit callback body in try/catchhandleException; never let an exception escape back into rrweb.
  • replay.ts startRecording(): detect a missing recorder (record() threw or returned undefined), surface it, and track a bounded failure count.
  • replay.ts: add _ensureRecordingIsRunning(), a flush-time watchdog that restarts a recorder which died while the integration is enabled and unpaused (bounded to avoid hot-looping).

Includes unit + integration tests, including a real-rrweb reproduction, covering the escape, the recovery, and the retry bound.

Follow-up

A separate fix in the @sentry-internal/rrweb fork (wrap takeFullSnapshot's lock/unlock in try/finally, and unlock on the !node early-return) will close the "stuck locked" hazard at the source.

🤖 Generated with Claude Code

rrweb recording could die while the rest of the Replay integration kept
working (breadcrumbs/network/console continued and segments kept flushing),
so it looked like recording just froze.

Every rrweb event flows through the `getHandleRecordingEmit` callback, which
had no try/catch. Because our rrweb `errorHandler` returns `undefined`, rrweb
re-throws anything that escapes the callback and can tear down recording or
leave the mutation buffer permanently locked. Separately, the buffer->session
conversion tears down rrweb and restarts it; if that restart failed (rrweb's
`record()` can throw or silently return `undefined`), `_stopRecording` was left
unset with no recovery while the integration stayed enabled.

- Wrap the emit callback so an exception can never escape back into rrweb.
- Detect a missing recorder after `startRecording()` and surface it.
- Add a bounded flush-time watchdog that restarts a recorder which died while
  the integration is still enabled and unpaused.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 27.39 kB - -
@sentry/browser - with treeshaking flags 25.82 kB - -
@sentry/browser (incl. Tracing) 45.68 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 47.92 kB - -
@sentry/browser (incl. Tracing, Profiling) 50.48 kB - -
@sentry/browser (incl. Tracing, Replay) 85.03 kB +0.15% +126 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 74.6 kB +0.13% +90 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 89.72 kB +0.14% +125 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 102.4 kB +0.12% +117 B 🔺
@sentry/browser (incl. Feedback) 44.55 kB - -
@sentry/browser (incl. sendFeedback) 32.19 kB - -
@sentry/browser (incl. FeedbackAsync) 37.3 kB - -
@sentry/browser (incl. Metrics) 28.46 kB - -
@sentry/browser (incl. Logs) 28.69 kB - -
@sentry/browser (incl. Metrics & Logs) 29.39 kB - -
@sentry/react 29.18 kB - -
@sentry/react (incl. Tracing) 47.98 kB - -
@sentry/vue 32.4 kB - -
@sentry/vue (incl. Tracing) 47.57 kB - -
@sentry/svelte 27.41 kB - -
CDN Bundle 29.78 kB - -
CDN Bundle (incl. Tracing) 48.16 kB - -
CDN Bundle (incl. Logs, Metrics) 31.32 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 49.48 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 70.68 kB +0.11% +71 B 🔺
CDN Bundle (incl. Tracing, Replay) 85.56 kB +0.07% +55 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 86.81 kB +0.07% +57 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 91.4 kB +0.07% +56 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 92.65 kB +0.07% +57 B 🔺
CDN Bundle - uncompressed 88.46 kB - -
CDN Bundle (incl. Tracing) - uncompressed 145.67 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 93.17 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 149.65 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 218.21 kB +0.11% +220 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed 264.76 kB +0.09% +220 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 268.72 kB +0.09% +220 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 278.46 kB +0.08% +220 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 282.41 kB +0.08% +220 B 🔺
@sentry/nextjs (client) 50.43 kB - -
@sentry/sveltekit (client) 46.1 kB - -
@sentry/core/server 76.01 kB - -
@sentry/core/browser 63.14 kB - -
@sentry/node-core 61.72 kB -0.01% -3 B 🔽
@sentry/node 130.4 kB +0.01% +1 B 🔺
@sentry/node - without tracing 74.11 kB - -
@sentry/aws-serverless 86.29 kB +0.01% +1 B 🔺
@sentry/cloudflare (withSentry) - minified 173.69 kB - -
@sentry/cloudflare (withSentry) 433.85 kB - -

View base workflow run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant