Skip to content

feat(flutter): Add experimental Session Replay for Flutter Web - #4059

Open
lucas-zimerman wants to merge 55 commits into
mainfrom
lz/replay/web-mask
Open

lucas-zimerman wants to merge 55 commits into
mainfrom
lz/replay/web-mask

Conversation

@lucas-zimerman

@lucas-zimerman lucas-zimerman commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

📜 Description

Adds experimental Session Replay for Flutter Web, with the same widget-level masking as mobile. It's opt-in through options.replay.enableWebReplay (@experimental, off by default):

options.replay.enableWebReplay = true;
options.replay.sessionSampleRate = 1.0; // and/or onErrorSampleRate

The JS SDK's canvas recording can't mask anything: it screenshots the whole <canvas>, and blocking is all-or-nothing per element. So instead:

  • Flutter's ScreenshotRecorder captures a masked frame (same masking pipeline as mobile).
  • SentryWebReplayRecorder draws it onto a hidden "shadow" canvas (#sentry-flutter-replay-canvas) placed behind the Flutter view.
  • The JS SDK's replayCanvasIntegration runs with enableManualSnapshot: true and snapshots only that shadow canvas, when Dart says a frame is ready.
  • Flutter's real canvas is blocked via block: ['canvas:not(#sentry-flutter-replay-canvas)']. It has to be a selector because the canvas lives in flt-glass-pane's shadow DOM, where tagging elements doesn't work.

Other changes:

  • ReplayIntegration is registered on web and drives the recorder through setReplayConfig. On web, its send-time hook leaves error events to the JS SDK, which sends buffered error replays itself. For feedback, captureReplay() flushes the active JS replay, as the JS SDK's own feedback integration does, because the JS SDK only sends a buffered replay for errors and transactions.
  • WebReplayEventProcessor adds the JS replay's ID to Dart events (replayId tag and trace context). Dart events reach the JS SDK as raw envelopes, which skip the JS event processors that would add it.
  • The recorder skips captures while the JS replay isn't recording (an unsampled session, or one the JS SDK stopped itself on session expiry or max duration), so those users don't pay for capture and masking.
  • Scope sync on web: NativeScopeObserver is now registered on web, so the Dart user, tags, extras, contexts and breadcrumbs are mirrored into the JS SDK. JS-captured errors now carry the Dart scope too. The JS calls are guarded, so a JS failure can't surface in Sentry.addBreadcrumb or configureScope. The JS SDK also gets httpContextIntegration (URL, user agent and referrer on JS events).
  • Replay quality maps to a web capture scale (low 0.5x, medium 0.7x, high 1.0x). The shadow canvas's backing buffer is always CSS size × dpr: Sentry's player (canvasReplayerPlugin) draws each recorded canvas into a copy made on its first draw and shows that copy's backing buffer as an image, so the buffer size is what viewers see.
  • ScreenshotRecorder.yieldToEventLoop() is a new hook that is a no-op everywhere except web. There it yields between capture phases so a capture doesn't block the browser's main thread.
  • SentryFlutter.replay controls work on web: start, startBuffering, stop and flush drive the JS SDK's replay and the Flutter recorder together. A stopped replay stays stopped across config changes such as a resize. pause/resume are no-ops on web, because the JS SDK can't pause a replay.
  • Resize and rotation:
    • Config changes no longer restart the capture scheduler on web, so the shadow canvas is resized at the capture rate, not on every frame of a window drag.
    • Once the shadow canvas has been recorded, a size change replaces the element instead of resizing it. The player's copy of a canvas never picks up later size changes, which showed up as content squished to one side and leftover duplicated widgets. A new element gets a fresh copy at the new size.
  • Error-only replays (onErrorSampleRate) work for Dart errors. Dart events reach the JS SDK through client.sendEnvelope(), which skips the afterSendEvent hook that Replay uses to turn a buffering replay into a session, so only JS errors used to trigger it. The binding now emits afterSendEvent for the events in each envelope once it's sent.
  • The JS SDK's body > flutter-view click breadcrumbs are dropped from replays. They can't say what was tapped; the Dart flutter.ui.click breadcrumb does.

Builds on @buenaflor's web canvas replay work (first two commits).

Opt-in: enableWebReplay

Web replay only starts when options.replay.enableWebReplay is true and replay.sessionSampleRate or replay.onErrorSampleRate is above 0. The flag is marked @experimental: its API and behavior may change in a minor release. Recording also needs the CanvasKit or Skwasm renderer; with any other renderer the replay bundle isn't loaded and nothing records.

Apps that already set a replay sample rate for mobile are unaffected on web until they set the flag: no replay JS bundle is loaded and nothing records. In that case init logs a debug message saying how to enable it. Native builds are unaffected: all web replay code sits behind dart.library.js_interop conditional exports.

💡 Motivation and Context

Closes #2897

💚 How did you test it?

Sample Replay: https://sentry-sdks.sentry.io/explore/replays/0596cac52a994bdcae6eb2f2d6be33f8/?playlistEnd=2026-10-20T22%3A15%3A00&playlistStart=2026-10-13T22%3A15%3A00&project=5428562&query=&referrer=replayList

  • Unit tests for the recorder, the JS-free bridge interface, the click filter, capture scale, SentryWeb and WebSdkIntegration.
  • Browser tests against the real DOM bridge: flutter test --platform chrome test/web/ (148 passing). Full VM suite passing.
  • enableWebReplay on and off against the built example app in headless Chromium: with the flag on, the replay records in session mode and uploads replay_event/replay_recording; with it off (same sample rates), there's no replay ID, no shadow canvas and no replay envelopes.
  • End to end against the built example app (flutter build web), by intercepting the replay envelopes with Playwright and decoding them:
    • Flutter's real canvas is serialized as a blocked placeholder (rr_width/rr_height, no rr_dataURL).
    • The shadow canvas is recorded via canvas mutations, and decoded frames show the masked app.
    • Resize and dpr=2 keep the backing buffer at CSS × dpr.
  • Resize and rotation against real Firefox replays (fetched through the Sentry API) and a local harness that drags the window in headless Edge and checks the recorded events: after the fixes, no canvas changes size after its first draw.
  • stop/start/flush against the real JS SDK in Chrome.
  • Error-only replays end to end: an example build with only onErrorSampleRate set and a Dart error, with Sentry requests answered locally. Before the fix the replay stayed in buffer with no upload; after it, it switches to session and uploads replay_event/replay_recording (3/3 runs).
  • Manual playback checked in the Sentry dashboard during development.

📝 Checklist

  • I reviewed submitted code
  • I added tests to verify changes
  • No new PII added or SDK only sends newly added PII if sendDefaultPii is enabled
  • I updated the docs if needed
  • All tests passing
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec
  • No breaking changes

🔮 Next steps

  • Semantics DOM: flt-semantics-host / flt-text-editing-host show up in the recording. They rely only on the JS SDK's default text/input masking and aren't explicitly blocked; this needs a decision.
  • Frame rate: web is limited by the JS SDK's quality presets for sampling.canvas (1/2/4 fps), even for manual snapshots. It needs a deliberate setting.
  • Known limitation, resizing: while the window is being resized, the canvas can lag the viewport by up to the capture interval (about 1s), leaving parts of the page empty until the next capture.
  • Edge cases: multiple Flutter views and platform views (HtmlElementView). Cross-origin iframes can't be captured at all; that's a browser limit.
  • Network tab: Dart HTTP requests show up as timeline breadcrumbs but not in the replay's Network tab. That will be done in a follow-up PR.
  • Shadow canvas visibility: the shadow canvas sits behind the Flutter view (z-index: -1). An app with a transparent background or a custom host element could show it. Hiding it needs a browser check that rrweb still records it.
  • Docs.

Changelog Entry

  • Add experimental Session Replay support for Flutter Web with widget masking. Opt in with options.replay.enableWebReplay = true along with replay.sessionSampleRate or replay.onErrorSampleRate. Requires the CanvasKit or Skwasm renderer. SentryFlutter.replay start/stop/flush also work on web, and the Dart scope (user, tags, extras, contexts, breadcrumbs) now syncs to the JS SDK on web (#2897)

buenaflor and others added 3 commits October 1, 2026 20:40
Enable an internal Flutter Web option for JS Replay canvas recording
and attach the active replay ID to Dart events sent through the web
transport.

Co-Authored-By: GPT-5.5 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Mirror Dart scope mutations into the browser JS SDK on web so native
JavaScript events receive Flutter scope data.

Emit replay-compatible breadcrumbs for Flutter cases that the JS replay
bundle filters from its generic breadcrumb handler.

Co-Authored-By: GPT-5.5 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Capture masked frames with the Flutter screenshot recorder and draw
them onto a shadow canvas that the JS SDK's replay canvas integration
snapshots manually. Flutter's own canvas is blocked by selector, since
it lives in flt-glass-pane's shadow DOM.

- Register ReplayIntegration on web; the JS SDK handles error mode
- Map replay quality to a web capture scale (0.5x/0.7x/1.0x)
- Drop the JS SDK's flutter-view click breadcrumbs in replays
- Enable web replay from the replay sample rates instead of the
  internal enableWebCanvasRecording flag
- Add a replay playground screen to the example app

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


Features

  • Add experimental Session Replay support for Flutter Web with widget masking. Opt in with options.replay.enableWebReplay = true along with replay.sessionSampleRate or replay.onErrorSampleRate. Requires the CanvasKit or Skwasm renderer. SentryFlutter.replay start/stop/flush also work on web, and the Dart scope (user, tags, extras, contexts, breadcrumbs) now syncs to the JS SDK on web (#2897) by lucas-zimerman, buenaflor and 3 others with sentry-junior in #4059

Fixes

  • (flutter) Use static SwiftPM binaries by philprime and lucas-zimerman in #4118

Dependencies

Deps

  • chore(deps): update Cocoa SDK to v9.32.0 by github-actions in #4128
  • chore(deps): update Native SDK to v0.17.5 by github-actions in #4127
  • chore(deps): update Android SDK to v8.62.0 by github-actions in #4124

Internal Changes

  • (flutter) Use Cocoa internal serializer by philprime, buenaflor and christophaigner in #4100
  • Use dark-mode-aware Sentry logo in READMEs by szokeasaurusrex with sentry-junior in #4123

🤖 This preview updates automatically when you update the PR.

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

🚨 Detected changes in high risk code 🚨

High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:

  • packages/flutter/lib/src/screenshot/recorder.dart

@codecov

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.29412% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.08%. Comparing base (f8991b2) to head (13bfebd).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...lib/src/web/replay/sentry_web_replay_recorder.dart 92.68% 3 Missing ⚠️
...ckages/flutter/lib/src/native/c/sentry_native.dart 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4059      +/-   ##
==========================================
+ Coverage   88.81%   93.08%   +4.26%     
==========================================
  Files         348      118     -230     
  Lines       12631     4713    -7918     
==========================================
- Hits        11218     4387    -6831     
+ Misses       1413      326    -1087     
Flag Coverage Δ
sentry ?
sentry_dio ?
sentry_drift ?
sentry_file ?
sentry_firebase_remote_config 100.00% <ø> (ø)
sentry_flutter 92.81% <95.29%> (+0.12%) ⬆️
sentry_grpc ?
sentry_hive ?
sentry_isar ?
sentry_link ?
sentry_logging ?
sentry_sqflite ?
sentry_supabase 97.48% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

iOS Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1249.41 ms 1250.46 ms 1.04 ms
Size 5.91 MiB 6.35 MiB 450.72 KiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
5367726 1211.00 ms 1216.73 ms 5.73 ms
c492ba7 1248.33 ms 1258.42 ms 10.09 ms
7b21e8b 1256.79 ms 1267.12 ms 10.33 ms
827bf09 1261.86 ms 1276.41 ms 14.55 ms
0bea8d9 1234.39 ms 1239.59 ms 5.20 ms
819c1e7 1250.59 ms 1249.08 ms -1.51 ms
e04b24b 1230.22 ms 1233.90 ms 3.67 ms
3801d52 1267.76 ms 1266.10 ms -1.65 ms
af26bcd 1243.98 ms 1243.49 ms -0.49 ms
98188da 1252.31 ms 1251.41 ms -0.90 ms

App size

Revision Plain With Sentry Diff
5367726 5.73 MiB 6.17 MiB 455.87 KiB
c492ba7 5.91 MiB 6.35 MiB 449.92 KiB
7b21e8b 5.53 MiB 6.00 MiB 479.96 KiB
827bf09 7.86 MiB 9.44 MiB 1.58 MiB
0bea8d9 5.73 MiB 6.18 MiB 463.52 KiB
819c1e7 5.53 MiB 6.00 MiB 479.96 KiB
e04b24b 5.53 MiB 6.00 MiB 480.00 KiB
3801d52 5.73 MiB 6.17 MiB 455.54 KiB
af26bcd 5.83 MiB 6.28 MiB 460.44 KiB
98188da 5.83 MiB 6.28 MiB 459.62 KiB

Previous results on branch: lz/replay/web-mask

Startup times

Revision Plain With Sentry Diff
e749de6 1253.47 ms 1263.08 ms 9.61 ms
c5b01dc 1247.21 ms 1251.60 ms 4.40 ms
fd971e7 1255.60 ms 1261.22 ms 5.62 ms
f85c15f 1254.51 ms 1248.76 ms -5.76 ms
1dd802b 1256.45 ms 1266.90 ms 10.45 ms
c26554e 1241.11 ms 1250.79 ms 9.68 ms

App size

Revision Plain With Sentry Diff
e749de6 5.91 MiB 6.35 MiB 450.57 KiB
c5b01dc 5.91 MiB 6.35 MiB 450.66 KiB
fd971e7 5.91 MiB 6.35 MiB 450.71 KiB
f85c15f 5.91 MiB 6.35 MiB 450.71 KiB
1dd802b 5.91 MiB 6.35 MiB 450.72 KiB
c26554e 5.91 MiB 6.35 MiB 450.57 KiB

@lucas-zimerman

lucas-zimerman commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator Author

Will target main once #4056 is merged.
PR doesn't depend on it, but it's a nice to have change for testing.

Comment thread packages/flutter/lib/src/web/script_loader/sentry_script_loader.dart Outdated
Comment thread packages/flutter/lib/src/web/sentry_web.dart
Comment thread packages/flutter/lib/src/web/sentry_web.dart Outdated
The replay playground has its own PR (#4056). This PR now targets
that branch, so the example still has the playground for testing
without duplicating it here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lucas-zimerman
lucas-zimerman changed the base branch from main to lz/replay-playground October 2, 2026 00:21
Comment thread packages/flutter/lib/src/web/replay/web_replay_event_processor.dart
Comment thread packages/flutter/lib/src/web/sentry_web.dart
Comment thread packages/flutter/test/web/web_sdk_integration_test.dart Outdated
lucas-zimerman and others added 2 commits October 1, 2026 21:58
- Script loader: track scripts as they load and remove them if a
  later one fails, so close() cleans up and a retry doesn't inject
  duplicates
- SentryWeb.close(): await the replay recorder's stop before closing
  the JS SDK, so an in-flight capture can't snapshot into it
- setReplayConfig: mark the recorder started only after start()
  succeeds, so a failed start is retried on the next config

Adds a createReplayCanvasBridge seam to SentryWeb for testing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	packages/flutter/lib/src/replay/integration.dart
#	packages/flutter/lib/src/screen_capture/recorder.dart
#	packages/flutter/lib/src/sentry_flutter.dart
#	packages/flutter/lib/src/web/script_loader/sentry_script_loader.dart
#	packages/flutter/lib/src/web/sentry_js_sdk_version.dart
#	packages/flutter/lib/src/web/sentry_web.dart
#	packages/flutter/lib/src/web/web_sentry_js_binding.dart
#	packages/flutter/test/sentry_flutter_test.dart
#	packages/flutter/test/web/sentry_web_test.dart
#	packages/flutter/test/web/web_sentry_js_binding_test.dart
Base automatically changed from lz/replay-playground to main October 13, 2026 21:05
lucas-zimerman and others added 3 commits October 13, 2026 22:07
A window drag or rotation changes the replay config every frame, and
each change restarted the capture scheduler, which captures on the
next frame. The shadow canvas was resized at frame rate while rrweb
throttled the matching snapshots, so recordings got canvas sizes out
of sync with their content.

While running with an unchanged frame rate, store the new config
without restarting: capture() reads it on every run, so the next
scheduled capture already uses the new size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sentry's replay player clones a canvas on its first recorded draw and
keeps drawing into that clone, so later width/height changes never
reach it. After a resize, smaller frames only covered the clone's
top-left corner, leaving the previous frame around them (duplicated
widgets) and squishing content to one side.

Once the shadow canvas has been snapshotted, a size change now swaps
in a new element, which the player clones fresh at the new size. Its
first snapshot waits a task, until rrweb has registered the node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Android Performance metrics 🚀

  Plain With Sentry Diff
Startup time 416.91 ms 462.62 ms 45.71 ms
Size 14.77 MiB 16.08 MiB 1.31 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
c58ce03 469.42 ms 472.40 ms 2.98 ms
9c9cb8e 360.30 ms 373.57 ms 13.27 ms
e3f3ea1 420.16 ms 436.54 ms 16.39 ms
e200a70 433.06 ms 437.00 ms 3.94 ms
d8f332d 365.31 ms 349.98 ms -15.33 ms
9993fa9 389.44 ms 390.85 ms 1.41 ms
5fc86f6 347.73 ms 334.13 ms -13.59 ms
25ed82c 363.31 ms 352.83 ms -10.48 ms
6b69699 456.06 ms 557.44 ms 101.38 ms
5a95d04 378.92 ms 364.33 ms -14.59 ms

App size

Revision Plain With Sentry Diff
c58ce03 14.31 MiB 15.49 MiB 1.19 MiB
9c9cb8e 14.55 MiB 15.87 MiB 1.31 MiB
e3f3ea1 13.93 MiB 15.18 MiB 1.25 MiB
e200a70 13.93 MiB 15.18 MiB 1.25 MiB
d8f332d 14.55 MiB 15.87 MiB 1.31 MiB
9993fa9 14.55 MiB 15.87 MiB 1.31 MiB
5fc86f6 14.55 MiB 15.87 MiB 1.31 MiB
25ed82c 14.55 MiB 15.87 MiB 1.31 MiB
6b69699 6.54 MiB 7.70 MiB 1.17 MiB
5a95d04 13.93 MiB 15.06 MiB 1.13 MiB

Previous results on branch: lz/replay/web-mask

Startup times

Revision Plain With Sentry Diff
f85c15f 373.65 ms 392.72 ms 19.07 ms
c26554e 374.06 ms 376.54 ms 2.48 ms
1dd802b 378.25 ms 376.87 ms -1.38 ms
fd971e7 378.36 ms 391.00 ms 12.64 ms
e749de6 370.47 ms 369.67 ms -0.80 ms
c5b01dc 388.46 ms 424.62 ms 36.16 ms

App size

Revision Plain With Sentry Diff
f85c15f 14.77 MiB 16.08 MiB 1.31 MiB
c26554e 14.77 MiB 16.08 MiB 1.31 MiB
1dd802b 14.77 MiB 16.08 MiB 1.31 MiB
fd971e7 14.77 MiB 16.08 MiB 1.31 MiB
e749de6 14.77 MiB 16.08 MiB 1.31 MiB
c5b01dc 14.77 MiB 16.02 MiB 1.25 MiB

lucas-zimerman and others added 5 commits October 14, 2026 22:39
SentryFlutter.replay's start, startBuffering, stop and flush now drive
the JS SDK's replay on web instead of logging "not supported", and
start or stop the Flutter recorder with it. A stopped replay stays
stopped across config changes (e.g. a window resize) until started
again. pause/resume remain no-ops: the JS SDK can't pause a replay.

The real-binding tests clear the JS SDK's stored replay session first;
a session left by another test on the same origin resumed in `session`
mode, so stop() hung uploading to the fake DSN.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tap breadcrumbs carry the widget's label and the labels in its touch
path when sendDefaultPii is on. The replay copy kept them, so text the
replay video masks showed up in the replay timeline. Strip them from
the replay breadcrumb only; the event breadcrumb is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assert the production script lists in release mode, with and without
replay; always picking the debug lists used to pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dart events reach the JS SDK through client.sendEnvelope(), which
skips the afterSendEvent hook that client.sendEvent() runs. Replay
switches a buffering replay to a session from that hook, so with
onErrorSampleRate a Dart error never uploaded the buffered replay;
only JS errors did.

Pass the events and transactions in each envelope to the binding,
which emits afterSendEvent for them once the envelope is sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread packages/flutter/lib/src/web/sentry_web.dart Outdated
Comment thread packages/flutter/lib/src/web/sentry_web.dart Outdated
Comment thread packages/flutter/test/web/replay/real_web_replay_canvas_bridge_test.dart Outdated
Comment thread packages/flutter/test/web/replay/sentry_web_replay_recorder_test.dart Outdated
Comment thread packages/flutter/test/web/sentry_web_test.dart Outdated
Comment thread packages/flutter/test/web/sentry_web_test.dart
Comment thread packages/flutter/test/web/web_sentry_js_binding_test.dart Outdated
lucas-zimerman and others added 2 commits October 14, 2026 23:39
- Catch errors from the deferred snapshot of a replaced replay canvas;
  it runs outside the recorder's error handling.
- close() closes the JS SDK even if stopping the recorder fails.
- stopReplay() during an in-flight recorder start now stops the
  recorder once the start completes, instead of leaving it capturing.

Tests: wrap the canvas bridge tests in one subject group, match JS
integration names exactly, and close the SDK after replay-enabled mock
tests so their schedulers stop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The cadence test measures from a rendered frame to the next capture
  instead of waiting a fixed 50ms, so a slow capture can't hide a
  scheduler restart.
- Pin the replay block selector as a literal, not the production
  constant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread packages/flutter/test/web/replay/sentry_web_replay_recorder_test.dart Outdated
Comment thread packages/flutter/lib/src/web/replay/web_replay_event_processor.dart
Comment thread packages/flutter/lib/src/web/sentry_web.dart Outdated
Comment thread packages/flutter/lib/src/web/sentry_web.dart
Comment thread packages/flutter/test/web/sentry_web_test.dart Outdated
lucas-zimerman and others added 2 commits October 15, 2026 19:21
If stopping the replay recorder threw, stopReplay() skipped stopping
the JS replay and left the recorder marked as started, so a later
startReplay() never restarted it. Always stop the JS replay, and mark
the recorder stopped before stopping it.

Also folds the replay controls test group into its parent to keep
test nesting within three levels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lucas-zimerman

Copy link
Copy Markdown
Collaborator Author

Will link to captured replays tomorrow.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/flutter/lib/src/web/sentry_web.dart
Comment thread packages/flutter/lib/src/web/sentry_web.dart Outdated
Replay start, stop, flush, config changes and close now run one at
a time in call order, replacing the flags that tracked their
interleavings. Document the JS SDK behavior the web replay relies
on, and model buffer mode in the event processor test fake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/flutter/lib/src/web/sentry_web.dart Outdated
Comment thread packages/flutter/lib/src/screen_capture/recorder.dart Outdated
Comment thread packages/flutter/test/web/web_sdk_integration_test.dart Outdated

@buenaflor buenaflor left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pls check and resolve the AI reviews first

- Always stop the web replay recorder on close, so a failed start
  doesn't leave the shadow canvas in the DOM.
- Add ReplayTelemetryIntegration on web and check replay support
  when it runs: the replay sample rates are set after the default
  integrations are created.
- Read back the captured image before yielding to the event loop.
- Pin a supported renderer in the replay script loading tests.

Co-Authored-By: Lucas Zimerman Fraulob <lzfstudio+sentry@hotmail.com>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/flutter/lib/src/replay/replay_telemetry_integration.dart
Nothing puts the JS replay's ID on the Dart scope, so
ReplayTelemetryIntegration marked every web replay as buffering.
Bindings can now report whether the replay is buffering; web asks the
JS Replay for its recording mode, as the JS SDK does for
sentry._internal.replay_is_buffering. Native bindings return null and
keep inferring it from the scope.
Comment thread packages/flutter/test/web/sentry_web_test.dart Outdated
Replace the microtask spin loops waiting for bridgeStart with a
Completer on the fake bridge, bounded by a timeout, so a start that
never runs fails the test instead of hanging it.
Comment thread packages/flutter/lib/src/web/web_sentry_js_binding.dart Outdated
Comment thread packages/flutter/test/web/replay/real_web_replay_canvas_bridge_test.dart Outdated
Comment thread packages/flutter/test/web/sentry_web_test.dart Outdated
A rejected client.sendEnvelope() drops the envelope, so log it at error
level with its stack trace instead of debug.

Also tighten the web replay tests: pin the shadow canvas id as a
literal and check that the block selector spares it while matching
other canvases, and keep the real-binding replay tests in buffer mode
so they never upload to the test DSN.
Comment thread packages/flutter/test/web/sentry_web_test.dart
Comment thread packages/flutter/test/web/web_sentry_js_binding_test.dart
sentry-junior Bot and others added 2 commits October 19, 2026 20:20
Enable replay in the attachment-only envelope test, so event
collection is on and the empty-events check means something. Assert
that the manual canvas integration has enableManualSnapshot set.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/flutter/lib/src/replay/scheduled_recorder.dart
sentry-junior Bot and others added 2 commits October 19, 2026 21:10
Flutter Web reports inactive when a visible page loses focus. The
resumed-only capture guard, meant for native GPU surface teardown,
froze the replay canvas then. Let the web recorder also capture while
inactive.
Rename web replay tests to plain verb phrases, split the scope
payload test per method, pin the JS SDK name and replay-canvas
URLs as literals, and clarify the isReplayBuffering null contract.

Co-Authored-By: Lucas Zimerman Fraulob <lzfstudio+sentry@hotmail.com>

@buenaflor buenaflor left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1st review pass, so far looks good!

Comment thread packages/flutter/lib/src/web/replay/replay_click_filter.dart
Comment thread packages/flutter/lib/src/web/web_sdk_integration.dart
Comment thread packages/flutter/lib/src/web/web_sentry_js_binding.dart Outdated
lucas-zimerman and others added 2 commits October 27, 2026 10:18
- Clear the shadow canvas before drawing each frame so transparent
  pixels don't keep the previous frame
- Pick the replay bundle from the binding's supportsReplay
- Drop the unused name-based replayCanvas integration
- Add a regression test feeding a real JS SDK flutter-view click
  through the replay click filter

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Drop the no-replay script test, already covered by the default
  supportsReplay stub
- Name the click filter tests with verb phrases

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@buenaflor buenaflor left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as discussed let's add an opt-in flag and mark it experimental

feel free to add some ideas on the naming of the flag

maybe smth like "enableWebReplay"?

@lucas-zimerman

Copy link
Copy Markdown
Collaborator Author

enableWebReplay goes straight to the point, can't think of a better name :D

lucas-zimerman and others added 2 commits October 29, 2026 21:53
Add the experimental `options.replay.enableWebReplay` flag, off by
default. Web Session Replay now only starts when it is set alongside a
replay sample rate; otherwise init logs how to enable it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lucas-zimerman lucas-zimerman changed the title feat(flutter): Add Session Replay for Flutter Web feat(flutter): Add experimental Session Replay for Flutter Web Oct 29, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 13bfebd. Configure here.

if (breadcrumb.message != null) 'message': breadcrumb.message,
if (breadcrumb.category != null) 'category': breadcrumb.category,
if (breadcrumb.data?.isNotEmpty ?? false)
'data': normalizeMap(breadcrumb.data),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replay path labels survive normalization

Medium Severity

normalize stringifies each tap-path node because production _getTouchPath returns List<Map<String, String?>>, and Map is invariant, so those nodes are not Map<String, dynamic>. _withoutLabels then never sees maps and cannot drop label. Replay click breadcrumbs keep widget text that this path was written to exclude.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot (Root)

Reviewed by Cursor Bugbot for commit 13bfebd. Configure here.

This branch has not been deployed

No deployments
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.

Replay Support for Flutter Web

2 participants