Skip to content

fix(replay): Check captureReplay return value in iOS bridge#6008

Open
antonis wants to merge 13 commits intomainfrom
fix/replay-capture-return-value
Open

fix(replay): Check captureReplay return value in iOS bridge#6008
antonis wants to merge 13 commits intomainfrom
fix/replay-capture-return-value

Conversation

@antonis
Copy link
Copy Markdown
Contributor

@antonis antonis commented Apr 16, 2026

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 Description

The iOS bridge was calling [PrivateSentrySDKOnly captureReplay] which is void — it discards the BOOL return value from the underlying SentrySessionReplayIntegration.captureReplay(). It then always returned the replay ID via getReplayId, even when the capture actually failed (e.g., replay display link not running, session ended, or sampling rejected).

This caused the JS side to receive a non-null replay ID, attach it as replay_id to error events, but the actual replay data was never captured and sent to Sentry. The error event in the dashboard would reference a replay that doesn't exist.

What changed

iOS bridge (RNSentry.mm): Added captureReplayWithReturnValue class method that calls the integration's captureReplay() directly (via methodForSelector:) to get the BOOL return value. The bridge now:

  • Returns the replay ID only when capture succeeds (YES)
  • Returns nil when capture fails (NO), allowing the JS fallback logic in mobilereplay.ts to handle it correctly

How the Cocoa SDK's captureReplay() works

public func captureReplay() -> Bool {
    guard isRunning else { return false }           // Display link must be active
    guard !isFullSession else { return true }        // Already recording
    guard delegate?.sessionReplayShouldCaptureReplayForError() == true else { return false }
    startFullReplay()                                // Convert buffer → full session
    createAndCaptureInBackground(...)                // Actually capture & send
    return true
}

Previously, the bridge ignored this return value and always returned a (potentially stale) replay ID from buffer mode.

💡 Motivation and Context

Fixes #5074

Customers reported that error replay sessions are not uploaded when using sentry-label with reactNavigationIntegration. Investigation revealed that the iOS bridge was masking native-side capture failures by always returning a replay ID. While this alone may not fully explain the customer's issue (the root cause of why captureReplay() returns false in their specific scenario needs further investigation), this fix ensures the JS layer gets accurate information about whether replay data was actually captured.

💚 How did you test it?

  • Added 3 JS tests in mobilereplay.test.ts verifying behavior when captureReplay returns null vs a replay ID
  • Added 2 iOS native tests in RNSentryTests.m verifying captureReplayWithReturnValue returns NO when replay is not available
  • All 1278 JS tests pass
  • All linters pass
  • iOS build compiles cleanly (verified via xcodebuildRNSentry.mm has no compilation errors)

📝 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
  • No breaking changes

🔮 Next steps

  • Investigate why the native captureReplay() fails in the customer's specific scenario (possibly the display link is paused during a session restart race condition)
  • Consider a similar fix for the Android bridge (RNSentryModuleImpl.java), where captureReplay also doesn't check the return value
  • Ask the customer to test with debug: true and this fix to get native-level [Session Replay] logs showing exactly why capture fails

The iOS bridge was calling `[PrivateSentrySDKOnly captureReplay]` which
is void and discards the BOOL result from the underlying
`SentrySessionReplayIntegration.captureReplay()`. It then always returned
the replay ID via `getReplayId`, even when the capture actually failed
(e.g., replay display link not running). This caused the JS side to
attach a `replay_id` to error events pointing to replay data that was
never captured and sent.

Now we call the integration's `captureReplay()` directly to get the BOOL
return value. The bridge only returns a replay ID when capture succeeds.
When capture fails, it returns nil, letting the JS fallback logic handle
it correctly.

Fixes #5074

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 16, 2026

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): Check captureReplay return value in iOS bridge by antonis in #6008
  • chore(deps): update Android SDK to v8.39.1 by github-actions in #6010
  • chore(deps): update JavaScript SDK to v10.49.0 by github-actions in #6011
  • ci: Integrate Warden for AI-powered PR code review by antonis in #6003
  • chore(lint): Fixes lint issue on main by antonis in #6013
  • feat(expo): Warn when prebuilt native projects are missing Sentry config by alwx in #5984

🤖 This preview updates automatically when you update the PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread packages/core/ios/RNSentry.mm Outdated
Addresses review feedback: if getReplayIntegration selector isn't found
on PrivateSentrySDKOnly (e.g., future Cocoa SDK changes), fall back to
the old void captureReplay call to avoid silently breaking replay capture.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor bot left a comment

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 49737b4. Configure here.

@antonis antonis marked this pull request as ready for review April 16, 2026 13:46
Comment thread packages/core/ios/RNSentry.mm
@antonis antonis added the ready-to-merge Triggers the full CI test suite label Apr 17, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 17, 2026

