Skip to content

fix(profiling): Drive the native UI profiler from JS - #6916

Merged
alwx merged 9 commits into
mainfrom
alwx/bugfix/ui-profiling-start-stop
Oct 22, 2026
Merged

alwx merged 9 commits into
mainfrom
alwx/bugfix/ui-profiling-start-stop

Conversation

@alwx

@alwx alwx commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 Description

This PR adds the startProfiler and stopProfiler bridge methods, and a default ProfilingIntegration that uses them to control the native UI profiler from JS. The native SDKs now always use the manual lifecycle. In trace mode, the integration runs the profiler while a sampled root span is active, and it calls stop one 60 second chunk after the last root span ends. In manual mode, it follows Sentry.profiler.startProfiler() and stopProfiler(), which are now exported.

Each transaction gets the profiler_id of the profile session that ran during its root span, and every span gets the native thread.id of the JS thread, so Sentry opens the profile on that thread. On iOS, sentry-cocoa has no public API for the profiler id, so iOS transactions are not linked to the profile yet.

💡 Motivation and Context

UI profiling from #5518 ran only for the app start profile. The native trace lifecycle needs native transactions, but React Native turns off native tracing. The manual lifecycle had no API that JS could call.

💚 How did you test it?

New Jest tests for the integration, the wrapper, and the default integrations. All JS tests, yarn lint, the build, and the API report pass. The updated Android RNSentryProfilingOptionsTest and the iOS RNSentryInitNativeSdkTests pass locally. I checked the change end to end on an Android 16 emulator with a test app and a Sentry project. Transactions get the profiler_id and the JS thread id, and the profile chunks cover the transactions on that thread.

Example with this PR and the stock sentry-java 8.61.0 (Android 16 emulator, trace lifecycle). The transactions single, first and second have the profiler_id and the JS thread.id, and the profile has samples on the JS thread during each of them:

📝 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.
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec.
  • No breaking changes.

🔮 Next steps

sentry-cocoa must expose the current profiler id, so that iOS transactions can link to the profile. In sentry-java (both profilers) and in sentry-cocoa, a start call that comes before a requested stop takes effect is ignored. The delayed stop in trace mode works around this for back-to-back spans, but a start in the window between the stop call and the end of the chunk still loses the rest of the profile. The native fixes are in getsentry/sentry-java#6291 and getsentry/sentry-cocoa#9401.

alwx added a commit that referenced this pull request Oct 20, 2026
Replace the PR number placeholder in the CHANGELOG entry with #6916.
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

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(profiling): Drive the native UI profiler from JS by alwx in #6916
  • chore(deps): update Android SDK to v8.61.1 by github-actions in #6929
  • ci: Pin Java 17 for the Android dependency updater by antonis in #6928
  • feat(tracing): Connect React Native traces with embedded WebViews by antonis in #6794
  • refactor(ios): Use internal Cocoa serialization by philprime in #6917
  • chore(deps): update CLI to v3.8.1 by github-actions in #6924
  • chore(deps): update Cocoa SDK to v9.31.0 by github-actions in #6925
  • fix(tracing): Respect enableHTTPTimings and stop leaking HTTP timing listeners by antonis in #6912
  • fix(core): Stop wrapping RNSentry in TurboModuleContext by default by alwx in #6921
  • chore(deps): bump launch-editor from 2.14.1 to 2.14.2 by dependabot in #6922
  • ci: Fix Linux jobs on Ubuntu 26.04 runners by antonis in #6920
  • fix(core): Match Sentry package paths with Windows separators in Metro resolvers by alwx in #6915
  • chore(e2e): Update Expo sample to SDK 58 by antonis in #6914
  • chore(e2e): Bump E2E tests to React Native 0.88.0 by antonis in #6899
  • fix(ios): Find .xcode.env in the debug symbols phase under SwiftPM by alwx in #6911
  • chore(deps): update React Native to v0.88.0 by github-actions in #6903
  • chore(deps): bump getsentry/craft from 2.34.1 to 2.35.0 by dependabot in #6906
  • chore(deps): bump actions/create-github-app-token from 3.2.0 to 3.3.0 by dependabot in #6908
  • chore(deps): bump getsentry/craft/.github/workflows/changelog-preview.yml from 2.33.1 to 2.35.0 by dependabot in #6907
  • chore(sample): Bump React Native sample to 0.88.0 by antonis in #6910
  • chore(deps): bump actions/setup-java from 6.0.1 to 6.1.0 by dependabot in #6909
  • chore(deps): bump the codeql-action group with 3 updates by dependabot in #6905
  • chore(deps): update Sentry Android Gradle Plugin to v6.24.0 by github-actions in #6902
  • chore(deps): bump @appium/support from 7.0.6 to 7.2.8 by dependabot in #6897

