Skip to content

feat(core): Attach TurboModule breakdown to active spans on spanEnd#6478

Open
alwx wants to merge 8 commits into
mainfrom
alwx/feature/6165
Open

feat(core): Attach TurboModule breakdown to active spans on spanEnd#6478
alwx wants to merge 8 commits into
mainfrom
alwx/feature/6165

Conversation

@alwx

@alwx alwx commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

📢 Type of change

  • New feature

📜 Description

On root spanEnd, attach per-(module, method) TurboModule call counts / duration / errors as turbo_module.<name>.<method>.* attributes on the span, plus summary keys. Async calls over slowCallThresholdMs (default 500ms) emit a native.turbo_module breadcrumb.

New options on turboModuleContextIntegration: enableSpanAttribution (default on), slowCallThresholdMs, maxTopModulesPerSpan.

Fed by a new per-record observer API on the JS aggregator so open spans can accumulate independently of the transaction / periodic flush drain path.

💡 Motivation and Context

Closes #6165

💚 How did you test it?

Unit tests for the span-attribution + breadcrumb paths and the observer contract. yarn test, yarn lint, yarn circularDepCheck, yarn api-report:check pass locally.

📝 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

Docs in #6168.

@github-actions

github-actions Bot commented Jul 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).


  • feat(core): Attach TurboModule breakdown to active spans on spanEnd by alwx in #6478
  • fix(android): make Sentry Gradle tasks Configuration Cache compatible by fabiendem in #6469
  • docs: Update 8.19.0 changelog by antonis in #6498
  • chore(deps): bump fast-uri from 3.1.2 to 3.1.4 by dependabot in #6496
  • chore(deps): pin glob 10.x to ^10.5.0 (dev-only advisory) by antonis in #6489
  • chore(deps): bump brace-expansion resolutions to ^2.1.2 (dev-only advisory) by antonis in #6488
  • chore(deps): add cross-spawn ^7.0.6 resolution (dev-only advisory) by antonis in #6487
  • chore(deps): bump js-yaml resolution to ^4.3.0 (dev-only advisory) by antonis in #6486
  • chore(deps): update CLI to v3.6.1 by github-actions in #6493
  • chore(deps): update JavaScript SDK to v10.67.0 by github-actions in #6494
  • chore(deps): update JavaScript SDK to v10.66.0 by github-actions in #6471
  • chore(deps): update Maestro to v2.7.0 by github-actions in #6481
  • chore(ios): Bump iOS binary size limit to 1650 KiB by antonis in #6485
  • chore(deps): bump tar from 7.5.16 to 7.5.20 by dependabot in #6483
  • chore(deps): bump shell-quote from 1.8.4 to 1.10.0 by dependabot in #6484
  • chore(deps): bump the codeql-action group with 3 updates by dependabot in #6475
  • chore(deps): bump brace-expansion from 1.1.13 to 1.1.16 by dependabot in #6482
  • chore(deps): bump axios from 1.16.0 to 1.18.1 by dependabot in #6479
  • chore(deps): bump actions/setup-java from 5.5.0 to 5.6.0 by dependabot in #6476

🤖 This preview updates automatically when you update the PR.

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against 5c098eb

@alwx

alwx commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

@cursor review

@alwx
alwx marked this pull request as ready for review July 20, 2026 13:43
@alwx
alwx requested review from a team, antonis and lucas-zimerman as code owners July 20, 2026 13:43
@alwx
alwx removed the request for review from a team July 20, 2026 13:44
Comment thread packages/core/src/js/integrations/turboModuleContext.ts Outdated
Comment thread packages/core/src/js/turbomodule/turboModuleAggregator.ts
Comment thread packages/core/src/js/turbomodule/turboModuleAggregator.ts
Comment thread packages/core/src/js/integrations/turboModuleContext.ts
Comment thread packages/core/src/js/turbomodule/turboModuleAggregator.ts
alwx added a commit that referenced this pull request Jul 20, 2026
- Nested `name→method` counter map so identifiers with any character
  can't collide (also removes a stray NUL byte in the compound key).
