feat(core): Add frames.delay data from native SDKs#5907
feat(core): Add frames.delay data from native SDKs#5907
Conversation
Fetch frames.delay from native (iOS via getFramesDelaySPI, Android via SentryFrameMetricsCollector listener) and attach it to app start spans, TTID/TTFD spans, and all JS API-started spans. Closes #4869 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Semver Impact of This PR⚪ None (no version bump detected) 📋 Changelog PreviewThis is how your changes will appear in the changelog.
Plus 7 more 🤖 This preview updates automatically when you update the PR. |
|
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: iOS nil result causes false zero delay report
- Added a nil guard for the SPI result before checking delayDuration so missing native data now resolves as nil instead of 0.
- ✅ Fixed: Leaked listener on repeated start without stop
- Updated frame delay collector start() to call stop() first, ensuring previous listener registrations are removed before re-registering.
Or push these changes by commenting:
@cursor push 932a4c0567
Preview (932a4c0567)
diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryFrameDelayCollector.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryFrameDelayCollector.java
--- a/packages/core/android/src/main/java/io/sentry/react/RNSentryFrameDelayCollector.java
+++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryFrameDelayCollector.java
@@ -31,6 +31,7 @@
if (frameMetricsCollector == null) {
return false;
}
+ stop();
this.collector = frameMetricsCollector;
this.listenerId = frameMetricsCollector.startCollection(this);
return this.listenerId != null;
diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm
--- a/packages/core/ios/RNSentry.mm
+++ b/packages/core/ios/RNSentry.mm
@@ -572,7 +572,7 @@
SentryFramesDelayResultSPI *result = [framesTracker getFramesDelaySPI:startSystemTime
endSystemTimestamp:endSystemTime];
- if (result.delayDuration >= 0) {
+ if (result != nil && result.delayDuration >= 0) {
resolve(@(result.delayDuration));
} else {
resolve(nil);This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
packages/core/android/src/main/java/io/sentry/react/RNSentryFrameDelayCollector.java
Show resolved
Hide resolved
… Android - iOS: Add nil check on getFramesDelaySPI result before accessing delayDuration (messaging nil returns 0 in ObjC, causing false frames.delay: 0) - Android: Call stop() before start() in RNSentryFrameDelayCollector to prevent leaked listeners on repeated initialization (e.g. JS bundle reload) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…TTFD Pass the intended span end timestamp into captureEndFramesAndAttachToSpan instead of falling back to Date.now(). The function runs before span.end() is called, so spanToJSON(span).timestamp is always undefined, causing the delay calculation to include frame data after the span semantically ended. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ID/TTFD and app start Wrap NATIVE.fetchNativeFramesDelay calls in timetodisplay.tsx and appStart.ts with Promise.race timeout (2s), matching the timeout protection already present in nativeFrames.ts. Prevents indefinite blocking if the native bridge hangs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Gate frames.delay fetch on appStartEndData.endFrames being present, matching the native SDK behavior where both frame counts and delay are gated on the frames tracker running. Prevents attaching frames.delay to spans that have no frames.total/slow/frozen data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
packages/core/android/src/main/java/io/sentry/react/RNSentryFrameDelayCollector.java
Show resolved
Hide resolved
antonis
left a comment
There was a problem hiding this comment.
Converting back to draft to fix the CI and bot review feedback
… build The SPI types (SentryFramesTracker, SentryCurrentDateProvider, SentryFramesDelayResultSPI) are only accessible via `@import Sentry;` which SentryScreenFramesWrapper.m already uses. RNSentry.mm only imports individual headers and cannot see these Swift SPI types. Move the frames delay computation into a new +framesDelayForStartTimestamp:endTimestamp: method on SentryScreenFramesWrapper and call it from RNSentry.mm. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Android (legacy) Performance metrics 🚀
|
iOS (legacy) Performance metrics 🚀
|
iOS (new) Performance metrics 🚀
|
Android (new) Performance metrics 🚀
|
…mesIntegration Move frames.delay fetch inside the `if (totalFrames > 0 || ...)` guard so it's only attached when frame count data is also present. Prevents spans from having frames.delay without frames.total. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lay-data # Conflicts: # packages/core/src/js/tracing/timetodisplay.tsx
packages/core/android/src/main/java/io/sentry/react/RNSentryFrameDelayCollector.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java
Show resolved
Hide resolved
|
I was going to fix this file but locally it's change too many lines, totalFrames was duplicated and it's probably a typo. |
- Resolve CHANGELOG conflict (keep both entries) - Fix pre-existing typo: frames.totalFrames checked twice instead of frames.frozenFrames in appStart.ts attachFrameDataToSpan - Add Android underflow guard for system-time conversion to match iOS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Good catch @lucas-zimerman 👍 Fixed |


📢 Type of change
📜 Description
Adds
frames.delayspan data by fetching it from the native iOS and Android SDKs. Theframes.delaymetric measures how much longer frames took to render than expected (sum of per-frame delays beyond the expected frame duration during a span's lifetime).New native bridge method:
fetchNativeFramesDelay(startTimestampSeconds, endTimestampSeconds)returns the accumulated delay in seconds for a given time range.iOS
Uses sentry-cocoa's existing
SentryFramesTracker.getFramesDelaySPIwhich accepts system timestamps and returnsdelayDuration. Wall-clock to system-time conversion is done in the bridge with underflow guards. No sentry-cocoa changes needed.Android
Creates a new
RNSentryFrameDelayCollectorthat implementsSentryFrameMetricsCollector.FrameMetricsCollectorListenerto collect per-frame delay data. Stores delayed frame records and providesgetFramesDelay(startNanos, endNanos)to query accumulated delay within a time range. Handles partial frame overlap with query boundaries. Callsstop()beforestart()to prevent leaked listeners on JS bundle reload.JS integration
frames.delayis attached to:app.start.cold/app.start.warm) — viaappStart.tsprocessEventui.load.initial_display/ui.load.full_display) — viatimetodisplay.tsx, using the actual span end timestamp (notDate.now())nativeFrames.tsspanEnd hookAll call sites have timeout protection (2s) to prevent blocking if the native bridge hangs. Both platforms include underflow guards for wall-clock to system-time timestamp conversion. iOS includes a nil guard on the SPI result.
💡 Motivation and Context
Closes #4869
The
frames.delaydata is already computed by the native SDKs for their own spans. This PR exposes it to the React Native SDK so it can be attached to RN-managed spans, matching the behavior of native iOS and Android apps.Sub-issues: #4931 (app start), #4932 (TTID/TTFD), #4933 (API spans)
💚 How did you test it?
./gradlew testDebugUnitTest)yarn lint:lerna,yarn lint:android)📝 Checklist
sendDefaultPIIis enabled🔮 Next steps
RNSentryFrameDelayCollectorwith a queryable API from sentry-java (similar to iOS'sgetFramesDelaySPI) once available. This would eliminate duplicate per-frame listener overhead.