Plus 1 more


🤖 This preview updates automatically when you update the PR.

Comment thread packages/core/src/js/profiling/uiProfilingIntegration.ts Outdated
Comment thread packages/core/src/js/profiling/uiProfilingIntegration.ts Outdated
Comment thread packages/core/src/js/profiling/uiProfilingIntegration.ts Outdated
Comment thread packages/core/src/js/profiling/uiProfilingIntegration.ts
Comment thread packages/core/src/js/integrations/default.ts Outdated
Comment thread packages/core/test/profiling/uiProfilingIntegration.test.ts
@alwx
alwx marked this pull request as ready for review October 20, 2026 13:54
@alwx
alwx requested review from a team, antonis and lucas-zimerman as code owners October 20, 2026 13:54

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4931e63. Configure here.

Comment thread packages/core/src/js/profiling/uiProfilingIntegration.ts
Comment thread packages/core/src/js/profiling/uiProfilingIntegration.ts
Comment thread packages/core/src/js/profiling/uiProfilingIntegration.ts

@antonis antonis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overall the changes make sense. Let's merge the latest from main and run the full ci checks.

The native fixes are in getsentry/sentry-java#6291 and getsentry/sentry-cocoa#9401.

Do we need those to test end to end?

Let's also add an example link to the Sentry dashboard in the description.

@lucas-zimerman

Copy link
Copy Markdown
Collaborator

Q: Should this integration be enabled when user sets defaultIntegrations: false?

alwx added 6 commits October 22, 2026 11:17
UI profiling never ran after app start. React Native turns off native
tracing, so the native trace lifecycle never sees a sampled
transaction. Also, no API let JS start or stop the native profiler, so
the manual lifecycle could not work either.

Add the startProfiler and stopProfiler bridge methods. On Android, they
call Sentry.startProfiler() and Sentry.stopProfiler(), and
startProfiler returns the profiler id. On iOS, they call SentrySDK.
sentry-cocoa has no public API for the profiler id, so iOS returns nil.

Always set the manual lifecycle on the native side, because both
native SDKs refuse manual calls in trace mode. A new default
integration, ProfilingIntegration, now chooses when the profiler runs.
In trace mode, it runs while a sampled root span is active. In manual
mode, it follows Sentry.profiler.startProfiler() and stopProfiler(),
which are now exported. The integration waits for native init, stops
an app start profile that is no longer needed, and adds the profiler
id to transactions so that they link to the profile.
Replace the PR number placeholder in the CHANGELOG entry with #6916.
Warden found two problems with the profiler id. The integration kept
one id for all transactions, so transactions that ran after the
profile stopped still got the id of the stopped profile. Also, the id
arrived asynchronously and was dropped when the root span ended before
the native start call resolved.

Each start call now creates a session that holds the promise of the
native start call. Each sampled root span that is active while a
session runs is mapped to that session, and processEvent waits for the
promise before it adds the id. A transaction now gets only the id of a
profile that ran during its root span.