iOS (legacy) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1206.48 ms 1206.60 ms 0.13 ms
Size 3.38 MiB 4.76 MiB 1.38 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
3817909+dirty 1183.90 ms 1187.50 ms 3.60 ms
3ce5254+dirty 1219.93 ms 1221.90 ms 1.96 ms
04207c4+dirty 1191.27 ms 1189.78 ms -1.48 ms
7ac3378+dirty 1213.37 ms 1218.15 ms 4.78 ms
5c1e987+dirty 1204.30 ms 1222.15 ms 17.85 ms
0d9949d+dirty 1211.38 ms 1219.67 ms 8.29 ms
4953e94+dirty 1212.06 ms 1214.83 ms 2.77 ms
df5d108+dirty 1225.90 ms 1220.14 ms -5.76 ms
a50b33d+dirty 1197.74 ms 1197.17 ms -0.57 ms
3d377b5+dirty 1218.48 ms 1219.51 ms 1.03 ms

App size

Revision Plain With Sentry Diff
3817909+dirty 3.38 MiB 4.73 MiB 1.35 MiB
3ce5254+dirty 3.38 MiB 4.76 MiB 1.38 MiB
04207c4+dirty 3.38 MiB 4.76 MiB 1.38 MiB
7ac3378+dirty 3.38 MiB 4.76 MiB 1.38 MiB
5c1e987+dirty 3.38 MiB 4.73 MiB 1.35 MiB
0d9949d+dirty 3.38 MiB 4.76 MiB 1.38 MiB
4953e94+dirty 3.38 MiB 4.73 MiB 1.35 MiB
df5d108+dirty 3.38 MiB 4.73 MiB 1.35 MiB
a50b33d+dirty 3.38 MiB 4.73 MiB 1.35 MiB
3d377b5+dirty 3.38 MiB 4.76 MiB 1.38 MiB

@sentry
Copy link
Copy Markdown

sentry bot commented Apr 17, 2026

📲 Install Builds

Android

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

⚙️ sentry-react-native Build Distribution Settings

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 17, 2026

iOS (new) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1231.77 ms 1228.91 ms -2.86 ms
Size 3.38 MiB 4.76 MiB 1.38 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
3817909+dirty 1210.76 ms 1215.64 ms 4.89 ms
3ce5254+dirty 1217.70 ms 1224.69 ms 6.99 ms
04207c4+dirty 1228.55 ms 1226.04 ms -2.51 ms
7ac3378+dirty 1202.35 ms 1198.31 ms -4.04 ms
5c1e987+dirty 1208.43 ms 1220.72 ms 12.29 ms
0d9949d+dirty 1203.94 ms 1202.27 ms -1.67 ms
4953e94+dirty 1217.41 ms 1223.53 ms 6.12 ms
df5d108+dirty 1207.34 ms 1210.50 ms 3.16 ms
a50b33d+dirty 1207.11 ms 1212.10 ms 5.00 ms
3d377b5+dirty 1201.55 ms 1201.80 ms 0.25 ms

App size

Revision Plain With Sentry Diff
3817909+dirty 3.38 MiB 4.73 MiB 1.35 MiB
3ce5254+dirty 3.38 MiB 4.76 MiB 1.38 MiB
04207c4+dirty 3.38 MiB 4.76 MiB 1.38 MiB
7ac3378+dirty 3.38 MiB 4.76 MiB 1.38 MiB
5c1e987+dirty 3.38 MiB 4.73 MiB 1.35 MiB
0d9949d+dirty 3.38 MiB 4.76 MiB 1.38 MiB
4953e94+dirty 3.38 MiB 4.73 MiB 1.35 MiB
df5d108+dirty 3.38 MiB 4.73 MiB 1.35 MiB
a50b33d+dirty 3.38 MiB 4.73 MiB 1.35 MiB
3d377b5+dirty 3.38 MiB 4.76 MiB 1.38 MiB

antonis and others added 2 commits April 17, 2026 14:54
The new captureReplayWithReturnValue method is declared in RNSentry+Test.h
but this header wasn't imported by RNSentryTests.m, causing iOS CI to fail
with "no known class method for selector 'captureReplayWithReturnValue'".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…y/sentry-react-native into fix/replay-capture-return-value
Comment thread packages/core/ios/RNSentry.mm Outdated
Addresses review feedback: the fallback path in captureReplayWithReturnValue
was calling PrivateSentrySDKOnly methods outside the @Try block. If these
calls throw, the exception would propagate up to the bridge and leave the
JS promise unresolved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor bot left a comment

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 1cdb266. Configure here.

Comment thread packages/core/ios/RNSentry.mm Outdated
clang-format was splitting @Try into "@" and "try {" on separate lines
in the fallback path. While clang accepts this, it's unusual and breaks
grep discoverability. Wrapped with clang-format off/on pragmas to
preserve single-line formatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

Unable to get replay session when integrating both reactNavigationIntegration() & mobileReplayIntegration()

1 participant