@@ -28,6 +28,29 @@ jest.mock('../../src/js/tracing/utils', () => {
28
28
} ;
29
29
} ) ;
30
30
31
+ type MockAppState = {
32
+ setState : ( state : AppStateStatus ) => void ;
33
+ listener : ( newState : AppStateStatus ) => void ;
34
+ removeSubscription : jest . Func ;
35
+ } ;
36
+ const mockedAppState : AppState & MockAppState = {
37
+ removeSubscription : jest . fn ( ) ,
38
+ listener : jest . fn ( ) ,
39
+ isAvailable : true ,
40
+ currentState : 'active' ,
41
+ addEventListener : ( _ , listener ) => {
42
+ mockedAppState . listener = listener ;
43
+ return {
44
+ remove : mockedAppState . removeSubscription ,
45
+ } ;
46
+ } ,
47
+ setState : ( state : AppStateStatus ) => {
48
+ mockedAppState . currentState = state ;
49
+ mockedAppState . listener ( state ) ;
50
+ } ,
51
+ } ;
52
+ jest . mock ( 'react-native/Libraries/AppState/AppState' , ( ) => mockedAppState ) ;
53
+
31
54
const getMockScope = ( ) => {
32
55
let scopeTransaction : Transaction | undefined ;
33
56
let scopeUser : User | undefined ;
@@ -62,6 +85,7 @@ const getMockHub = () => {
62
85
63
86
import type { BrowserClientOptions } from '@sentry/browser/types/client' ;
64
87
import type { Scope } from '@sentry/types' ;
88
+ import type { AppState , AppStateStatus } from 'react-native' ;
65
89
66
90
import { APP_START_COLD , APP_START_WARM } from '../../src/js/measurements' ;
67
91
import {
@@ -100,16 +124,7 @@ describe('ReactNativeTracing', () => {
100
124
enableNativeFramesTracking : false ,
101
125
} ) ;
102
126
103
- const timeOriginMilliseconds = Date . now ( ) ;
104
- const appStartTimeMilliseconds = timeOriginMilliseconds - 100 ;
105
- const mockAppStartResponse : NativeAppStartResponse = {
106
- isColdStart : true ,
107
- appStartTime : appStartTimeMilliseconds ,
108
- didFetchAppStart : false ,
109
- } ;
110
-
111
- mockFunction ( getTimeOriginMilliseconds ) . mockReturnValue ( timeOriginMilliseconds ) ;
112
- mockFunction ( NATIVE . fetchNativeAppStart ) . mockResolvedValue ( mockAppStartResponse ) ;
127
+ const [ timeOriginMilliseconds , appStartTimeMilliseconds ] = mockAppStartResponse ( { cold : true } ) ;
113
128
114
129
const mockHub = getMockHub ( ) ;
115
130
integration . setupOnce ( addGlobalEventProcessor , ( ) => mockHub ) ;
@@ -139,16 +154,7 @@ describe('ReactNativeTracing', () => {
139
154
it ( 'Starts route transaction (warm)' , async ( ) => {
140
155
const integration = new ReactNativeTracing ( ) ;
141
156
142
- const timeOriginMilliseconds = Date . now ( ) ;
143
- const appStartTimeMilliseconds = timeOriginMilliseconds - 100 ;
144
- const mockAppStartResponse : NativeAppStartResponse = {
145
- isColdStart : false ,
146
- appStartTime : appStartTimeMilliseconds ,
147
- didFetchAppStart : false ,
148
- } ;
149
-
150
- mockFunction ( getTimeOriginMilliseconds ) . mockReturnValue ( timeOriginMilliseconds ) ;
151
- mockFunction ( NATIVE . fetchNativeAppStart ) . mockResolvedValue ( mockAppStartResponse ) ;
157
+ const [ timeOriginMilliseconds , appStartTimeMilliseconds ] = mockAppStartResponse ( { cold : false } ) ;
152
158
153
159
const mockHub = getMockHub ( ) ;
154
160
integration . setupOnce ( addGlobalEventProcessor , ( ) => mockHub ) ;
@@ -173,6 +179,24 @@ describe('ReactNativeTracing', () => {
173
179
}
174
180
} ) ;
175
181
182
+ it ( 'Cancels route transaction when app goes to background' , async ( ) => {
183
+ const integration = new ReactNativeTracing ( ) ;
184
+
185
+ mockAppStartResponse ( { cold : false } ) ;
186
+
187
+ const mockHub = getMockHub ( ) ;
188
+ integration . setupOnce ( addGlobalEventProcessor , ( ) => mockHub ) ;
189
+
190
+ await jest . advanceTimersByTimeAsync ( 500 ) ;
191
+ const transaction = mockHub . getScope ( ) ?. getTransaction ( ) ;
192
+
193
+ mockedAppState . setState ( 'background' ) ;
194
+ jest . runAllTimers ( ) ;
195
+
196
+ expect ( transaction ?. status ) . toBe ( 'cancelled' ) ;
197
+ expect ( mockedAppState . removeSubscription ) . toBeCalledTimes ( 1 ) ;
198
+ } ) ;
199
+
176
200
it ( 'Does not add app start measurement if more than 60s' , async ( ) => {
177
201
const integration = new ReactNativeTracing ( ) ;
178
202
@@ -212,16 +236,7 @@ describe('ReactNativeTracing', () => {
212
236
it ( 'Does not create app start transaction if didFetchAppStart == true' , async ( ) => {
213
237
const integration = new ReactNativeTracing ( ) ;
214
238
215
- const timeOriginMilliseconds = Date . now ( ) ;
216
- const appStartTimeMilliseconds = timeOriginMilliseconds - 100 ;
217
- const mockAppStartResponse : NativeAppStartResponse = {
218
- isColdStart : true ,
219
- appStartTime : appStartTimeMilliseconds ,
220
- didFetchAppStart : true ,
221
- } ;
222
-
223
- mockFunction ( getTimeOriginMilliseconds ) . mockReturnValue ( timeOriginMilliseconds ) ;
224
- mockFunction ( NATIVE . fetchNativeAppStart ) . mockResolvedValue ( mockAppStartResponse ) ;
239
+ mockAppStartResponse ( { cold : false , didFetchAppStart : true } ) ;
225
240
226
241
const mockHub = getMockHub ( ) ;
227
242
integration . setupOnce ( addGlobalEventProcessor , ( ) => mockHub ) ;
@@ -235,22 +250,38 @@ describe('ReactNativeTracing', () => {
235
250
} ) ;
236
251
237
252
describe ( 'With routing instrumentation' , ( ) => {
238
- it ( 'Adds measurements and child span onto existing routing transaction and sets the op (cold) ' , async ( ) => {
253
+ it ( 'Cancels route transaction when app goes to background ' , async ( ) => {
239
254
const routingInstrumentation = new RoutingInstrumentation ( ) ;
240
255
const integration = new ReactNativeTracing ( {
241
256
routingInstrumentation,
242
257
} ) ;
243
258
244
- const timeOriginMilliseconds = Date . now ( ) ;
245
- const appStartTimeMilliseconds = timeOriginMilliseconds - 100 ;
246
- const mockAppStartResponse : NativeAppStartResponse = {
247
- isColdStart : true ,
248
- appStartTime : appStartTimeMilliseconds ,
249
- didFetchAppStart : false ,
250
- } ;
259
+ mockAppStartResponse ( { cold : true } ) ;
251
260
252
- mockFunction ( getTimeOriginMilliseconds ) . mockReturnValue ( timeOriginMilliseconds ) ;
253
- mockFunction ( NATIVE . fetchNativeAppStart ) . mockResolvedValue ( mockAppStartResponse ) ;
261
+ const mockHub = getMockHub ( ) ;
262
+ integration . setupOnce ( addGlobalEventProcessor , ( ) => mockHub ) ;
263
+ // wait for internal promises to resolve, fetch app start data from mocked native
264
+ await Promise . resolve ( ) ;
265
+
266
+ const routeTransaction = routingInstrumentation . onRouteWillChange ( {
267
+ name : 'test' ,
268
+ } ) as IdleTransaction ;
269
+
270
+ mockedAppState . setState ( 'background' ) ;
271
+
272
+ jest . runAllTimers ( ) ;
273
+
274
+ expect ( routeTransaction . status ) . toBe ( 'cancelled' ) ;
275
+ expect ( mockedAppState . removeSubscription ) . toBeCalledTimes ( 1 ) ;
276
+ } ) ;
277
+
278
+ it ( 'Adds measurements and child span onto existing routing transaction and sets the op (cold)' , async ( ) => {
279
+ const routingInstrumentation = new RoutingInstrumentation ( ) ;
280
+ const integration = new ReactNativeTracing ( {
281
+ routingInstrumentation,
282
+ } ) ;
283
+
284
+ const [ timeOriginMilliseconds , appStartTimeMilliseconds ] = mockAppStartResponse ( { cold : true } ) ;
254
285
255
286
const mockHub = getMockHub ( ) ;
256
287
integration . setupOnce ( addGlobalEventProcessor , ( ) => mockHub ) ;
@@ -297,16 +328,7 @@ describe('ReactNativeTracing', () => {
297
328
routingInstrumentation,
298
329
} ) ;
299
330
300
- const timeOriginMilliseconds = Date . now ( ) ;
301
- const appStartTimeMilliseconds = timeOriginMilliseconds - 100 ;
302
- const mockAppStartResponse : NativeAppStartResponse = {
303
- isColdStart : false ,
304
- appStartTime : appStartTimeMilliseconds ,
305
- didFetchAppStart : false ,
306
- } ;
307
-
308
- mockFunction ( getTimeOriginMilliseconds ) . mockReturnValue ( timeOriginMilliseconds ) ;
309
- mockFunction ( NATIVE . fetchNativeAppStart ) . mockResolvedValue ( mockAppStartResponse ) ;
331
+ const [ timeOriginMilliseconds , appStartTimeMilliseconds ] = mockAppStartResponse ( { cold : false } ) ;
310
332
311
333
const mockHub = getMockHub ( ) ;
312
334
integration . setupOnce ( addGlobalEventProcessor , ( ) => mockHub ) ;
@@ -353,16 +375,7 @@ describe('ReactNativeTracing', () => {
353
375
routingInstrumentation,
354
376
} ) ;
355
377
356
- const timeOriginMilliseconds = Date . now ( ) ;
357
- const appStartTimeMilliseconds = timeOriginMilliseconds - 100 ;
358
- const mockAppStartResponse : NativeAppStartResponse = {
359
- isColdStart : false ,
360
- appStartTime : appStartTimeMilliseconds ,
361
- didFetchAppStart : true ,
362
- } ;
363
-
364
- mockFunction ( getTimeOriginMilliseconds ) . mockReturnValue ( timeOriginMilliseconds ) ;
365
- mockFunction ( NATIVE . fetchNativeAppStart ) . mockResolvedValue ( mockAppStartResponse ) ;
378
+ const [ , appStartTimeMilliseconds ] = mockAppStartResponse ( { cold : false , didFetchAppStart : true } ) ;
366
379
367
380
const mockHub = getMockHub ( ) ;
368
381
integration . setupOnce ( addGlobalEventProcessor , ( ) => mockHub ) ;
@@ -641,6 +654,24 @@ describe('ReactNativeTracing', () => {
641
654
expect ( actualTransactionContext ?. sampled ) . toEqual ( false ) ;
642
655
} ) ;
643
656
657
+ test ( 'does cancel UI event transaction when app goes to background' , ( ) => {
658
+ tracing . startUserInteractionTransaction ( mockedUserInteractionId ) ;
659
+
660
+ const actualTransaction = mockedScope . getTransaction ( ) as Transaction | undefined ;
661
+
662
+ mockedAppState . setState ( 'background' ) ;
663
+ jest . runAllTimers ( ) ;
664
+
665
+ const actualTransactionContext = actualTransaction ?. toContext ( ) ;
666
+ expect ( actualTransactionContext ) . toEqual (
667
+ expect . objectContaining ( {
668
+ endTimestamp : expect . any ( Number ) ,
669
+ status : 'cancelled' ,
670
+ } ) ,
671
+ ) ;
672
+ expect ( mockedAppState . removeSubscription ) . toBeCalledTimes ( 1 ) ;
673
+ } ) ;
674
+
644
675
test ( 'do not overwrite existing status of UI event transactions' , ( ) => {
645
676
tracing . startUserInteractionTransaction ( mockedUserInteractionId ) ;
646
677
@@ -792,3 +823,18 @@ describe('ReactNativeTracing', () => {
792
823
} ) ;
793
824
} ) ;
794
825
} ) ;
826
+
827
+ function mockAppStartResponse ( { cold, didFetchAppStart } : { cold : boolean ; didFetchAppStart ?: boolean } ) {
828
+ const timeOriginMilliseconds = Date . now ( ) ;
829
+ const appStartTimeMilliseconds = timeOriginMilliseconds - 100 ;
830
+ const mockAppStartResponse : NativeAppStartResponse = {
831
+ isColdStart : cold ,
832
+ appStartTime : appStartTimeMilliseconds ,
833
+ didFetchAppStart : didFetchAppStart ?? false ,
834
+ } ;
835
+
836
+ mockFunction ( getTimeOriginMilliseconds ) . mockReturnValue ( timeOriginMilliseconds ) ;
837
+ mockFunction ( NATIVE . fetchNativeAppStart ) . mockResolvedValue ( mockAppStartResponse ) ;
838
+
839
+ return [ timeOriginMilliseconds , appStartTimeMilliseconds ] ;
840
+ }
0 commit comments