@@ -147,6 +147,12 @@ export class Tracing implements Integration {
147147
148148 private static _performanceCursor : number = 0 ;
149149
150+ private static _heartbeatTimer : number = 0 ;
151+
152+ private static _prevHeartbeatString : string | undefined ;
153+
154+ private static _heartbeatCounter : number = 0 ;
155+
150156 /**
151157 * Constructor for Tracing
152158 *
@@ -200,27 +206,7 @@ export class Tracing implements Integration {
200206 return ;
201207 }
202208
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
224210 if ( global . location && global . location . href ) {
225211 // `${global.location.href}` will be used a temp transaction name
226212 Tracing . startIdleTransaction ( global . location . href , {
@@ -229,36 +215,17 @@ export class Tracing implements Integration {
229215 } ) ;
230216 }
231217
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 ( ) ;
242219
243- addInstrumentationHandler ( {
244- callback : errorCallback ,
245- type : 'error' ,
246- } ) ;
220+ this . _setupFetchTracing ( ) ;
247221
248- addInstrumentationHandler ( {
249- callback : errorCallback ,
250- type : 'unhandledrejection' ,
251- } ) ;
222+ this . _setupHistory ( ) ;
252223
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 ( ) ;
262229
263230 // This EventProcessor makes sure that the transaction is not longer than maxTransactionDuration
264231 addGlobalEventProcessor ( ( event : Event ) => {
@@ -284,6 +251,119 @@ export class Tracing implements Integration {
284251 } ) ;
285252 }
286253
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+
287367 /**
288368 * Is tracing enabled
289369 */
@@ -376,8 +456,8 @@ export class Tracing implements Integration {
376456 public static finishIdleTransaction ( ) : void {
377457 const active = Tracing . _activeTransaction as SpanClass ;
378458 if ( active ) {
379- logger . log ( '[Tracing] finishIdleTransaction' , active . transaction ) ;
380459 Tracing . _addPerformanceEntries ( active ) ;
460+ logger . log ( '[Tracing] finishIdleTransaction' , active . transaction ) ;
381461 // true = use timestamp of last span
382462 active . finish ( true ) ;
383463 }
0 commit comments