In trace mode, the integration now waits one 60 second chunk after the
last root span ends before it calls stop. The native SDKs ignore a
start call that comes before a requested stop takes effect at the end
of the chunk, so back-to-back root spans lost their profile after the
chunk ended.
Sentry opens the profile of a span on the thread from the span's
thread.id. React Native spans had no thread.id, so the profile opened
on the main thread, and the JS work was in another, unnamed thread.

Add the getCurrentThreadId bridge method. It runs synchronously on the
JS thread and returns the thread id in the format of the native
profiler: the Linux thread id on Android and the Mach thread port on
iOS. The UI profiling integration reads it once and sets thread.id on
every span that does not have one.
…ampled roots

Warden found two more problems in the UI profiling integration.

In manual mode, a new session overwrote the session of root spans that
started under an earlier session. A transaction could then get the id
of a later profile. A root span now keeps the session that was running
when it started.

In trace mode with startOnAppStart, unsampled root spans were not
tracked, so their end never cleared the pending app start profile.
With tracesSampleRate: 0, the profiler then ran for the whole session.
The end of any root span now ends the app start profile.
…app start profile

With startOnAppStart, the native SDK starts the profile before JS runs.
Root spans that started and ended before native init had no session,
so their transactions got no profiler id. Also, the afterInit handler
then saw nothing to run and stopped the app start profile at once,
without the delay that trace mode uses.

The integration now creates the app start session in setup. Root spans
that start before native init map to it, and its id resolves when
native init completes. After that, the normal logic decides when the
profiler stops. The session resolves without an id if the client
closes before native init, so no transaction waits forever.
@alwx
alwx force-pushed the alwx/bugfix/ui-profiling-start-stop branch from be8626e to ff7b4bb Compare October 22, 2026 09:19
@antonis antonis added the ready-to-merge Triggers the full CI test suite label Oct 22, 2026
@alwx

alwx commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

@lucas-zimerman Good catch. I would keep the defaultIntegrations: false contract and not add it implicitly. Today that case is broken: the integration is not exported, so it cannot be added by hand, and with startOnAppStart the native app start profile never stops. I will export the integration (it will read profilingOptions from the client options) and log a warning when profilingOptions is set but the integration is missing, same pattern as hermesProfilingIntegration.

…s: false

With defaultIntegrations: false, the UI profiling integration was not
installed and could not be added, because it was not exported. The
native SDK still received profilingOptions, so trace mode never ran,
Sentry.profiler did nothing, and with startOnAppStart nothing ever
stopped the app start profile.

Export uiProfilingIntegration. It now reads profilingOptions from the
client options, so users add it without arguments. If profilingOptions
is set but the integration is not installed, the client logs a warning
and does not send profilingOptions to the native SDK.
@alwx

alwx commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

@lucas-zimerman Done in 565a986. uiProfilingIntegration is now exported and reads profilingOptions from the client options. With defaultIntegrations: false and no integration, the client logs a warning and does not send profilingOptions to native, so no profile starts that nothing would stop.

@sentry

sentry Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

📲 Install Builds

Android

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

⚙️ sentry-react-native Build Distribution Settings

@alwx
alwx requested a review from antonis October 22, 2026 09:56
@alwx

alwx commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

@antonis Thanks! Rebased on main, and full CI is running.

Do we need those to test end to end?

No. I tested this PR end to end with the stock sentry-java 8.61.0: transactions get the profiler_id, and the profile chunks reach Sentry and link to them. The native fixes only matter when a transaction starts after the stop call but before the current 60s chunk ends. Without them, the profile ends at the end of that chunk. Both native PRs are approved.

About the dashboard link: I will do one more run with this PR and the stock native SDK, and add a trace link to the description.

@alwx

alwx commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

The trace link added :)

@antonis antonis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@alwx
alwx merged commit b4352d6 into main Oct 22, 2026
73 of 78 checks passed
@alwx
alwx deleted the alwx/bugfix/ui-profiling-start-stop branch October 22, 2026 10:34
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.

3 participants