@@ -4,9 +4,7 @@ import { debug } from '@sentry/core';
44
55import { createSpanJSON } from '../tracing/utils' ;
66import {
7- beginSuppressFirstTurboModuleRecordCallback ,
87 drainTurboModuleAggregate ,
9- endSuppressFirstTurboModuleRecordCallback ,
108 HISTOGRAM_BUCKET_LABELS ,
119 hasTurboModuleAggregateData ,
1210 setAggregateRecordingEnabled ,
@@ -74,9 +72,14 @@ export interface TurboModuleContextOptions {
7472 aggregateFlushIntervalMs ?: number ;
7573
7674 /**
77- * TurboModules whose calls should NOT be counted in the aggregate. Users
78- * may e.g. want to exclude `RNSentry` itself to keep the signal clean of
79- * the SDK's own internal calls.
75+ * TurboModules whose calls should NOT be counted in the aggregate.
76+ *
77+ * Default: `['RNSentry']`. The SDK's own transport call
78+ * (`RNSentry.captureEnvelope`) fires from every `captureEvent`, so leaving
79+ * `RNSentry` in the aggregate would (a) pollute app-level TurboModule
80+ * signals with SDK internal noise and (b) allow the periodic flush's own
81+ * `captureEvent` to record back into the aggregator and perpetually re-arm
82+ * the flush timer in idle sessions. Pass `[]` to opt back in.
8083 *
8184 * Note: this does NOT disable wrapping — crashes during those calls still
8285 * get attributed via `contexts.turbo_module`. It only opts the module out
@@ -146,7 +149,7 @@ export const turboModuleContextIntegration = (options: TurboModuleContextOptions
146149
147150 setAggregateRecordingEnabled ( enableAggregate ) ;
148151 if ( enableAggregate ) {
149- setIgnoredTurboModules ( options . ignoreTurboModules ) ;
152+ setIgnoredTurboModules ( options . ignoreTurboModules ?? [ 'RNSentry' ] ) ;
150153 }
151154 } ,
152155 setup ( client : Client ) : void {
@@ -202,6 +205,13 @@ export const turboModuleContextIntegration = (options: TurboModuleContextOptions
202205/**
203206 * Mutates a transaction event in place to add the aggregate breakdown as a
204207 * synthetic child span plus a few headline measurements on the root span.
208+ *
209+ * Draining here runs before `beforeSendTransaction`, so if a user hook drops
210+ * this transaction, the drained batch is lost. Trade-off is intentional:
211+ * peeking without draining would require send-confirmation bookkeeping across
212+ * events and multiple transactions in flight would double-count. Data loss
213+ * from a dropped transaction is bounded (one interval) and self-heals — the
214+ * next transaction or periodic flush picks up fresh activity.
205215 */
206216function attachAggregateToTransactionEvent ( event : TransactionEvent ) : void {
207217 const trace = event . contexts ?. trace ;
@@ -268,16 +278,10 @@ function attachAggregateToTransactionEvent(event: TransactionEvent): void {
268278 * sessions without a transaction still produce a signal. No-op when there's
269279 * nothing to flush.
270280 *
271- * The `captureEvent` call travels through `RNSentry.captureEnvelope`, which
272- * is itself a wrapped TurboModule call. Without the suppression scope below,
273- * the resulting empty→non-empty transition on the aggregator would re-arm
274- * the flush timer indefinitely in an otherwise-idle session. The release is
275- * deferred to the next macrotask so the async record fired from the
276- * transport's `.then()` (after the native side resolves) also lands inside
277- * the suppression window; the leftover entries are then drained so the next
278- * real user call registers as an empty→non-empty transition and re-arms
279- * the periodic timer (otherwise a suppressed record would poison the map
280- * and the timer would never re-arm again).
281+ * `client.captureEvent` reaches wrapped `RNSentry.captureEnvelope` via the
282+ * native transport — so if `RNSentry` were aggregated, the flush's own send
283+ * would re-arm the lazy timer indefinitely. `ignoreTurboModules` defaults
284+ * to `['RNSentry']` for exactly this reason.
281285 */
282286function flushPeriodicAggregate ( client : Client ) : void {
283287 if ( ! hasTurboModuleAggregateData ( ) ) {
@@ -287,35 +291,20 @@ function flushPeriodicAggregate(client: Client): void {
287291 const totals = summarise ( snapshot ) ;
288292 const topByTotalMs = [ ...snapshot ] . sort ( ( a , b ) => b . totalDurationMs - a . totalDurationMs ) ;
289293
290- beginSuppressFirstTurboModuleRecordCallback ( ) ;
291- try {
292- client . captureEvent ?.( {
293- message : 'TurboModule aggregate (periodic)' ,
294- level : 'info' ,
295- tags : {
296- 'event.kind' : 'turbo_modules.aggregate' ,
297- } ,
298- extra : {
299- total_call_count : totals . callCount ,
300- total_error_count : totals . errorCount ,
301- total_duration_ms : roundMs ( totals . totalDurationMs ) ,
302- unique_methods : snapshot . length ,
303- modules : topByTotalMs . slice ( 0 , MAX_AGGREGATE_ATTRIBUTE_ROWS ) . map ( serialiseRowAsObject ) ,
304- } ,
305- } ) ;
306- } finally {
307- setTimeout ( ( ) => {
308- endSuppressFirstTurboModuleRecordCallback ( ) ;
309- // Discard whatever landed during the suppression window — those
310- // records are (in the common case) our own transport's self-noise.
311- // Leaving them in the aggregator would leave `size > 0`, so the
312- // next real user call wouldn't be an empty→non-empty transition
313- // and `onFirstRecordAfterEmpty` would never re-arm the timer.
314- if ( hasTurboModuleAggregateData ( ) ) {
315- drainTurboModuleAggregate ( ) ;
316- }
317- } , 0 ) ;
318- }
294+ client . captureEvent ?.( {
295+ message : 'TurboModule aggregate (periodic)' ,
296+ level : 'info' ,
297+ tags : {
298+ 'event.kind' : 'turbo_modules.aggregate' ,
299+ } ,
300+ extra : {
301+ total_call_count : totals . callCount ,
302+ total_error_count : totals . errorCount ,
303+ total_duration_ms : roundMs ( totals . totalDurationMs ) ,
304+ unique_methods : snapshot . length ,
305+ modules : topByTotalMs . slice ( 0 , MAX_AGGREGATE_ATTRIBUTE_ROWS ) . map ( serialiseRowAsObject ) ,
306+ } ,
307+ } ) ;
319308}
320309
321310function summarise ( snapshot : ReadonlyArray < TurboModuleAggregate > ) : {
0 commit comments