@@ -331,6 +331,114 @@ describe('ReactNativeTracing', () => {
331
331
expect ( transaction ) . toBeUndefined ( ) ;
332
332
} ) ;
333
333
334
+ describe ( 'bundle execution spans' , ( ) => {
335
+ afterEach ( ( ) => {
336
+ clearReactNativeBundleExecutionStartTimestamp ( ) ;
337
+ } ) ;
338
+
339
+ it ( 'does not add bundle executions span if __BUNDLE_START_TIME__ is undefined' , async ( ) => {
340
+ const integration = new ReactNativeTracing ( ) ;
341
+
342
+ mockAppStartResponse ( { cold : true } ) ;
343
+
344
+ setup ( integration ) ;
345
+
346
+ await jest . advanceTimersByTimeAsync ( 500 ) ;
347
+ await jest . runOnlyPendingTimersAsync ( ) ;
348
+
349
+ const transaction = client . event ;
350
+
351
+ const bundleStartSpan = transaction ! . spans ! . find (
352
+ ( { description } ) =>
353
+ description === 'JS Bundle Execution Start' || description === 'JS Bundle Execution Before React Root' ,
354
+ ) ;
355
+
356
+ expect ( bundleStartSpan ) . toBeUndefined ( ) ;
357
+ } ) ;
358
+
359
+ it ( 'adds bundle execution span' , async ( ) => {
360
+ const integration = new ReactNativeTracing ( ) ;
361
+
362
+ const [ timeOriginMilliseconds ] = mockAppStartResponse ( { cold : true } ) ;
363
+ mockReactNativeBundleExecutionStartTimestamp ( ) ;
364
+
365
+ setup ( integration ) ;
366
+ integration . onAppStartFinish ( timeOriginMilliseconds + 200 ) ;
367
+
368
+ await jest . advanceTimersByTimeAsync ( 500 ) ;
369
+ await jest . runOnlyPendingTimersAsync ( ) ;
370
+
371
+ const transaction = client . event ;
372
+
373
+ const appStartRootSpan = transaction ! . spans ! . find ( ( { description } ) => description === 'Cold App Start' ) ;
374
+ const bundleStartSpan = transaction ! . spans ! . find (
375
+ ( { description } ) => description === 'JS Bundle Execution Start' ,
376
+ ) ;
377
+ const appStartRootSpanJSON = spanToJSON ( appStartRootSpan ! ) ;
378
+ const bundleStartSpanJSON = spanToJSON ( bundleStartSpan ! ) ;
379
+
380
+ expect ( appStartRootSpan ) . toBeDefined ( ) ;
381
+ expect ( bundleStartSpan ) . toBeDefined ( ) ;
382
+ expect ( appStartRootSpanJSON ) . toEqual (
383
+ expect . objectContaining ( < SpanJSON > {
384
+ description : 'Cold App Start' ,
385
+ span_id : expect . any ( String ) ,
386
+ op : APP_START_COLD_OP ,
387
+ } ) ,
388
+ ) ;
389
+ expect ( bundleStartSpanJSON ) . toEqual (
390
+ expect . objectContaining ( < SpanJSON > {
391
+ description : 'JS Bundle Execution Start' ,
392
+ start_timestamp : expect . closeTo ( ( timeOriginMilliseconds - 50 ) / 1000 ) ,
393
+ timestamp : expect . closeTo ( ( timeOriginMilliseconds - 50 ) / 1000 ) ,
394
+ parent_span_id : spanToJSON ( appStartRootSpan ! ) . span_id , // parent is the root app start span
395
+ op : spanToJSON ( appStartRootSpan ! ) . op , // op is the same as the root app start span
396
+ } ) ,
397
+ ) ;
398
+ } ) ;
399
+
400
+ it ( 'adds bundle execution before react root' , async ( ) => {
401
+ const integration = new ReactNativeTracing ( ) ;
402
+
403
+ const [ timeOriginMilliseconds ] = mockAppStartResponse ( { cold : true } ) ;
404
+ mockReactNativeBundleExecutionStartTimestamp ( ) ;
405
+
406
+ setup ( integration ) ;
407
+ integration . setRootComponentFirstConstructorCallTimestampMs ( timeOriginMilliseconds - 10 ) ;
408
+
409
+ await jest . advanceTimersByTimeAsync ( 500 ) ;
410
+ await jest . runOnlyPendingTimersAsync ( ) ;
411
+
412
+ const transaction = client . event ;
413
+
414
+ const appStartRootSpan = transaction ! . spans ! . find ( ( { description } ) => description === 'Cold App Start' ) ;
415
+ const bundleStartSpan = transaction ! . spans ! . find (
416
+ ( { description } ) => description === 'JS Bundle Execution Before React Root' ,
417
+ ) ;
418
+ const appStartRootSpanJSON = spanToJSON ( appStartRootSpan ! ) ;
419
+ const bundleStartSpanJSON = spanToJSON ( bundleStartSpan ! ) ;
420
+
421
+ expect ( appStartRootSpan ) . toBeDefined ( ) ;
422
+ expect ( bundleStartSpan ) . toBeDefined ( ) ;
423
+ expect ( appStartRootSpanJSON ) . toEqual (
424
+ expect . objectContaining ( < SpanJSON > {
425
+ description : 'Cold App Start' ,
426
+ span_id : expect . any ( String ) ,
427
+ op : APP_START_COLD_OP ,
428
+ } ) ,
429
+ ) ;
430
+ expect ( bundleStartSpanJSON ) . toEqual (
431
+ expect . objectContaining ( < SpanJSON > {
432
+ description : 'JS Bundle Execution Before React Root' ,
433
+ start_timestamp : expect . closeTo ( ( timeOriginMilliseconds - 50 ) / 1000 ) ,
434
+ timestamp : ( timeOriginMilliseconds - 10 ) / 1000 ,
435
+ parent_span_id : spanToJSON ( appStartRootSpan ! ) . span_id , // parent is the root app start span
436
+ op : spanToJSON ( appStartRootSpan ! ) . op , // op is the same as the root app start span
437
+ } ) ,
438
+ ) ;
439
+ } ) ;
440
+ } ) ;
441
+
334
442
it ( 'adds native spans as a child of the main app start span' , async ( ) => {
335
443
const integration = new ReactNativeTracing ( ) ;
336
444
@@ -991,3 +1099,20 @@ function mockAppStartResponse({
991
1099
function setup ( integration : ReactNativeTracing ) {
992
1100
integration . setupOnce ( addGlobalEventProcessor , getCurrentHub ) ;
993
1101
}
1102
+
1103
+ /**
1104
+ * Mocks RN Bundle Start Module
1105
+ * `var __BUNDLE_START_TIME__=this.nativePerformanceNow?nativePerformanceNow():Date.now()`
1106
+ */
1107
+ function mockReactNativeBundleExecutionStartTimestamp ( ) {
1108
+ RN_GLOBAL_OBJ . nativePerformanceNow = ( ) => 100 ; // monotonic clock like `performance.now()`
1109
+ RN_GLOBAL_OBJ . __BUNDLE_START_TIME__ = 50 ; // 50ms after time origin
1110
+ }
1111
+
1112
+ /**
1113
+ * Removes mock added by mockReactNativeBundleExecutionStartTimestamp
1114
+ */
1115
+ function clearReactNativeBundleExecutionStartTimestamp ( ) {
1116
+ delete RN_GLOBAL_OBJ . nativePerformanceNow ;
1117
+ delete RN_GLOBAL_OBJ . __BUNDLE_START_TIME__ ;
1118
+ }
0 commit comments