@@ -331,6 +331,114 @@ describe('ReactNativeTracing', () => {
331331 expect ( transaction ) . toBeUndefined ( ) ;
332332 } ) ;
333333
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+
334442 it ( 'adds native spans as a child of the main app start span' , async ( ) => {
335443 const integration = new ReactNativeTracing ( ) ;
336444
@@ -991,3 +1099,20 @@ function mockAppStartResponse({
9911099function setup ( integration : ReactNativeTracing ) {
9921100 integration . setupOnce ( addGlobalEventProcessor , getCurrentHub ) ;
9931101}
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