Skip to content

feat(flutter): Add manual replay APIs - #4010

Merged
buenaflor merged 14 commits into
v10-branchfrom
feat/manual-replay-apis
Sep 15, 2026
Merged

buenaflor merged 14 commits into
v10-branchfrom
feat/manual-replay-apis

Conversation

@buenaflor

@buenaflor buenaflor commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

📜 Description

Adds SentryFlutter.replay with start, startBuffering, pause, resume, stop, and flush, 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:

ReplayTelemetryIntegration no longer checks the sample rates. A manually started replay records while both rates are 0, and the old gates would have dropped its replay_id from logs, spans, and metrics. Attributes are now derived from whether a replay is actually recording, which is safe because:

  • The session gate was redundant — native writes the replay ID to the scope only in session mode, so the scope already answers the question. It also suppressed a legitimate case: a buffered replay flushed by an error keeps recording in session mode.
  • The buffering gate guarded a state only real buffering produces — "the binding has an ID but the scope doesn't". iOS can't produce that split at all (the screenshot provider hardcodes replayIsBuffering = NO), leaving Android's replayStarted(id, isBuffering: true) as its only source.

iOS now clears the replay ID on stop. iOS has no replayStopped callback, so after stop() a finished replay's ID stayed on scope.replayId and 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?

  • Full sentry_flutter unit suite, plus new coverage for manual starts with sampling disabled, the "no replay running" cases, and stopReplay() clearing both the binding cache and the scope (verified to fail before the fix).
  • Replay integration tests on an iOS simulator; Android integration test target builds.
  • flutter analyze and formatting with the Flutter-bundled Dart that CI uses.

📝 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

None.

Made with Cursor

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>
@github-actions

github-actions Bot commented Sep 7, 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

  • (flutter) Add manual replay APIs by buenaflor in #4010

🤖 This preview updates automatically when you update the PR.

@github-actions

github-actions Bot commented Sep 7, 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/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt
  • packages/flutter/darwin/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift

@codecov

codecov Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.32787% with 12 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (v10-branch@7f3c7dc). Learn more about missing BASE report.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...ckages/flutter/lib/src/native/c/sentry_native.dart 0.00% 12 Missing ⚠️
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           
Flag Coverage Δ
sentry_firebase_remote_config 100.00% <ø> (?)
sentry_flutter 92.67% <80.32%> (?)
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.

buenaflor and others added 5 commits September 7, 2026 17:06
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>
@buenaflor

Copy link
Copy Markdown
Contributor Author

@cursor review

@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.

buenaflor and others added 2 commits September 8, 2026 11:26
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>
@buenaflor
buenaflor marked this pull request as ready for review September 8, 2026 11:28
Copilot AI lite review requested due to automatic review settings September 8, 2026 11:28
@buenaflor
buenaflor requested a review from denrase as a code owner September 8, 2026 11:28
_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>

Copilot AI 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.

🟡 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 SentryReplay API (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.

Comment thread packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart
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>
@buenaflor

Copy link
Copy Markdown
Contributor Author

bugbot run

@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.

✅ 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 lucas-zimerman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Left two non-blocking comments, LGTM!

buenaflor and others added 2 commits September 15, 2026 10:33
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>
@buenaflor
buenaflor merged commit fbdb074 into v10-branch Sep 15, 2026
72 checks passed
@buenaflor
buenaflor deleted the feat/manual-replay-apis branch September 15, 2026 11:14
buenaflor added a commit to getsentry/sentry-docs that referenced this pull request Oct 9, 2026
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>
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.

3 participants