Skip to content

fix(replay): Defer buffered replay upload until after error sampling - #6685

Merged
antonis merged 6 commits into
mainfrom
antonis/fix-6598-buffer-replay-orphaned
Sep 10, 2026
Merged

fix(replay): Defer buffered replay upload until after error sampling#6685
antonis merged 6 commits into
mainfrom
antonis/fix-6598-buffer-replay-orphaned

Conversation

@antonis

@antonis antonis commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 Description

In buffer / on-error Session Replay mode (replaysOnErrorSampleRate), the buffered replay was flushed inside the mobile-replay integration's beforeSend wrapper. Since @sentry/core 10.70.0 the error sampleRate roll runs after beforeSend, so a replay was uploaded even for errors that were then dropped by sampleRate — orphaning the replay and burning replay quota.

The work is split in two:

  1. tagEventWithReplayId (in the beforeSend wrapper) only links the event to the current buffered replay id — a read via NATIVE.getCurrentReplayId(), no flush.
  2. flushReplayForSentEvent (in a new afterSendEvent client hook) performs the native captureReplay() flush, and only fires for events that survive sampling and are actually sent.

Reading the id before the flush requires the current replay id to be available while a buffer replay is still recording:

  • Android: getCurrentReplayId() now prefers ReplayController.getReplayId() (assigned at record start) and falls back to the scope. Self-contained — sentry-java already exposes it.
  • iOS: relies on the matching sentry-cocoa change to SentrySDK.internal.replay.replayId (fix(replay): Return buffered replay id from internal hybrid API sentry-cocoa#8976). Until sentry_cocoa_version in RNSentry.podspec is bumped to a release containing that fix, on-error replays are not linked or uploaded on iOS. This is the blocker for taking this PR out of draft.

💡 Motivation and Context

Fixes #6598.

💚 How did you test it?

  • Manual testing
  • Rewrote mobilereplay.test.ts to assert the new split (no flush in beforeSend; flush in afterSendEvent).
  • Added mobilereplay.sampling.test.ts, which drives a real @sentry/core client and proves sampleRate: 0captureReplay is never called.
  • Added RNSentryReplayIdTest.java covering the controller-first getCurrentReplayId() lookup (4 cases).
  • Local: JS replay suites pass (48), yarn build:sdk, yarn circularDepCheck, oxlint, and Java format all clean.

📝 Checklist

  • 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.
  • I updated the wizard 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

In buffer (on-error) mode, the replay was flushed inside the beforeSend
wrapper. Since @sentry/core 10.70.0 error sampleRate runs AFTER
beforeSend, so a replay was uploaded even for errors later dropped by
sampleRate, orphaning the replay and consuming quota (#6598).

Split the work: link the event to the buffered replay id in beforeSend
(read-only, no flush) and flush the native replay in an afterSendEvent
hook, which only fires for events that survive sampling.

To link before flushing, the current replay id must be readable while a
buffer replay is still recording. On Android getCurrentReplayId now
prefers ReplayController.getReplayId(); iOS relies on the matching
sentry-cocoa change to SentrySDK.internal.replay.replayId.

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

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Semver Impact of This PR

None (no version bump detected)

📋 Changelog Preview

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


  • fix(replay): Defer buffered replay upload until after error sampling by antonis in #6685
  • feat(tracing): sync JS scope propagation context to native scope by alwx in #6686
  • fix(android): identify the screen appear event by name instead of class by TaduJR in #6692
  • chore(deps): update Android SDK to v8.56.0 by github-actions in #6694
  • ci: reduce sample app build matrix to production-only by alwx in #6687
  • chore(deps): update JavaScript SDK to v10.74.0 by github-actions in #6695
  • chore(deps): update Cocoa SDK to v9.28.0 by github-actions in #6693
  • test(e2e): Evaluate replay assertion on Android by antonis in #6684
  • test(e2e): Restore iOS replay assertion in captureReplay test by antonis in #6683
  • feat(visionos): Add visionOS sample app by antonis in #6676
  • fix(core): Align beforeBreadcrumb and tracesSampler error fallbacks with spec by antonis in #6675
  • chore(replay): Mark mobileReplayIntegration as stable by antonis in #6679
  • feat(sample): Add tvOS sample app by antonis in #6677
  • fix(e2e): Pin json gem < 3.0 for RN < 0.72 iOS builds by antonis in #6678
  • feat(ios): Expose enableMemoryIntrospection option by antonis in #6674
  • feat(android): Add anrProfilingSampleRate option by antonis in #6673
  • ci: Unpin Android E2E emulator build by antonis in #6672
  • ref(ios): Remove deprecated private SDK API usage by philprime in #6647
  • chore(deps): update Cocoa SDK to v9.27.0 by github-actions in #6670
  • chore(deps): update Sentry Android Gradle Plugin to v6.21.0 by github-actions in #6671
  • chore(deps): Bump fast-uri to ^3.1.6 to resolve security alerts by antonis in #6662
  • chore(deps): Bump qs to ^6.16.0 to resolve security alerts by antonis in #6663
  • chore(deps): Bump @xmldom/xmldom pins to patched versions to resolve security alerts by antonis in #6664
  • docs: Revamp root and nested AGENTS.md by antonis in #6666

🤖 This preview updates automatically when you update the PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@antonis
antonis force-pushed the antonis/fix-6598-buffer-replay-orphaned branch from 04a8df5 to 04f6aa8 Compare September 8, 2026 09:05
@antonis antonis added Blocked ready-to-merge Triggers the full CI test suite labels Sep 8, 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.

✅ 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 04f6aa8. Configure here.

@antonis

antonis commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Blocking till getsentry/sentry-cocoa#8976 is released

@sentry

sentry Bot commented Sep 8, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
Sentry RN io.sentry.reactnative.sample 8.25.0 (105) Release

⚙️ sentry-react-native Build Distribution Settings

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

iOS (legacy) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 3844.81 ms 1225.34 ms -2619.47 ms
Size 5.15 MiB 6.89 MiB 1.74 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
0a9e622+dirty 3835.87 ms 1221.07 ms -2614.80 ms
7d6fd3a+dirty 1223.29 ms 1229.57 ms 6.28 ms
5ca03f9+dirty 3844.55 ms 1218.36 ms -2626.19 ms
774257e+dirty 3846.90 ms 1215.02 ms -2631.88 ms
40c9884+dirty 3837.72 ms 1226.61 ms -2611.11 ms
4e0b819+dirty 3839.05 ms 1210.75 ms -2628.30 ms
d038a14+dirty 3845.71 ms 1228.11 ms -2617.59 ms
57e0069+dirty 3842.29 ms 1212.12 ms -2630.17 ms
882f8ae+dirty 3840.30 ms 1224.41 ms -2615.88 ms
5125c43+dirty 3846.45 ms 1221.12 ms -2625.32 ms

App size

Revision Plain With Sentry Diff
0a9e622+dirty 4.98 MiB 6.51 MiB 1.53 MiB
7d6fd3a+dirty 3.38 MiB 4.77 MiB 1.39 MiB
5ca03f9+dirty 4.98 MiB 6.53 MiB 1.55 MiB
774257e+dirty 5.15 MiB 6.70 MiB 1.54 MiB
40c9884+dirty 4.98 MiB 6.51 MiB 1.53 MiB
4e0b819+dirty 4.98 MiB 6.46 MiB 1.49 MiB
d038a14+dirty 5.15 MiB 6.67 MiB 1.51 MiB
57e0069+dirty 4.98 MiB 6.50 MiB 1.52 MiB
882f8ae+dirty 5.15 MiB 6.70 MiB 1.54 MiB
5125c43+dirty 5.15 MiB 6.68 MiB 1.53 MiB

Previous results on branch: antonis/fix-6598-buffer-replay-orphaned

Startup times

Revision Plain With Sentry Diff
f55facc+dirty 3848.98 ms 1221.90 ms -2627.07 ms
1fcb382+dirty 3844.48 ms 1227.18 ms -2617.30 ms

App size

Revision Plain With Sentry Diff
f55facc+dirty 5.15 MiB 6.88 MiB 1.73 MiB
1fcb382+dirty 5.15 MiB 6.88 MiB 1.73 MiB

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Android (legacy) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 417.17 ms 440.27 ms 23.10 ms
Size 50.56 MiB 56.46 MiB 5.90 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
a2585ce+dirty 426.36 ms 483.26 ms 56.90 ms
882f8ae+dirty 399.98 ms 427.06 ms 27.08 ms
af33f3b+dirty 434.90 ms 506.14 ms 71.24 ms
ecf47a2+dirty 420.40 ms 458.02 ms 37.62 ms
acd838e+dirty 422.63 ms 462.39 ms 39.76 ms
7fd0012+dirty 444.73 ms 470.08 ms 25.35 ms
580fb5c+dirty 436.34 ms 471.63 ms 35.28 ms
5ca03f9+dirty 423.30 ms 467.00 ms 43.70 ms
5c1e987+dirty 423.52 ms 471.64 ms 48.12 ms
822d35b+dirty 429.31 ms 498.04 ms 68.73 ms

App size

Revision Plain With Sentry Diff
a2585ce+dirty 49.74 MiB 55.36 MiB 5.61 MiB
882f8ae+dirty 48.30 MiB 53.60 MiB 5.29 MiB
af33f3b+dirty 49.74 MiB 55.09 MiB 5.35 MiB
ecf47a2+dirty 49.74 MiB 54.82 MiB 5.07 MiB
acd838e+dirty 48.30 MiB 53.60 MiB 5.30 MiB
7fd0012+dirty 50.56 MiB 56.46 MiB 5.90 MiB
580fb5c+dirty 49.74 MiB 54.79 MiB 5.05 MiB
5ca03f9+dirty 49.74 MiB 55.26 MiB 5.52 MiB
5c1e987+dirty 43.75 MiB 48.08 MiB 4.33 MiB
822d35b+dirty 49.74 MiB 54.84 MiB 5.10 MiB

Previous results on branch: antonis/fix-6598-buffer-replay-orphaned

Startup times

Revision Plain With Sentry Diff
1fcb382+dirty 446.20 ms 489.64 ms 43.44 ms
f55facc+dirty 423.85 ms 450.68 ms 26.83 ms

App size

Revision Plain With Sentry Diff
1fcb382+dirty 50.56 MiB 56.46 MiB 5.90 MiB
f55facc+dirty 50.56 MiB 56.46 MiB 5.90 MiB

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

iOS (new) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 3862.56 ms 1224.86 ms -2637.70 ms
Size 5.15 MiB 6.89 MiB 1.74 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
64630e5+dirty 3845.49 ms 1215.19 ms -2630.30 ms
b0d3373+dirty 3842.49 ms 1218.49 ms -2624.00 ms
b04af96+dirty 3830.54 ms 1206.11 ms -2624.44 ms
f9c1ed4+dirty 3842.09 ms 1220.70 ms -2621.40 ms
09a902f+dirty 3847.65 ms 1221.31 ms -2626.34 ms
44abcc2+dirty 3841.42 ms 1214.77 ms -2626.65 ms
acd838e+dirty 3835.94 ms 1215.87 ms -2620.07 ms
bfba737+dirty 3834.18 ms 1222.80 ms -2611.38 ms
ce7b368+dirty 3851.41 ms 1222.37 ms -2629.04 ms
4e0ba9c+dirty 3856.39 ms 1234.44 ms -2621.95 ms

App size

Revision Plain With Sentry Diff
64630e5+dirty 4.98 MiB 6.46 MiB 1.49 MiB
b0d3373+dirty 5.15 MiB 6.68 MiB 1.53 MiB
b04af96+dirty 4.98 MiB 6.54 MiB 1.56 MiB
f9c1ed4+dirty 4.98 MiB 6.50 MiB 1.53 MiB
09a902f+dirty 4.98 MiB 6.46 MiB 1.49 MiB
44abcc2+dirty 4.98 MiB 6.55 MiB 1.57 MiB
acd838e+dirty 5.15 MiB 6.70 MiB 1.55 MiB
bfba737+dirty 4.98 MiB 6.51 MiB 1.53 MiB
ce7b368+dirty 4.98 MiB 6.51 MiB 1.53 MiB
4e0ba9c+dirty 5.15 MiB 6.67 MiB 1.51 MiB

Previous results on branch: antonis/fix-6598-buffer-replay-orphaned

Startup times

Revision Plain With Sentry Diff
f55facc+dirty 3839.88 ms 1227.33 ms -2612.54 ms
1fcb382+dirty 3852.24 ms 1220.86 ms -2631.38 ms

App size

Revision Plain With Sentry Diff
f55facc+dirty 5.15 MiB 6.88 MiB 1.73 MiB
1fcb382+dirty 5.15 MiB 6.88 MiB 1.73 MiB

Comment thread packages/core/src/js/replay/mobilereplay.ts
Comment thread packages/core/test/replay/mobilereplay.test.ts
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Android (new) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 315.68 ms 304.60 ms -11.08 ms
Size 50.56 MiB 56.46 MiB 5.90 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
37a2091+dirty 429.71 ms 477.00 ms 47.29 ms
af33f3b+dirty 417.68 ms 448.04 ms 30.37 ms
c2e182c+dirty 468.50 ms 545.44 ms 76.94 ms
a50b33d+dirty 353.21 ms 398.48 ms 45.27 ms
5ca03f9+dirty 412.83 ms 459.40 ms 46.57 ms
f3215d3+dirty 396.53 ms 436.66 ms 40.13 ms
7887847+dirty 420.47 ms 460.55 ms 40.08 ms
23598c3+dirty 371.92 ms 420.65 ms 48.74 ms
bc8f61e+dirty 419.31 ms 453.39 ms 34.08 ms
09a902f+dirty 423.02 ms 472.18 ms 49.16 ms

App size

Revision Plain With Sentry Diff
37a2091+dirty 48.30 MiB 53.58 MiB 5.28 MiB
af33f3b+dirty 49.74 MiB 55.09 MiB 5.35 MiB
c2e182c+dirty 49.74 MiB 54.85 MiB 5.11 MiB
a50b33d+dirty 43.94 MiB 48.94 MiB 5.00 MiB
5ca03f9+dirty 49.74 MiB 55.26 MiB 5.52 MiB
f3215d3+dirty 48.30 MiB 53.49 MiB 5.19 MiB
7887847+dirty 49.74 MiB 54.81 MiB 5.07 MiB
23598c3+dirty 43.94 MiB 49.02 MiB 5.08 MiB
bc8f61e+dirty 49.74 MiB 55.09 MiB 5.35 MiB
09a902f+dirty 49.74 MiB 54.81 MiB 5.07 MiB

Previous results on branch: antonis/fix-6598-buffer-replay-orphaned

Startup times

Revision Plain With Sentry Diff
1fcb382+dirty 426.39 ms 462.00 ms 35.61 ms
f55facc+dirty 477.25 ms 517.52 ms 40.27 ms

App size

Revision Plain With Sentry Diff
1fcb382+dirty 50.56 MiB 56.46 MiB 5.90 MiB
f55facc+dirty 50.56 MiB 56.46 MiB 5.90 MiB

antonis and others added 2 commits September 8, 2026 11:57
…cache

Address review findings on the buffered-replay sampling fix:

- Android captureReplay now resolves with the scope's replayId (populated
  only when a replay was actually sent) instead of the controller's buffered
  id, so an on-error sampling miss resolves null, matching iOS.
- getCurrentReplayId still prefers the controller id for linking in beforeSend.
- Drop the bounded pending-flush set in favour of keying the flush decision on
  the event's own linked replay context, removing an eviction race.
- Re-read the current recording id when the native flush uploads nothing, so
  the cache no longer exposes an id that was never uploaded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@antonis
antonis marked this pull request as ready for review September 10, 2026 06:48

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

One question here (except for review comments): what happens if If two errors survive sampling simultaneously (or near-simultaneously, with a super minor difference in time)?

I think it would mean both will reach afterSendEvent before either flush completes and then both will call captureReplay.

Maybe some sort of guard needs to be added here.

// sent, so it stays empty while a buffer replay is recording (issue #6598).
final @NotNull SentryId controllerId =
Sentry.getCurrentScopes().getOptions().getReplayController().getReplayId();
if (controllerId != SentryId.EMPTY_ID) {

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.

Shouldn't it be something like that to avoid checking for object equality and check for value instead?

if (!SentryId.EMPTY_ID.equals(controllerId)) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. fixed with 649f560

// native `replaysOnErrorSampleRate` roll (which still happens at flush time in
// `captureReplay`). If that roll misses, the event carries a `replay_id` for a
// replay that is never uploaded. A fully-correct fix requires the native SDKs
// to decouple the on-error sampling decision from the buffer upload.

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.

If that roll misses, the event carries a replay_id for a replay that is never uploaded.

Maybe I'm wrong but seems like the fix introduced in this PR still produces orphaned replay_id links when replaysOnErrorSampleRate misses which sounds a bit like a bug, not really a trade off.
IMO it's better to fix it, or at least create an issue instead of writing it down as a block comment in production code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point 👍 Created an issue to follow up on this #6696

@antonis antonis removed Blocked ready-to-merge Triggers the full CI test suite labels Sep 10, 2026
Reference equality (`!=` / `==` against SentryId.EMPTY_ID) would misclassify
a freshly-constructed empty SentryId as non-empty, handing JS an all-zeros
replay id. Compare by value via SentryId.EMPTY_ID.equals(...) instead, which
is also null-safe as the left operand.

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

antonis commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

One question here (except for review comments): what happens if If two errors survive sampling simultaneously (or near-simultaneously, with a super minor difference in time)?

I think it would mean both will reach afterSendEvent before either flush completes and then both will call captureReplay.

Maybe some sort of guard needs to be added here.

Good question 👍 I traced both native paths and should be safe. Both errors share the same buffered replayId, and the captureReplay bridge calls run on a single serial thread on each platform (iOS methodQueue = main queue; Android's Promise @ReactMethod = native-modules thread), so they execute in sequence.

  • iOS: captureReplay() starts with guard !isFullSession else { return true }; the first call flips isFullSession before returning, so the second no-ops.
  • Android: the first call converts BufferCaptureStrategy → SessionCaptureStrategy, whose captureReplay is a no-op ("already running in 'session' mode").

Ready for another pass 🙇

@antonis antonis added the ready-to-merge Triggers the full CI test suite label Sep 10, 2026
@antonis
antonis requested a review from alwx September 10, 2026 10:28
@antonis
antonis merged commit 64964cd into main Sep 10, 2026
72 of 113 checks passed
@antonis
antonis deleted the antonis/fix-6598-buffer-replay-orphaned branch September 10, 2026 10:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-merge Triggers the full CI test suite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

On-error mobile replay orphaned when its error is dropped by sampleRate

2 participants