- Fire per-record observers even when `enableAggregateStats: false`, and
  apply `ignoreTurboModules` whenever any consumer of the record path is
  active. Fixes span attribution + slow-call breadcrumb going silent when
  aggregate stats are opted out.
- Snapshot the set of open windows at call start via a new
  `notifyTurboModuleCallStart` hook, so async calls that outlive their
  originating span still credit that span. Late-settling records
  re-emit `setAttributes` on the (now closed) span.
- CHANGELOG entry references PR #6478 instead of the issue.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread packages/core/src/js/integrations/turboModuleContext.ts
Comment thread packages/core/src/js/integrations/turboModuleContext.ts Outdated
Comment thread packages/core/src/js/integrations/turboModuleContext.ts
Comment thread packages/core/src/js/integrations/turboModuleContext.ts
Comment thread packages/core/src/js/integrations/turboModuleContext.ts Outdated
alwx and others added 4 commits July 22, 2026 10:05
Adds per-span attribution to `turboModuleContextIntegration`: on root
span end, writes `turbo_module.<name>.<method>.{call_count,duration_ms,
error_count}` attributes plus summary keys. Async calls above
`slowCallThresholdMs` (default 500ms) also emit a `native.turbo_module`
breadcrumb. New knobs: `enableSpanAttribution`, `slowCallThresholdMs`,
`maxTopModulesPerSpan`.

Closes #6165.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Nested `name→method` counter map so identifiers with any character
  can't collide (also removes a stray NUL byte in the compound key).
- Fire per-record observers even when `enableAggregateStats: false`, and
  apply `ignoreTurboModules` whenever any consumer of the record path is
  active. Fixes span attribution + slow-call breadcrumb going silent when
  aggregate stats are opted out.
- Snapshot the set of open windows at call start via a new
  `notifyTurboModuleCallStart` hook, so async calls that outlive their
  originating span still credit that span. Late-settling records
  re-emit `setAttributes` on the (now closed) span.
- CHANGELOG entry references PR #6478 instead of the issue.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`startObserver` used to skip writing to `pendingCallWindows` when the
snapshot was empty. When the call settled, `recordObserver` couldn't
find the recordId and fell back to the currently-open windows —
crediting spans that opened *after* the call started. Common case: an
async TurboModule call kicked off during app init, before any nav /
user span has opened.

