feat(replay): Add manual replay control API - #5978
Conversation
Expose start, buffering, pause, resume, stop, and flush operations through Sentry.replay(). Keep lifecycle pauses distinct from explicit user pauses. Foregrounding therefore does not resume sensitive-screen recording unexpectedly. Refs JAVA-325 Co-Authored-By: OpenAI Codex <noreply@openai.com>
|
📲 Install BuildsAndroid
|
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| a21e537 | 319.04 ms | 357.04 ms | 38.00 ms |
| 5cdb0b8 | 311.75 ms | 369.08 ms | 57.33 ms |
| b208705 | 315.54 ms | 354.54 ms | 39.00 ms |
| f158a9a | 352.96 ms | 413.17 ms | 60.21 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| a21e537 | 0 B | 0 B | 0 B |
| 5cdb0b8 | 0 B | 0 B | 0 B |
| b208705 | 0 B | 0 B | 0 B |
| f158a9a | 0 B | 0 B | 0 B |
A foreground callback can run before ReplayIntegration registers and initializes its options. Ignore lifecycle callbacks until the integration is enabled to avoid crashing during SDK initialization. Refs JAVA-325 Co-Authored-By: Codex <noreply@openai.com>
Track whether a buffered replay was started automatically so only automatic buffers apply per-error sampling. Manually started buffers now capture on errors as documented. Refs JAVA-325 Co-Authored-By: Codex <noreply@openai.com>
This reverts commit 8073208. Reason: Match Sentry JavaScript by applying onErrorSampleRate to all buffered replay captures. Refs JAVA-325 Co-Authored-By: Codex <noreply@openai.com>
runningcode
left a comment
There was a problem hiding this comment.
I did a first pass quick review, ping me for a second round.
|
|
||
| override fun onAppForegrounded(startNewSession: Boolean) { | ||
| enqueueOnMainThread { | ||
| if (!isEnabled.get()) { |
There was a problem hiding this comment.
would it make more sense to check this condition before enqueing?
There was a problem hiding this comment.
apparently it can break a subtle edge-case #5978 (comment) so i reverted it to be safe
|
|
||
| override fun onAppForegrounded(startNewSession: Boolean) { | ||
| enqueueOnMainThread { | ||
| if (!isEnabled.get()) { |
There was a problem hiding this comment.
would it make more sense to check this condition before enqueing?
| */ | ||
| public interface IReplayApi { | ||
|
|
||
| /** Starts a new replay session. Does nothing if a replay is already being recorded. */ |
There was a problem hiding this comment.
just for my understanding why is start and startBuffering two different calls. Why do we want to start a session separately from Buffering?
There was a problem hiding this comment.
we give customers an option to choose which mode they want to start recording in. This could've been a method argument, but we're mimicking the JS API here to stay aligned.
There was a problem hiding this comment.
session mode would continuously send replay segments while in buffer mode customers would have to explicitly call flush() or raise an error to have anything sent
| */ | ||
| void startBuffering(); | ||
|
|
||
| /** Stops the current replay. A subsequent {@link #start()} begins a new replay session. */ |
There was a problem hiding this comment.
does this mean the session is stopped or the buffering is stopped or both?
I ask because we have two different start methods but only a single stop/pause which makes it not clear which one the stop stops
There was a problem hiding this comment.
both, the stop logic does not differ between the two (also updated javadoc)
| enqueueOnMainThread { | ||
| val current = state.get() | ||
| if (!current.isRecording) { | ||
| startInternal(isFullSession = true) |
There was a problem hiding this comment.
why does flush call startInternal if we're not recording?
There was a problem hiding this comment.
Also following the JS API :) I agree it can be a real footgun though - there's one issue open in sentry-javascript re. that getsentry/sentry-javascript#12664. I think I'd still not introduce recordingMode as they suggest in the issue above for now, until someone actually hits this (we document the behaviour). Or we could also diverge from JS here and not start a new recording in flush at all.
Drop foreground and background callbacks received before Replay is registered instead of leaving stale work on the main queue. Clarify the manual replay API documentation. Refs JAVA-325 Co-Authored-By: Codex <noreply@openai.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit df60eec. Configure here.
Check Replay registration when the foreground callback executes so AppState catch-up can start Replay after registration. Cover both callback orderings with tests. Refs JAVA-325 Co-Authored-By: Codex <noreply@openai.com>

Depends on #5965.
📜 Description
Add
Sentry.replay()controls for starting full-session or buffered recording, stopping, pausing, resuming, and flushing.Lifecycle-driven background pauses remain separate from explicit user pauses. Foregrounding resumes only lifecycle-paused recording, so an app can pause replay on sensitive screens without the SDK unexpectedly restarting it. Starting an already-running replay is a no-op; flushing a stopped replay starts a full session, matching the JavaScript SDK. Explicit starts bypass replay sample rates, which continue to control automatic startup.
💡 Motivation and Context
Give Android applications explicit control over Session Replay for opt-in flows and sensitive screens while preserving automatic lifecycle behavior.
Refs JAVA-325
💚 How did you test it?
./gradlew spotlessApply apiDump.sentry-android-coreunit tests: 1,601 tests, 0 failures, 1 skipped.sentry-android-replayunit tests: 252 tests, 0 failures, 1 skipped.📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps