Skip to content

feat(core): Aggregate TurboModule call counts and latency per (module, method, kind)#6377

Draft
alwx wants to merge 2 commits into
mainfrom
alwx/features/aggregated-stats
Draft

feat(core): Aggregate TurboModule call counts and latency per (module, method, kind)#6377
alwx wants to merge 2 commits into
mainfrom
alwx/features/aggregated-stats

Conversation

@alwx

@alwx alwx commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

📢 Type of change

  • New feature

📜 Description

Adds per-(module, method, kind) aggregation for TurboModule invocations on top of the existing wrapTurboModule instrumentation. Aggregates flush at two points:

  1. On transaction finish — a synthetic turbo_modules.aggregate child span on the event carries the per-method breakdown in span attributes; headline measurements (turbo_modules.call_count, .error_count, .total_ms, .top_module_ms) land on the root span for the standard Measurements panel.
  2. Lazy timer (default 30s) — emits an info-level event tagged event.kind=turbo_modules.aggregate so idle sessions without transactions still produce a signal. Armed only on the first record after a drain, so silent sessions don't churn timers.

New turboModuleContextIntegration options: enableAggregateStats (default true), aggregateFlushIntervalMs (default 30000, 0 disables), ignoreTurboModules (excludes modules from counters; still wrapped for crash attribution).

O(1) per call: counters + 6-bucket histogram (<1ms, <5ms, <20ms, <100ms, <500ms, >=500ms). Failures inside the aggregator never break the wrapped TurboModule call.

💡 Motivation and Context

Closes #6164. Per-call spans for every TurboModule invocation would explode span counts on hot async paths; aggregated counters are the right tradeoff.

💚 How did you test it?

📝 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

@github-actions

github-actions Bot commented Jun 30, 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): Aggregate TurboModule call counts and latency per (module, method, kind) by alwx in #6377
  • feat(core): Expose top-level Sentry.setAttribute / setAttributes by antonis in #6354
  • docs: Add AI Use section to CONTRIBUTING.md by christophaigner in #6374
  • feat(replay): Default networkCaptureBodies to true by alwx in #6372
  • chore(deps): bump getsentry/craft from 2.26.10 to 2.26.13 by dependabot in #6368
  • chore(deps): bump getsentry/github-workflows/danger from 17cc15eb58ea3687cd8f2714a4192dcee4aa09ef to 4013fc6e1aeb1be1f9d3b4d232624f0ec1afa613 by dependabot in #6366
  • chore(deps): bump getsentry/github-workflows/validate-pr from 71588ddf95134f804e82c5970a8098588e2eaecd to 4013fc6e1aeb1be1f9d3b4d232624f0ec1afa613 by dependabot in #6364
  • chore(deps): bump getsentry/craft/.github/workflows/changelog-preview.yml from 2.26.10 to 2.26.13 by dependabot in #6367
  • feat(core): Wire TurboModulePerfLogger on iOS and Android by alwx in #6307
  • chore(deps): bump actions/cache from 4 to 6 by dependabot in #6365
  • chore(deps): update CLI to v3.6.0 by github-actions in #6362
  • chore(deps): bump faraday from 1.10.5 to 1.10.6 in /samples/react-native by dependabot in #6363
  • chore(deps): update JavaScript SDK to v10.62.0 by github-actions in #6361
  • Expo Router ErrorBoundary auto wrapped by alwx in #6347
  • chore(ci): Move sample app iOS build jobs to GitHub Actions runners by itaybre in #6356
  • docs: Add missing 8.14.1 to changelog and SDK versions table by antonis in #6360
  • chore(deps): update Android SDK to v8.46.0 by github-actions in #6357
  • chore(ci): Move testflight and size-analysis iOS jobs to GitHub Actions macos-26 by itaybre in #6355
  • feat(core): Use native btoa for envelope base64 encoding by alwx in #6351

🤖 This preview updates automatically when you update the PR.

alwx and others added 2 commits June 30, 2026 11:20
…, method, kind)

Adds a small fixed-bucket histogram + counters per `(module, method, kind)`
fed by the existing `wrapTurboModule` instrumentation. Aggregates flush on
transaction finish (synthetic `turbo_modules.aggregate` child span + headline
measurements on the root span) and on a lazy timer (info-level event for
long-running sessions without transactions).

Closes #6164.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drop edge-case tests in favor of one happy-path check per surface.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@alwx alwx force-pushed the alwx/features/aggregated-stats branch from 933fdb9 to 5702b46 Compare June 30, 2026 09:21
@github-actions

Copy link
Copy Markdown
Contributor
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 5702b46

// Don't let a misbehaving observer corrupt the aggregate.
try {
onFirstRecordAfterEmpty();
} catch {

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.

Periodic flush re-arms itself via SDK's own captureEnvelope call, defeating the idle-session guard

After each periodic flush, flushPeriodicAggregate calls client.captureEvent, which (on the native transport) ultimately sends the envelope through RNSentry.captureEnvelope. That method is wrapped by wrapTurboModule (it is not in RNSENTRY_SKIP, and RNSentry is not ignored by default), so it records a TurboModule call. Because drainTurboModuleAggregate() already cleared the map, the recorded call sees wasEmpty === true and fires onFirstRecordAfterEmpty, scheduling another 30s timer — even in a fully idle session with no user activity. This defeats the design intent of arming the timer only on the first record after a drain so silent sessions don't churn timers; instead the SDK perpetually re-flushes its own envelope-send overhead every interval. Consider adding RNSentry to the default ignoreTurboModules set, or suppressing the re-arm when the only recorded activity originates from the flush's own capture path.

Evidence
  • flushPeriodicAggregate (turboModuleContext.ts) calls drainTurboModuleAggregate() (clearing the map) then client.captureEvent.
  • wrapTurboModule mutates the live module in place (target[key] = wrapper), and wrapper.ts:234 sends envelopes through that same RNSentry.captureEnvelope instance, so the call is tracked.
  • captureEnvelope is absent from RNSENTRY_SKIP (turboModuleContext.ts) and RNSentry is not in the default ignoreTurboModules, so recordTurboModuleCall runs for it.
  • At turboModuleAggregator.ts:120 wasEmpty = aggregates.size === 0 is true right after the drain, so the guard at line 153 fires onFirstRecordAfterEmpty, re-arming the timer (turboModuleContext.ts setOnFirstTurboModuleRecord).
  • The existing test stubs captureEvent as jest.fn(), so the native send/record cycle never occurs and the self-re-arming behavior is not exercised.

Identified by Warden code-review · 2Q2-JQP

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.

Aggregated per-module / per-method stats

1 participant