getsentry / getsentry/sentry-react-native
Buffered replay can leave an orphaned replay_id link on a replaysOnErrorSampleRate miss
- Dominant language
- TypeScript
- Stars
- 1.8k
- Forks
- 366
- Avg merge
- 1d 9h
- Merged PRs (30d)
- 89
Description
### Background
PR #6685 fixed #6598 (a buffered/on-error replay was uploaded for an error later dropped by error `sampleRate`, orphaning the replay). Since `@sentry/core` 10.70.0 (getsentry/sentry-javascript#22819) the error `sampleRate` roll runs **after** `beforeSend`, so the fix splits the work:
1. `tagEventWithReplayId` (in the `beforeSend` wrapper) links the event to the buffered replay id — **no flush**.
2. `flushReplayForSentEvent` (in `afterSendEvent`, which only fires for events that survive sampling) performs the native flush via `NATIVE.captureReplay()`.
This fully fixes the reported case. This issue tracks a **narrower residual gap** that the split introduces.
### The gap
The buffered replay id we tag with in `beforeSend` is *provisional*: native assigns it when recording starts, but whether the buffer is actually uploaded depends on the `replaysOnErrorSampleRate` roll, which happens later inside the async `captureReplay()` flush.
When an error **survives error `sampleRate`** but the **`replaysOnErrorSampleRate` roll misses**, the event has already been serialized and sent carrying `contexts.replay.replay_id`, while no replay is ever uploaded → the event links to a replay that doesn't exist.
Only occurs when `replaysOnErrorSampleRate` is set below `1.0`. Impact is limited to a dangling link (the event's Replay tab resolves to nothing); no replay data or quota is wasted.
### Steps to reproduce
1. Configure mobile replay in buffer/on-error mode with `replaysOnErrorSampleRate` between 0 and 1 (e.g. `0.5`) and error `sampleRate: 1.0`.
2. Trigger errors until one hits the `replaysOnErrorSampleRate` miss branch.
3. Observe the error event in Sentry carries a `replay_id` but the referenced replay was never uploaded.
### Why it isn't fixable in the JS layer alone
The pipeline order is fixed by `@sentry/core`:
```
beforeSend → [error sampleRate gate] → serialize + send → afterSendEvent
(tag here) (flush here)
```
- Tagging must happen **before send** (the link must be in the outgoing event).
- Flushing must happen **after the sampleRate gate** (flushing earlier reintroduces #6598).
- So the flush — and the `replaysOnErrorSampleRate` roll inside it — can only run in `afterSendEvent`, **after** the event is already sent. The `replay_id` is gone over the wire and can't be conditionally removed.
- The only hook between the gate and transmission is `beforeEnvelope`, but `captureReplay()` is an **async** bridge call and can't block the send. A synchronous native flush isn't viable (it does I/O; the old-arch bridge has no sync methods).
### Proposed fix (native)
Provide a way for the native SDKs (sentry-cocoa / sentry-android) to **decouple the on-error sampling decision from the buffer upload**, so RN can reconcile tag and upload. Either:
- **(a)** expose the `replaysOnErrorSampleRate` decision as a synchronous, side-effect-free query the RN layer can call in `beforeSend` (tag only when it will capture), the decision being cached so the later flush honors the same result; or
- **(b)** decouple the decision from the upload so the tag and the eventual upload always agree.
### Relationship to #6598
This is the **opposite** orphaning direction from #6598, and gated by a **different** sample rate:
| | #6598 (fixed) | #6696 (this issue) |
|---|---|---|
| Orphan direction | replay with **no event** | event with **no replay** |
| Caused by | the bug (flush before sampling) | the fix's optimistic tagging |
| Gate | `sampleRate < 1` | `replaysOnErrorSampleRate < 1` |
| Severity | quota waste + orphaned replay | harmless dangling link |
| Scope | broad (any dropped error) | narrow (survives `sampleRate` **and** replay roll misses) |
The fix in #6685 traded a broad, quota-costing orphaned *replay* for this rare, harmless orphaned *link*.
### References
- Fix PR: #6685
- Original issue: #6598
- Upstream ordering change: getsentry/sentry-javascript#22819
- Code: `packages/core/src/js/replay/mobilereplay.ts` (`tagEventWithReplayId`, `flushReplayForSentEvent`)
Contributor guide
Research direction
Start with packages/core/src/js/replay/mobilereplay.ts, especially tagEventWithReplayId and flushReplayForSentEvent, and trace the beforeSend-to-afterSendEvent pipeline described in the issue. Reproduce with buffered/on-error mobile replay, replaysOnErrorSampleRate below 1, and error sampleRate 1.0. Done means the event is not linked to a replay unless the native SDK will upload that replay, without regressing #6598.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, ios, react-native, typescript
- Domain
- mobile-dev
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100