@@ -147,6 +147,12 @@ export class Tracing implements Integration {
147
147
148
148
private static _performanceCursor : number = 0 ;
149
149
150
+ private static _heartbeatTimer : number = 0 ;
151
+
152
+ private static _prevHeartbeatString : string | undefined ;
153
+
154
+ private static _heartbeatCounter : number = 0 ;
155
+
150
156
/**
151
157
* Constructor for Tracing
152
158
*
@@ -200,27 +206,7 @@ export class Tracing implements Integration {
200
206
return ;
201
207
}
202
208
203
- if ( Tracing . options . traceXHR ) {
204
- addInstrumentationHandler ( {
205
- callback : xhrCallback ,
206
- type : 'xhr' ,
207
- } ) ;
208
- }
209
-
210
- if ( Tracing . options . traceFetch && supportsNativeFetch ( ) ) {
211
- addInstrumentationHandler ( {
212
- callback : fetchCallback ,
213
- type : 'fetch' ,
214
- } ) ;
215
- }
216
-
217
- if ( Tracing . options . startTransactionOnLocationChange ) {
218
- addInstrumentationHandler ( {
219
- callback : historyCallback ,
220
- type : 'history' ,
221
- } ) ;
222
- }
223
-
209
+ // Starting our inital pageload transaction
224
210
if ( global . location && global . location . href ) {
225
211
// `${global.location.href}` will be used a temp transaction name
226
212
Tracing . startIdleTransaction ( global . location . href , {
@@ -229,36 +215,17 @@ export class Tracing implements Integration {
229
215
} ) ;
230
216
}
231
217
232
- /**
233
- * If an error or unhandled promise occurs, we mark the active transaction as failed
234
- */
235
- // tslint:disable-next-line: completed-docs
236
- function errorCallback ( ) : void {
237
- if ( Tracing . _activeTransaction ) {
238
- logger . log ( `[Tracing] Global error occured, setting status in transaction: ${ SpanStatus . InternalError } ` ) ;
239
- ( Tracing . _activeTransaction as SpanClass ) . setStatus ( SpanStatus . InternalError ) ;
240
- }
241
- }
218
+ this . _setupXHRTracing ( ) ;
242
219
243
- addInstrumentationHandler ( {
244
- callback : errorCallback ,
245
- type : 'error' ,
246
- } ) ;
220
+ this . _setupFetchTracing ( ) ;
247
221
248
- addInstrumentationHandler ( {
249
- callback : errorCallback ,
250
- type : 'unhandledrejection' ,
251
- } ) ;
222
+ this . _setupHistory ( ) ;
252
223
253
- if ( Tracing . options . discardBackgroundSpans && global . document ) {
254
- document . addEventListener ( 'visibilitychange' , ( ) => {
255
- if ( document . hidden && Tracing . _activeTransaction ) {
256
- logger . log ( '[Tracing] Discarded active transaction incl. activities since tab moved to the background' ) ;
257
- Tracing . _activeTransaction = undefined ;
258
- Tracing . _activities = { } ;
259
- }
260
- } ) ;
261
- }
224
+ this . _setupErrorHandling ( ) ;
225
+
226
+ this . _setupBackgroundTabDetection ( ) ;
227
+
228
+ Tracing . _pingHeartbeat ( ) ;
262
229
263
230
// This EventProcessor makes sure that the transaction is not longer than maxTransactionDuration
264
231
addGlobalEventProcessor ( ( event : Event ) => {
@@ -284,6 +251,119 @@ export class Tracing implements Integration {
284
251
} ) ;
285
252
}
286
253
254
+ /**
255
+ * Pings the heartbeat
256
+ */
257
+ private static _pingHeartbeat ( ) : void {
258
+ Tracing . _heartbeatTimer = ( setTimeout ( ( ) => {
259
+ Tracing . _beat ( ) ;
260
+ } , 5000 ) as any ) as number ;
261
+ }
262
+
263
+ /**
264
+ * Checks when entries of Tracing._activities are not changing for 3 beats. If this occurs we finish the transaction
265
+ *
266
+ */
267
+ private static _beat ( ) : void {
268
+ clearTimeout ( Tracing . _heartbeatTimer ) ;
269
+ const keys = Object . keys ( Tracing . _activities ) ;
270
+ if ( keys . length ) {
271
+ const heartbeatString = keys . reduce ( ( prev : string , current : string ) => prev + current ) ;
272
+ if ( heartbeatString === Tracing . _prevHeartbeatString ) {
273
+ Tracing . _heartbeatCounter ++ ;
274
+ } else {
275
+ Tracing . _heartbeatCounter = 0 ;
276
+ }
277
+ if ( Tracing . _heartbeatCounter >= 3 ) {
278
+ if ( Tracing . _activeTransaction ) {
279
+ logger . log (
280
+ "[Tracing] Heartbeat safeguard kicked in, finishing transaction since activities content hasn't changed for 3 beats" ,
281
+ ) ;
282
+ Tracing . _activeTransaction . setStatus ( SpanStatus . DeadlineExceeded ) ;
283
+ Tracing . _activeTransaction . setTag ( 'heartbeat' , 'failed' ) ;
284
+ Tracing . finishIdleTransaction ( ) ;
285
+ }
286
+ }
287
+ Tracing . _prevHeartbeatString = heartbeatString ;
288
+ }
289
+ Tracing . _pingHeartbeat ( ) ;
290
+ }
291
+
292
+ /**
293
+ * Discards active transactions if tab moves to background
294
+ */
295
+ private _setupBackgroundTabDetection ( ) : void {
296
+ if ( Tracing . options . discardBackgroundSpans && global . document ) {
297
+ document . addEventListener ( 'visibilitychange' , ( ) => {
298
+ if ( document . hidden && Tracing . _activeTransaction ) {
299
+ logger . log ( '[Tracing] Discarded active transaction incl. activities since tab moved to the background' ) ;
300
+ Tracing . _activeTransaction = undefined ;
301
+ Tracing . _activities = { } ;
302
+ }
303
+ } ) ;
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Registers to History API to detect navigation changes
309
+ */
310
+ private _setupHistory ( ) : void {
311
+ if ( Tracing . options . startTransactionOnLocationChange ) {
312
+ addInstrumentationHandler ( {
313
+ callback : historyCallback ,
314
+ type : 'history' ,
315
+ } ) ;
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Attaches to fetch to add sentry-trace header + creating spans
321
+ */
322
+ private _setupFetchTracing ( ) : void {
323
+ if ( Tracing . options . traceFetch && supportsNativeFetch ( ) ) {
324
+ addInstrumentationHandler ( {
325
+ callback : fetchCallback ,
326
+ type : 'fetch' ,
327
+ } ) ;
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Attaches to XHR to add sentry-trace header + creating spans
333
+ */
334
+ private _setupXHRTracing ( ) : void {
335
+ if ( Tracing . options . traceXHR ) {
336
+ addInstrumentationHandler ( {
337
+ callback : xhrCallback ,
338
+ type : 'xhr' ,
339
+ } ) ;
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Configures global error listeners
345
+ */
346
+ private _setupErrorHandling ( ) : void {
347
+ // tslint:disable-next-line: completed-docs
348
+ function errorCallback ( ) : void {
349
+ if ( Tracing . _activeTransaction ) {
350
+ /**
351
+ * If an error or unhandled promise occurs, we mark the active transaction as failed
352
+ */
353
+ logger . log ( `[Tracing] Global error occured, setting status in transaction: ${ SpanStatus . InternalError } ` ) ;
354
+ ( Tracing . _activeTransaction as SpanClass ) . setStatus ( SpanStatus . InternalError ) ;
355
+ }
356
+ }
357
+ addInstrumentationHandler ( {
358
+ callback : errorCallback ,
359
+ type : 'error' ,
360
+ } ) ;
361
+ addInstrumentationHandler ( {
362
+ callback : errorCallback ,
363
+ type : 'unhandledrejection' ,
364
+ } ) ;
365
+ }
366
+
287
367
/**
288
368
* Is tracing enabled
289
369
*/
@@ -376,8 +456,8 @@ export class Tracing implements Integration {
376
456
public static finishIdleTransaction ( ) : void {
377
457
const active = Tracing . _activeTransaction as SpanClass ;
378
458
if ( active ) {
379
- logger . log ( '[Tracing] finishIdleTransaction' , active . transaction ) ;
380
459
Tracing . _addPerformanceEntries ( active ) ;
460
+ logger . log ( '[Tracing] finishIdleTransaction' , active . transaction ) ;
381
461
// true = use timestamp of last span
382
462
active . finish ( true ) ;
383
463
}
0 commit comments