fix(replay): Defer buffered replay upload until after error sampling - #6685
Draft
antonis wants to merge 3 commits into
Draft
fix(replay): Defer buffered replay upload until after error sampling#6685antonis wants to merge 3 commits into
antonis wants to merge 3 commits into
Conversation
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>
Contributor
Semver Impact of This PR⚪ None (no version bump detected) 📋 Changelog PreviewThis is how your changes will appear in the changelog.
🤖 This preview updates automatically when you update the PR. |
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
antonis
force-pushed
the
antonis/fix-6598-buffer-replay-orphaned
branch
from
September 8, 2026 09:05
04a8df5 to
04f6aa8
Compare
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 04f6aa8. Configure here.
Contributor
Author
|
Blocking till getsentry/sentry-cocoa#8976 is released |
📲 Install BuildsAndroid
|
Contributor
iOS (legacy) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 64630e5+dirty | 3842.70 ms | 1218.11 ms | -2624.60 ms |
| b0d3373+dirty | 3831.75 ms | 1227.29 ms | -2604.46 ms |
| b04af96+dirty | 3818.92 ms | 1219.76 ms | -2599.16 ms |
| 3d31fcf+dirty | 3838.09 ms | 1223.46 ms | -2614.63 ms |
| a0a3177+dirty | 3844.73 ms | 1225.23 ms | -2619.51 ms |
| af33f3b+dirty | 3849.98 ms | 1236.45 ms | -2613.53 ms |
| 09a902f+dirty | 3835.67 ms | 1217.11 ms | -2618.57 ms |
| 5a316ea+dirty | 3820.11 ms | 1211.28 ms | -2608.83 ms |
| acd838e+dirty | 3849.78 ms | 1230.00 ms | -2619.78 ms |
| c2e182c+dirty | 3848.40 ms | 1211.79 ms | -2636.61 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 |
| 3d31fcf+dirty | 4.98 MiB | 6.56 MiB | 1.58 MiB |
| a0a3177+dirty | 4.98 MiB | 6.55 MiB | 1.58 MiB |
| af33f3b+dirty | 4.98 MiB | 6.51 MiB | 1.54 MiB |
| 09a902f+dirty | 4.98 MiB | 6.46 MiB | 1.49 MiB |
| 5a316ea+dirty | 4.98 MiB | 6.51 MiB | 1.53 MiB |
| acd838e+dirty | 5.15 MiB | 6.70 MiB | 1.55 MiB |
| c2e182c+dirty | 4.98 MiB | 6.50 MiB | 1.52 MiB |
Contributor
Android (legacy) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 68672fc+dirty | 425.02 ms | 487.56 ms | 62.54 ms |
| 71abba0+dirty | 496.54 ms | 525.16 ms | 28.63 ms |
| 7ff4d0f+dirty | 413.81 ms | 450.64 ms | 36.83 ms |
| 9ad4522+dirty | 420.73 ms | 444.18 ms | 23.45 ms |
| 0a147b2+dirty | 464.71 ms | 538.81 ms | 74.09 ms |
| d2eadf8+dirty | 414.64 ms | 454.56 ms | 39.92 ms |
| 57e0069+dirty | 413.82 ms | 453.47 ms | 39.65 ms |
| 26843eb+dirty | 532.15 ms | 624.13 ms | 91.98 ms |
| 9210ae6+dirty | 475.41 ms | 525.24 ms | 49.84 ms |
| 1e5d96d+dirty | 519.43 ms | 543.62 ms | 24.19 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 68672fc+dirty | 48.30 MiB | 53.61 MiB | 5.31 MiB |
| 71abba0+dirty | 48.30 MiB | 53.49 MiB | 5.19 MiB |
| 7ff4d0f+dirty | 48.30 MiB | 53.60 MiB | 5.30 MiB |
| 9ad4522+dirty | 49.74 MiB | 55.38 MiB | 5.63 MiB |
| 0a147b2+dirty | 49.74 MiB | 55.08 MiB | 5.34 MiB |
| d2eadf8+dirty | 48.30 MiB | 53.48 MiB | 5.18 MiB |
| 57e0069+dirty | 49.74 MiB | 54.85 MiB | 5.11 MiB |
| 26843eb+dirty | 49.74 MiB | 55.26 MiB | 5.52 MiB |
| 9210ae6+dirty | 48.30 MiB | 53.54 MiB | 5.23 MiB |
| 1e5d96d+dirty | 49.74 MiB | 54.81 MiB | 5.07 MiB |
Contributor
iOS (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 68ae91b+dirty | 3836.36 ms | 1222.04 ms | -2614.32 ms |
| 0b5a379+dirty | 3857.69 ms | 1230.34 ms | -2627.35 ms |
| bf168a4+dirty | 3846.83 ms | 1221.33 ms | -2625.49 ms |
| f170ec3+dirty | 3844.74 ms | 1222.67 ms | -2622.07 ms |
| 68672fc+dirty | 3832.22 ms | 1228.29 ms | -2603.93 ms |
| 84cba31+dirty | 3830.40 ms | 1204.44 ms | -2625.96 ms |
| 2c735cc+dirty | 1223.33 ms | 1224.38 ms | 1.04 ms |
| b9bebee+dirty | 3858.02 ms | 1231.92 ms | -2626.11 ms |
| a50b33d+dirty | 1207.11 ms | 1212.10 ms | 5.00 ms |
| 5569641+dirty | 3824.35 ms | 1210.78 ms | -2613.57 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 68ae91b+dirty | 4.98 MiB | 6.46 MiB | 1.48 MiB |
| 0b5a379+dirty | 5.15 MiB | 6.70 MiB | 1.54 MiB |
| bf168a4+dirty | 4.98 MiB | 6.47 MiB | 1.49 MiB |
| f170ec3+dirty | 5.15 MiB | 6.69 MiB | 1.53 MiB |
| 68672fc+dirty | 5.15 MiB | 6.71 MiB | 1.55 MiB |
| 84cba31+dirty | 4.98 MiB | 6.55 MiB | 1.57 MiB |
| 2c735cc+dirty | 3.38 MiB | 4.74 MiB | 1.35 MiB |
| b9bebee+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| a50b33d+dirty | 3.38 MiB | 4.73 MiB | 1.35 MiB |
| 5569641+dirty | 5.15 MiB | 6.67 MiB | 1.51 MiB |
Contributor
Android (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| eb93136+dirty | 500.37 ms | 532.58 ms | 32.21 ms |
| 9210ae6+dirty | 444.14 ms | 459.80 ms | 15.66 ms |
| 94af3bd+dirty | 413.04 ms | 451.76 ms | 38.71 ms |
| 7d6fd3a+dirty | 435.06 ms | 458.78 ms | 23.72 ms |
| 15d4514+dirty | 413.63 ms | 449.62 ms | 35.99 ms |
| 9ad4522+dirty | 432.43 ms | 450.91 ms | 18.49 ms |
| 0a147b2+dirty | 442.80 ms | 522.24 ms | 79.44 ms |
| 7887847+dirty | 420.47 ms | 460.55 ms | 40.08 ms |
| a3265b6+dirty | 410.96 ms | 444.76 ms | 33.80 ms |
| 57e0069+dirty | 442.25 ms | 486.64 ms | 44.39 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| eb93136+dirty | 48.30 MiB | 53.58 MiB | 5.28 MiB |
| 9210ae6+dirty | 48.30 MiB | 53.54 MiB | 5.23 MiB |
| 94af3bd+dirty | 48.30 MiB | 53.57 MiB | 5.26 MiB |
| 7d6fd3a+dirty | 43.94 MiB | 49.00 MiB | 5.06 MiB |
| 15d4514+dirty | 48.30 MiB | 53.60 MiB | 5.30 MiB |
| 9ad4522+dirty | 49.74 MiB | 55.38 MiB | 5.63 MiB |
| 0a147b2+dirty | 49.74 MiB | 55.08 MiB | 5.34 MiB |
| 7887847+dirty | 49.74 MiB | 54.81 MiB | 5.07 MiB |
| a3265b6+dirty | 48.30 MiB | 53.58 MiB | 5.28 MiB |
| 57e0069+dirty | 49.74 MiB | 54.85 MiB | 5.11 MiB |
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📢 Type of change
📜 Description
In buffer / on-error Session Replay mode (
replaysOnErrorSampleRate), the buffered replay was flushed inside the mobile-replay integration'sbeforeSendwrapper. Since@sentry/core10.70.0 the errorsampleRateroll runs afterbeforeSend, so a replay was uploaded even for errors that were then dropped bysampleRate— orphaning the replay and burning replay quota.The work is split in two:
tagEventWithReplayId(in thebeforeSendwrapper) only links the event to the current buffered replay id — a read viaNATIVE.getCurrentReplayId(), no flush.flushReplayForSentEvent(in a newafterSendEventclient hook) performs the nativecaptureReplay()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:
getCurrentReplayId()now prefersReplayController.getReplayId()(assigned at record start) and falls back to the scope. Self-contained — sentry-java already exposes it.SentrySDK.internal.replay.replayId(fix(replay): Return buffered replay id from internal hybrid API sentry-cocoa#8976). Untilsentry_cocoa_versioninRNSentry.podspecis 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?
mobilereplay.test.tsto assert the new split (no flush inbeforeSend; flush inafterSendEvent).mobilereplay.sampling.test.ts, which drives a real@sentry/coreclient and provessampleRate: 0→captureReplayis never called.RNSentryReplayIdTest.javacovering the controller-firstgetCurrentReplayId()lookup (4 cases).yarn build:sdk,yarn circularDepCheck, oxlint, and Java format all clean.📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps