Skip to content

feat(core): Add frames.delay data from native SDKs#5907

Open
antonis wants to merge 14 commits intomainfrom
antonis/add-frames-delay-data
Open

feat(core): Add frames.delay data from native SDKs#5907
antonis wants to merge 14 commits intomainfrom
antonis/add-frames-delay-data

Conversation

@antonis
Copy link
Copy Markdown
Contributor

@antonis antonis commented Mar 27, 2026

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 Description

Adds frames.delay span data by fetching it from the native iOS and Android SDKs. The frames.delay metric 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.getFramesDelaySPI which accepts system timestamps and returns delayDuration. Wall-clock to system-time conversion is done in the bridge with underflow guards. No sentry-cocoa changes needed.

Android

Creates a new RNSentryFrameDelayCollector that implements SentryFrameMetricsCollector.FrameMetricsCollectorListener to collect per-frame delay data. Stores delayed frame records and provides getFramesDelay(startNanos, endNanos) to query accumulated delay within a time range. Handles partial frame overlap with query boundaries. Calls stop() before start() to prevent leaked listeners on JS bundle reload.

JS integration

frames.delay is attached to:

  • App start spans (app.start.cold / app.start.warm) — via appStart.ts processEvent
  • TTID/TTFD spans (ui.load.initial_display / ui.load.full_display) — via timetodisplay.tsx, using the actual span end timestamp (not Date.now())
  • All JS API-started spans — via nativeFrames.ts spanEnd hook

All 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.delay data 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?

  • All 1185 Jest tests pass (6 new tests added across nativeframes, appStart, timetodisplay)
  • Android native unit tests pass (./gradlew testDebugUnitTest)
  • iOS native build could not be verified locally (Xcode missing iOS 26.4 platform components) — relies on CI
  • Lint passes (yarn lint:lerna, yarn lint:android)

📝 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

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>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 27, 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).


  • feat(core): Add frames.delay data from native SDKs by antonis in #5907
  • chore(core): Deprecate FeedbackButton FAB APIs by antonis in #5933
  • Fix: Disable global prettier by lucas-zimerman in #5937
  • refactor(core): Rename FeedbackWidget to FeedbackForm by antonis in #5931
  • refactor(core): Extract playground modal styles to separate file by antonis in #5927
  • fix(ci): Avoid unnecessary runner allocation by splitting platform matrix into separate jobs by alwx in #5924
  • feat(core): Track shake to report integration usage by antonis in #5929
  • chore(deps): update CLI to v3.3.5 by github-actions in #5925
  • chore: Replace prettier with oxfmt by antonis in #5880
  • chore(deps): bump brace-expansion to ^5.0.5 by antonis in #5920
  • chore(deps): bump path-to-regexp to ^8.4.0 by antonis in #5919
  • chore: Migrate from ESLint to oxlint by antonis in #5867
  • chore(deps): bump yaml to ^2.8.3 by antonis in #5921
  • chore(deps): bump activesupport to >= 7.2.3.1 by antonis in #5922
  • fix(ci): Update validate-pr action to remove draft enforcement by stephanie-anderson in #5923
  • chore(deps): bump actions/checkout from 4 to 6 by dependabot in #5916
  • chore(deps): bump getsentry/craft from 2.25.0 to 2.25.2 by dependabot in #5918
  • chore(deps): bump getsentry/craft/.github/workflows/changelog-preview.yml from 2.25.0 to 2.25.2 by dependabot in #5914
  • chore(deps): bump github/codeql-action from 4.34.1 to 4.35.1 by dependabot in #5917
  • chore(deps): bump dorny/paths-filter from 3.0.2 to 4.0.1 by dependabot in #5915
  • fix: Prevent script injection vulnerability in platform-check action by fix-it-felix-sentry in #5913
  • chore(ios): Upgrade clang-format from v20 to v22 by antonis in #5905
  • chore: Add PR validation workflow by stephanie-anderson in #5906
  • chore(deps): bump brace-expansion from 1.1.12 to 1.1.13 by dependabot in #5909

Plus 7 more


🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 27, 2026

Fails
🚫 Pull request is not ready for merge, please add the "ready-to-merge" label to the pull request
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against 6c1518f

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 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.

Create PR

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.

antonis and others added 2 commits March 27, 2026 14:38
… 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>
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

antonis and others added 2 commits March 27, 2026 15:07
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>
@antonis antonis marked this pull request as ready for review March 27, 2026 14:08
@antonis antonis added the ready-to-merge Triggers the full CI test suite label Mar 27, 2026
@antonis antonis marked this pull request as draft March 27, 2026 14:18
Copy link
Copy Markdown
Contributor Author

@antonis antonis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 27, 2026

Android (legacy) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 523.43 ms 516.69 ms -6.73 ms
Size 43.75 MiB 48.08 MiB 4.33 MiB

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 27, 2026

iOS (legacy) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1217.20 ms 1222.34 ms 5.14 ms
Size 3.38 MiB 4.73 MiB 1.35 MiB

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 27, 2026

iOS (new) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1207.11 ms 1217.45 ms 10.34 ms
Size 3.38 MiB 4.73 MiB 1.35 MiB

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 27, 2026

Android (new) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 452.71 ms 472.36 ms 19.65 ms
Size 43.94 MiB 48.94 MiB 5.00 MiB

antonis and others added 3 commits March 30, 2026 08:46
…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>
@antonis antonis marked this pull request as ready for review March 30, 2026 07:40
@antonis antonis marked this pull request as draft March 30, 2026 12:14
@antonis antonis marked this pull request as ready for review March 30, 2026 12:14
@antonis antonis removed the ready-to-merge Triggers the full CI test suite label Mar 30, 2026
…lay-data

# Conflicts:
#	packages/core/src/js/tracing/timetodisplay.tsx
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.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

@lucas-zimerman
Copy link
Copy Markdown
Collaborator

I was going to fix this file but locally it's change too many lines,
would you be able to do the following fix too?

  In appStart.ts:167:                                                                                                                  
  if (frames.totalFrames <= 0 && frames.slowFrames <= 0 && frames.totalFrames <= 0) {                                                  
                                                                                                             vvvvvvvvvvvvvvvvvvv
  if (frames.totalFrames <= 0 && frames.slowFrames <= 0 && frames.frozenFrames <= 0) {                                                 

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>
@antonis
Copy link
Copy Markdown
Contributor Author

antonis commented Mar 31, 2026

totalFrames was duplicated and it's probably a typo.

Good catch @lucas-zimerman 👍 Fixed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add frames delay data from native

2 participants