getsentry / getsentry/sentry-react-native

Buffered replay can leave an orphaned replay_id link on a replaysOnErrorSampleRate miss

Open
#6,696 1 comment 0 reactions 0 assignees View on GitHub
Improvement React-Native
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.