Repository navigation
feat(flutter): Add manual replay APIs - #4010
Conversation
Expose start, buffering, pause, resume, stop, and flush controls through SentryFlutter.replay on Android and iOS. Keep manual replay available when automatic sampling is disabled and clean up replay callbacks on close. Fixes #2558 Co-Authored-By: GPT-5.6 Sol <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Semver Impact of This PR🟡 Minor (new features) 📋 Changelog PreviewThis is how your changes will appear in the changelog. Features
🤖 This preview updates automatically when you update the PR. |
🚨 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:
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## v10-branch #4010 +/- ##
=============================================
Coverage ? 92.96%
=============================================
Files ? 113
Lines ? 4395
Branches ? 0
=============================================
Hits ? 4086
Misses ? 309
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
Replay telemetry was gated on the replay sample rates, so a replay started through SentryFlutter.replay with zero sampling produced no replay attributes on logs, spans and metrics, and never registered its trace ID or segment name with the native layer. Resolve the running replay from SDK state instead, and clear the cached replay ID when a replay stops so a stale ID isn't attached afterwards. Also addressed from review: - Close the Android ReplayIntegration through the SDK's own teardown and only drop the static reference afterwards. - Collapse six copies of the platform guard in the iOS plugin. - Replace the repeated replay integration lookup in the JNI binding with a single getter. - Clear the hub and options when the replay integration closes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the _invoke indirection in the replay API for the same reason _controlReplay went: it made every method restate its own name as a string and wrap a single call in a lambda. Resolving the binding through a getter leaves six one-line overrides. Follow the SentryMetrics/SentryLogger shape for the public API by replacing the createSentryReplay factory with a named DefaultSentryReplay implementation. In the integration test, replace the boolean flag that switched setupSentryAndApp between two different init paths with a separate helper, and skip the mobile-only tests through `skip:` rather than returning early from the body. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
The test sat outside any group, so it never ran loadTestPackage() and failed on Linux and Windows with a MissingPluginException. It also asserts a replay ID, which only the Cocoa binding tracks, so it failed on the browser where SentryWeb is always built. Move it into a group with the usual setup and mark it vm-only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
iOS reports replay IDs only through the screenshot provider, which goes quiet once recording stops. stopReplay() cleared the binding's cached ID but not the scope's, so events and the trace context kept carrying a dead replay ID after a manual stop. Android already handles this via replayStopped. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Fold _replayContext into _replayAttributes so the buffering inference reads top to bottom in one place instead of being passed between two functions via a record. Spell out why the integration no longer consults the sample rates: the attributes key off whether a replay is actually recording, which manual starts also satisfy. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
|
@cursor review |
setReplayConfig released the ScreenshotRecorderConfig only on the success path. onConfigurationChanged ends in a check() that throws on a pending Java exception, and tryCatchSync swallows it, so a failing call leaked a global ref every resize or orientation change. Scope the config to an arena, matching the rest of the file. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Drop wording that restated signatures and the ??= lookup, and stop explaining the sample-rate change in two places. The _replay doc now states the ownership rule that matters for JNI: only _setNativeReplay may drop the cached reference. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
_removeOnBuildCallback sat next to _onBeforeSendEventCallback and shared its suffix, implying both hold callbacks. It holds the disposer that removes one. Name it _removeOnBuildListener. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
🟡 Changes recommended
SentryNativeCocoa.stopReplay() should clear cached/scope replay IDs even if the native stop call throws, otherwise stale IDs can continue to be attached after a failed stop.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds a public, manual Session Replay control surface to sentry_flutter (SentryFlutter.replay) and adjusts replay wiring so manual recording works even when automatic replay sampling rates are set to 0.
Changes:
- Introduces
SentryReplayAPI (start,startBuffering,pause,resume,stop,flush) and routes calls to native (JNI on Android, method channel on iOS). - Keeps replay integrations/callbacks available when automatic sampling is disabled, and updates telemetry attributes to be based on “is a replay actually recording?” rather than sample-rate gates.
- Improves lifecycle cleanup (e.g., replay build callback teardown) and adds/updates tests + example integration tests for manual replay flows.
File summaries
| File | Description |
|---|---|
| packages/flutter/lib/src/sentry_flutter.dart | Adds SentryFlutter.replay entry point for manual replay control. |
| packages/flutter/lib/src/replay/replay_api.dart | Defines SentryReplay interface + default implementation that forwards to native when available. |
| packages/flutter/lib/sentry_flutter.dart | Exports SentryReplay from the public barrel. |
| packages/flutter/lib/src/native/sentry_native_binding.dart | Extends native binding contract with manual replay control methods. |
| packages/flutter/lib/src/native/sentry_native_channel.dart | Implements replay control methods via method channel. |
| packages/flutter/lib/src/native/java/sentry_native_java.dart | Implements replay control methods through cached JNI replay integration; improves JNI resource management. |
| packages/flutter/lib/src/native/java/sentry_native_java_init.dart | Ensures replay recorder callbacks are created even when automatic sampling is disabled; clears replay ID on stop callback. |
| packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart | Sets up replay screenshot callback whenever replay is supported; clears replay ID/scope on stopReplay(). |
| packages/flutter/lib/src/native/c/sentry_native.dart | Adds no-op replay control methods for unsupported native (C/FFI) platforms. |
| packages/flutter/lib/src/web/sentry_web.dart | Adds no-op replay control methods for web binding. |
| packages/flutter/lib/src/replay/integration.dart | Makes replay integration always register when supported (not sample-rate gated), adds teardown for screenshot build callback, and cleans integration state on close. |
| packages/flutter/lib/src/integrations/replay_telemetry_integration.dart | Removes sample-rate checks; attaches replay attributes based on live replay state so manual replays are covered. |
| packages/flutter/lib/src/screenshot/sentry_screenshot_widget.dart | Changes onBuild to return an unregister function for explicit callback cleanup. |
| packages/flutter/test/replay/replay_api_test.dart | Adds unit tests verifying the new manual replay API forwards to native and behaves safely when unavailable/unsupported. |
| packages/flutter/test/replay/replay_integration_test.dart | Updates expectations and adds coverage for configuring replay when automatic sampling is disabled and ensuring teardown prevents post-close config. |
| packages/flutter/test/integrations/replay_telemetry_integration_test.dart | Updates tests to assert telemetry integration registers even with sampling disabled and correctly annotates logs/spans for manual replays. |
| packages/flutter/test/sentry_native_channel_test.dart | Adds method-channel tests for replay control method invocations and iOS replay ID clearing on stop. |
| packages/flutter/test/sentry_flutter_test.dart | Adds VM-only iOS-mocked tests ensuring replay screenshot handling works with sampling disabled and stop clears replay IDs from scope/binding. |
| packages/flutter/darwin/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift | Adds method-channel handlers that call Sentry Cocoa replay controls. |
| packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt | Adjusts teardown so replay integration reference is dropped on close; installs replay integration when callbacks exist (not sample-rate gated). |
| packages/flutter/example/integration_test/replay_test.dart | Extends integration test coverage for manual replay controls and manual start with automatic sampling disabled; adds Android teardown assertion. |
| packages/flutter/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved | Bumps sentry-cocoa SPM version/revision used by the example app. |
| packages/flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | Bumps sentry-cocoa SPM version/revision used by the example app. |
Review details
- Files reviewed: 23/23 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
stopReplay cleared the cached and scope replay IDs only after the channel call returned, so a throwing stop left Dart believing a replay was still recording. Move the clearing into a finally; the error still propagates to the caller. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit b34b37b. Configure here.
Reapply screenshot configuration when Android replay starts, and synchronize Dart replay state after native buffer promotion. Preserve the active replay ID when error sampling rejects a capture. Add six Android regression tests for frames, restart, and telemetry. Verified 18 Android replay tests, 1061 Flutter unit tests, analysis, formatting, and minified release JNI checks. Refs #2558 Co-Authored-By: Codex <noreply@openai.com>
lucas-zimerman
left a comment
There was a problem hiding this comment.
Left two non-blocking comments, LGTM!
Share the replay platform guard and return nil for control operations. Preserve the capture result and explicit method routing. Refs #4010 Co-Authored-By: Codex <noreply@openai.com>
Add opt-in, pause/resume, and buffer/flush scenarios with a manual-only launch setting. Stop replay when switching scenarios or leaving. Verified three widget tests, analysis, and all scenarios on a Pixel 4. Refs #4010 Co-Authored-By: Codex <noreply@openai.com>
Pause replay while the example screen is open and resume on exit. Restore automatic sampling and remove the demo flag, docs, and tests. Verified formatting, analysis, and launch on a Pixel 4. Refs #4010 Co-Authored-By: Codex <noreply@openai.com>
Prepare the Flutter documentation for v10 as the recommended SDK version. Cover platform minimums and Swift Package Manager setup, current callbacks and defaults, logs and metrics, integration data, Replay masking and manual controls, release health, and profiling removal. Shared Dart content is updated where Flutter inherits it. Document the Windows/Linux Breakpad default and Crashpad override, terminal web `unhandled` sessions with JavaScript SDK 11, filtering callback failures and sampler fallbacks, and app-start breakdown spans with their binding requirements. This is the release-wide documentation companion to #19390. Merge with the v10 rollout. Closes getsentry/sentry-dart#4025 Manual Replay guidance follows [sentry-dart#4010](getsentry/sentry-dart#4010) and [sentry-dart#3992](getsentry/sentry-dart#3992), covering zero-rate setup, buffering, pause/resume, stop/restart, and flush behavior. ## IS YOUR CHANGE URGENT? - [ ] Urgent deadline (GA date, etc.): YYYY-MM-DD - [ ] Other deadline: YYYY-MM-DD - [x] No deadline: Not urgent, can wait up to 1 week+ Merge with the v10 rollout; no release date has been set here. ## PRE-MERGE CHECKLIST - [ ] Checked Vercel preview for correctness, including links - [ ] PR was reviewed and approved by any necessary SMEs (subject matter experts) - [ ] PR was reviewed and approved by a member of the Sentry docs team --------- Co-authored-by: Codex <noreply@openai.com>
📜 Description
Adds
SentryFlutter.replaywithstart,startBuffering,pause,resume,stop, andflush, routed through JNI on Android and the method channel on iOS. The replay recorder now stays available when automatic sampling is off, and native and Dart callbacks are cleaned up on close.Two changes worth a closer look in review:
ReplayTelemetryIntegrationno longer checks the sample rates. A manually started replay records while both rates are0, and the old gates would have dropped itsreplay_idfrom logs, spans, and metrics. Attributes are now derived from whether a replay is actually recording, which is safe because:replayIsBuffering = NO), leaving Android'sreplayStarted(id, isBuffering: true)as its only source.iOS now clears the replay ID on stop. iOS has no
replayStoppedcallback, so afterstop()a finished replay's ID stayed onscope.replayIdand kept riding along on events and the trace context.SentryNativeCocoa.stopReplay()now clears both the binding cache and the scope. Pre-existing and still open: a native-initiated stop (session max duration) has no callback, and it can't be covered by a test because FFI/JNI can't be mocked.💡 Motivation and Context
Applications need explicit Session Replay control for opt-in flows and sensitive screens, independent of the automatic sample rates.
Fixes #2558
💚 How did you test it?
sentry_flutterunit suite, plus new coverage for manual starts with sampling disabled, the "no replay running" cases, andstopReplay()clearing both the binding cache and the scope (verified to fail before the fix).flutter analyzeand formatting with the Flutter-bundled Dart that CI uses.📝 Checklist
sendDefaultPiiis enabled🔮 Next steps
None.
Made with Cursor