Skip to content

Commit 268c7e7

Browse files
HazATkamilogorekrhcarvalho
authored
feat(apm): Heartbeat (#2478)
* ref: Setup code * feat: Add heartbeat to finish a transaction * meta: Changelog * chore: CodeReview * Update packages/apm/src/integrations/tracing.ts Co-Authored-By: Kamil Ogórek <[email protected]> * Update packages/apm/src/integrations/tracing.ts Co-Authored-By: Rodolfo Carvalho <[email protected]> Co-authored-by: Kamil Ogórek <[email protected]> Co-authored-by: Rodolfo Carvalho <[email protected]>
1 parent 76da9e7 commit 268c7e7

File tree

2 files changed

+131
-50
lines changed

2 files changed

+131
-50
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44

55
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
66

7+
- [apm] feat: Add a simple heartbeat check, if activities don't change in 3 beats, finish the transaction (#2478)
78
- [apm] feat: Make use of the `performance` browser API to provide better instrumentation (#2474)
8-
- [browser] ref: Move global error handler + unhandled promise rejection to instrument
9+
- [browser] ref: Move global error handler + unhandled promise rejection to instrument (#2475)
910

1011
## 5.13.2
1112

packages/apm/src/integrations/tracing.ts

Lines changed: 129 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)