Always record a snapshot (even an empty one), and reserve the
currently-open fallback for records without a recordId (direct
`recordTurboModuleCall` callers in tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Clear per-method attribute keys from a previous emit that no longer fit in
  the top-N. `setAttributes` merges, so without this a late-settling async
  call that re-ranks the top-N would leave dropped rows on the span.
- Cap `pendingCallWindows` at 1024 so abandoned (never-settling) async
  promises can't pin `WindowState` indefinitely. Evicted entries are
  silently dropped on late settle rather than mis-attributed.
- Correct the misleading comment about the WeakMap — it enables O(1)
  `spanEnd` lookup; it does not protect against leaks (the parallel
  `openWindowList` holds strong refs).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@alwx
alwx force-pushed the alwx/feature/6165 branch from 13a5f2a to 94db31a Compare July 22, 2026 09:27
Comment thread packages/core/src/js/integrations/turboModuleContext.ts
Comment thread packages/core/src/js/integrations/turboModuleContext.ts
Comment thread packages/core/src/js/integrations/turboModuleContext.ts
Comment thread packages/core/src/js/integrations/turboModuleContext.ts Outdated
@antonis antonis added the ready-to-merge Triggers the full CI test suite label Jul 22, 2026
- Register the record observer whenever either `enableSpanAttribution` or
  `slowCallThresholdMs > 0` is active, and gate span-attribution and
  breadcrumb logic separately inside. Previously the breadcrumb code lived
  inside the `enableSpanAttribution` block, so disabling span attribution
  silently turned off breadcrumbs — contradicting the documented independent
  `slowCallThresholdMs` knob. `setIgnoredTurboModules` also now applies in
  breadcrumbs-only mode.
- Sync calls skip `pendingCallWindows`. Sync start + settle happen in the
  same turn, so they can credit `openWindowList` directly. Skipping them
  also prevents sync bursts from evicting genuine async entries under the
  MAX_PENDING_CALL_WINDOWS cap.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread packages/core/src/js/turbomodule/wrapTurboModule.ts
Comment thread packages/core/src/js/turbomodule/wrapTurboModule.ts
@sentry

sentry Bot commented Jul 22, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

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

⚙️ sentry-react-native Build Distribution Settings

Comment thread packages/core/src/js/turbomodule/wrapTurboModule.ts
Comment thread CHANGELOG.md

- Attach a per-`(module, method)` TurboModule breakdown to active spans on `spanEnd`, plus `native.turbo_module` breadcrumbs for slow async calls ([#6478](https://github.com/getsentry/sentry-react-native/pull/6478))

When a root span ends (idle nav spans from `reactNavigationIntegration` / `expoRouterIntegration`, or a user's own `Sentry.startSpan(...)`), the integration writes `turbo_module.<name>.<method>.{call_count,duration_ms,error_count}` attributes plus summary keys (`turbo_module.total_call_count`, `turbo_module.total_duration_ms`, `turbo_module.top_module`). Async calls above `slowCallThresholdMs` (default 500ms) additionally record a `native.turbo_module` breadcrumb. Both surfaces enabled by default; new knobs `enableSpanAttribution`, `slowCallThresholdMs`, `maxTopModulesPerSpan` on `turboModuleContextIntegration`.

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.

Nit: Missing line before the Fixes section.

@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 PR LGTM once the CI is 🟢 (apart from the known issue related failures).

I would advocate towards making the changes opt-in / experimental first to gather some feedback from users (track adoption similar to this) and verify stability before releasing for all. Wdyt?

`wrapTurboModule` always calls `notifyTurboModuleCallStart` with kind
`'sync'` and only relabels to `'async'` once the return value is known
to be thenable. Gating the start observer on `kind === 'async'` therefore
silently dropped every async attribution in production. Revert to
snapshotting on every start — sync entries settle in the same
synchronous turn and are removed by their paired record, so they don't
accumulate under normal traffic.

Also add the missing blank line between the Features and Fixes sections
in the CHANGELOG.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

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

Reviewed by Cursor Bugbot for commit 5c098eb. Configure here.

// parent transaction is serialised.
if (window.closed) {
attachWindowToSpan(window.span, window, maxTopModulesPerSpan);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Late async attributes miss transactions

Medium Severity

Late-settling async TurboModule calls re-apply span attributes with setAttributes after spanEnd, but the transaction event is usually already built and captured in that same turn. Those updates therefore never appear on the sent transaction. Similar late data in this codebase (e.g. native frames) is merged in processEvent instead.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 5c098eb. Configure here.

@github-actions

Copy link
Copy Markdown
Contributor

Android (legacy) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 450.26 ms 505.00 ms 54.74 ms
Size 49.74 MiB 55.36 MiB 5.62 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
f3215d3+dirty 411.11 ms 454.38 ms 43.27 ms
d2eadf8+dirty 414.64 ms 454.56 ms 39.92 ms
b0d3373+dirty 557.66 ms 579.42 ms 21.76 ms
27d9693+dirty 419.08 ms 469.12 ms 50.04 ms
4e0b819+dirty 420.56 ms 470.08 ms 49.52 ms
9474ead+dirty 411.45 ms 446.80 ms 35.35 ms
0bd8916+dirty 412.77 ms 451.31 ms 38.54 ms
f9c1ed4+dirty 431.00 ms 466.22 ms 35.22 ms
3d536d1+dirty 524.34 ms 547.32 ms 22.98 ms
04207c4+dirty 459.19 ms 518.54 ms 59.35 ms

App size

Revision Plain With Sentry Diff
f3215d3+dirty 48.30 MiB 53.49 MiB 5.19 MiB
d2eadf8+dirty 48.30 MiB 53.48 MiB 5.18 MiB
b0d3373+dirty 48.30 MiB 53.58 MiB 5.28 MiB
27d9693+dirty 49.74 MiB 55.09 MiB 5.34 MiB
4e0b819+dirty 49.74 MiB 54.81 MiB 5.07 MiB
9474ead+dirty 48.30 MiB 53.61 MiB 5.30 MiB
0bd8916+dirty 48.30 MiB 53.57 MiB 5.26 MiB
f9c1ed4+dirty 49.74 MiB 54.86 MiB 5.12 MiB
3d536d1+dirty 49.74 MiB 55.26 MiB 5.52 MiB
04207c4+dirty 43.75 MiB 48.12 MiB 4.37 MiB

@github-actions

Copy link
Copy Markdown
Contributor

iOS (legacy) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 3831.83 ms 1208.48 ms -2623.35 ms
Size 4.98 MiB 6.56 MiB 1.59 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
b9bebee+dirty 3850.15 ms 1227.51 ms -2622.64 ms
3d377b5+dirty 1218.48 ms 1219.51 ms 1.03 ms
1a2e7e0+dirty 3842.49 ms 1220.04 ms -2622.45 ms
e5bb5f6+dirty 3826.14 ms 1212.24 ms -2613.90 ms
6acdf1d+dirty 3844.33 ms 1212.96 ms -2631.38 ms
41d6254+dirty 3845.71 ms 1224.51 ms -2621.20 ms
9210ae6+dirty 3815.93 ms 1214.14 ms -2601.79 ms
c004dae+dirty 3850.32 ms 1227.79 ms -2622.53 ms
4e0b819+dirty 3839.05 ms 1210.75 ms -2628.30 ms
0b5120f+dirty 3838.39 ms 1232.91 ms -2605.48 ms

App size

Revision Plain With Sentry Diff
b9bebee+dirty 5.15 MiB 6.68 MiB 1.53 MiB
3d377b5+dirty 3.38 MiB 4.76 MiB 1.38 MiB
1a2e7e0+dirty 4.98 MiB 6.46 MiB 1.49 MiB
e5bb5f6+dirty 4.98 MiB 6.51 MiB 1.53 MiB
6acdf1d+dirty 4.98 MiB 6.51 MiB 1.53 MiB
41d6254+dirty 5.15 MiB 6.70 MiB 1.54 MiB
9210ae6+dirty 5.15 MiB 6.68 MiB 1.53 MiB
c004dae+dirty 5.15 MiB 6.67 MiB 1.51 MiB
4e0b819+dirty 4.98 MiB 6.46 MiB 1.49 MiB
0b5120f+dirty 5.15 MiB 6.68 MiB 1.53 MiB

@github-actions

Copy link
Copy Markdown
Contributor

iOS (new) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 3849.77 ms 1231.12 ms -2618.65 ms
Size 4.98 MiB 6.56 MiB 1.59 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
b0d3373+dirty 3842.49 ms 1218.49 ms -2624.00 ms
ef27341+dirty 3835.20 ms 1212.23 ms -2622.97 ms
e5bb5f6+dirty 3825.74 ms 1217.30 ms -2608.43 ms
44c8b3f+dirty 3849.24 ms 1209.94 ms -2639.31 ms
68672fc+dirty 3832.22 ms 1228.29 ms -2603.93 ms
5748023+dirty 3844.74 ms 1225.49 ms -2619.26 ms
6acdf1d+dirty 3835.35 ms 1218.30 ms -2617.06 ms
2c735cc+dirty 1223.33 ms 1224.38 ms 1.04 ms
100ce80+dirty 3843.57 ms 1226.46 ms -2617.12 ms
9210ae6+dirty 3834.11 ms 1216.64 ms -2617.47 ms

App size

Revision Plain With Sentry Diff
b0d3373+dirty 5.15 MiB 6.68 MiB 1.53 MiB
ef27341+dirty 5.15 MiB 6.68 MiB 1.53 MiB
e5bb5f6+dirty 4.98 MiB 6.51 MiB 1.53 MiB
44c8b3f+dirty 5.15 MiB 6.66 MiB 1.51 MiB
68672fc+dirty 5.15 MiB 6.71 MiB 1.55 MiB
5748023+dirty 5.15 MiB 6.68 MiB 1.53 MiB
6acdf1d+dirty 4.98 MiB 6.51 MiB 1.53 MiB
2c735cc+dirty 3.38 MiB 4.74 MiB 1.35 MiB
100ce80+dirty 5.15 MiB 6.67 MiB 1.51 MiB
9210ae6+dirty 5.15 MiB 6.68 MiB 1.53 MiB

@github-actions

Copy link
Copy Markdown
Contributor

Android (new) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 458.11 ms 495.57 ms 37.47 ms
Size 49.74 MiB 55.36 MiB 5.62 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
a50b33d+dirty 353.21 ms 398.48 ms 45.27 ms
d038a14+dirty 405.08 ms 444.36 ms 39.28 ms
68672fc+dirty 407.55 ms 442.96 ms 35.41 ms
ad66da3+dirty 411.49 ms 449.38 ms 37.89 ms
f170ec3+dirty 505.96 ms 551.88 ms 45.92 ms
27d9693+dirty 438.63 ms 514.08 ms 75.46 ms
41d6254+dirty 406.20 ms 445.52 ms 39.32 ms
68ae91b+dirty 515.04 ms 578.08 ms 63.04 ms
5ee78d6+dirty 411.18 ms 437.83 ms 26.65 ms
ae37560+dirty 428.96 ms 456.86 ms 27.90 ms

App size

Revision Plain With Sentry Diff
a50b33d+dirty 43.94 MiB 48.94 MiB 5.00 MiB
d038a14+dirty 48.30 MiB 53.49 MiB 5.19 MiB
68672fc+dirty 48.30 MiB 53.61 MiB 5.31 MiB
ad66da3+dirty 48.30 MiB 53.49 MiB 5.19 MiB
f170ec3+dirty 48.30 MiB 53.57 MiB 5.26 MiB
27d9693+dirty 49.74 MiB 55.09 MiB 5.34 MiB
41d6254+dirty 48.30 MiB 53.60 MiB 5.30 MiB
68ae91b+dirty 49.74 MiB 54.79 MiB 5.05 MiB
5ee78d6+dirty 48.30 MiB 53.58 MiB 5.28 MiB
ae37560+dirty 48.30 MiB 53.60 MiB 5.29 MiB

Comment on lines +537 to +540
for (const [attributes] of span.setAttributes.mock.calls) {
expect(attributes['turbo_module.First.work.call_count']).toBeUndefined();
}
});

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.

Cap eviction test assertion never executes

The evicted call and all fillers are unsettled, so span.setAttributes is never called and this loop assertion never runs. Settle a filler call to validate the eviction behavior.

Evidence
  • notifyTurboModuleCallStart('First') followed by 1024 filler starts causes the oldest entry (First) to be evicted from pendingCallWindows.
  • recordTurboModuleCall for the evicted firstRecordId finds no windows, so recordIntoWindow is never called.
  • None of the fillers are settled, leaving window.counters empty; attachWindowToSpan returns early on spanEnd.
  • Consequently span.setAttributes is never invoked, mock.calls is empty, and the for...of block at line 537 never executes.

Identified by Warden code-review · 2PS-4YV

Comment on lines 42 to +44
*/
const MAX_AGGREGATE_ATTRIBUTE_ROWS = 64;
export const DEFAULT_MAX_TOP_MODULES_PER_SPAN = 16;

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.

Span attribute key collision when TurboModule names or methods contain dots

Per-span attribute keys like turbo_module.${name}.${method} collide when module or method names contain dots, causing distinct calls to overwrite each other (e.g. ("a.b", "c") and ("a", "b.c") both map to turbo_module.a.b.c.call_count).

Evidence
  • WindowState.counters uses nested Maps and a comment at turboModuleContext.ts:380 claims identifiers with dots "can never collide with the pair separator".
  • In attachWindowToSpan (turboModuleContext.ts:452), the prefix is built as `turbo_module.${row.name}.${row.method}`, using . as the only separator with no escaping.
  • Two distinct (module, method) pairs such as ("a.b", "c") and ("a", "b.c") produce the identical prefix turbo_module.a.b.c and overwrite each other in the attributes object passed to span.setAttributes.
  • Test coverage in turboModuleContext.test.ts does not exercise names containing dots, so the collision is unguarded.

Identified by Warden find-bugs · STE-MTS

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.

JS-side integration: attach TurboModule stats to active spans

2 participants