Skip to content

feat(replay): Add manual replay control API - #5978

Open
romtsn wants to merge 7 commits into
romtsn/feat/java-665-replay-start-stopfrom
romtsn/feat/java-325-replay-control-api
Open

feat(replay): Add manual replay control API#5978
romtsn wants to merge 7 commits into
romtsn/feat/java-665-replay-start-stopfrom
romtsn/feat/java-325-replay-control-api

Conversation

@romtsn

@romtsn romtsn commented Aug 19, 2026

Copy link
Copy Markdown
Member

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?

  • Ran ./gradlew spotlessApply apiDump.
  • Ran all sentry-android-core unit tests: 1,601 tests, 0 failures, 1 skipped.
  • Ran all sentry-android-replay unit tests: 252 tests, 0 failures, 1 skipped.
  • Added focused coverage for the public API, manual pause behavior, buffering, flushing, and lifecycle transitions.

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec.

🔮 Next steps

  • Document the public replay controls in the Android documentation repository.

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>
@linear-code

linear-code Bot commented Aug 19, 2026

Copy link
Copy Markdown

JAVA-325

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against 20d793b

@sentry

sentry Bot commented Aug 19, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.53.0 (1) release

⚙️ sentry-android Build Distribution Settings

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 315.47 ms 382.74 ms 67.27 ms
Size 0 B 0 B 0 B

Baseline results on branch: romtsn/feat/java-665-replay-start-stop

Startup times

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

Previous results on branch: romtsn/feat/java-325-replay-control-api

Startup times

Revision Plain With Sentry Diff
a12ad59 311.45 ms 364.02 ms 52.57 ms
dbf03ab 319.24 ms 369.88 ms 50.63 ms

App size

Revision Plain With Sentry Diff
a12ad59 0 B 0 B 0 B
dbf03ab 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>
@romtsn
romtsn marked this pull request as ready for review August 19, 2026 16:24
@romtsn romtsn added the deep-dive PR needs a thorough review of design, behavior, and edge cases label Aug 19, 2026
romtsn and others added 2 commits August 19, 2026 20:28
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 runningcode 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.

I did a first pass quick review, ping me for a second round.


override fun onAppForegrounded(startNewSession: Boolean) {
enqueueOnMainThread {
if (!isEnabled.get()) {

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.

would it make more sense to check this condition before enqueing?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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()) {

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.

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. */

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.

just for my understanding why is start and startBuffering two different calls. Why do we want to start a session separately from Buffering?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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

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.

Can we document that flush() sends it up to Sentry? I think it isn't so clear (it could just be flushing to disk).

*/
void startBuffering();

/** Stops the current replay. A subsequent {@link #start()} begins a new replay session. */

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.

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

@romtsn romtsn Aug 24, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

both, the stop logic does not differ between the two (also updated javadoc)

enqueueOnMainThread {
val current = state.get()
if (!current.isRecording) {
startInternal(isFullSession = true)

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.

why does flush call startInternal if we're not recording?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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>

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

Reviewed by Cursor Bugbot for commit df60eec. Configure here.

Comment thread sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt Outdated
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>

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

looks good. I still think the start and startBuffering methods are confusing. Not sure if better javadocs would be less confusing. I understand that it is that way to align with js.

*/
public interface IReplayApi {

/** Starts a new replay session. Does nothing if a replay is already being recorded. */

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.

Can we document that flush() sends it up to Sentry? I think it isn't so clear (it could just be flushing to disk).

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

lgtm!

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

Labels

deep-dive PR needs a thorough review of design, behavior, and edge cases

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants