Skip to content

Commit 1d5c610

Browse files
outsiderisLms24
andauthored
feat(tracing): Make BrowserTracing heartbeat interval configurable (#5867)
Make the heartbeat interval of an `IdleTransaction` configurable via `BrowserTracingOptions`. Add an optional parameter to `startIdleTransaction` to pass in a custom headtbeat interval. Signed-off-by: Outsider <[email protected]> Co-authored-by: Lukas Stracke <[email protected]>
1 parent 4bbfb2a commit 1d5c610

File tree

5 files changed

+75
-23
lines changed

5 files changed

+75
-23
lines changed

packages/tracing/src/browser/browsertracing.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { EventProcessor, Integration, Transaction, TransactionContext } from '@s
44
import { baggageHeaderToDynamicSamplingContext, getDomElement, getGlobalObject, logger } from '@sentry/utils';
55

66
import { startIdleTransaction } from '../hubextensions';
7-
import { DEFAULT_FINAL_TIMEOUT, DEFAULT_IDLE_TIMEOUT } from '../idletransaction';
7+
import { DEFAULT_FINAL_TIMEOUT, DEFAULT_HEARTBEAT_INTERVAL, DEFAULT_IDLE_TIMEOUT } from '../idletransaction';
88
import { extractTraceparentData } from '../utils';
99
import { registerBackgroundTabDetection } from './backgroundtab';
1010
import { addPerformanceEntries, startTrackingLongTasks, startTrackingWebVitals } from './metrics';
@@ -40,6 +40,15 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions {
4040
*/
4141
finalTimeout: number;
4242

43+
/**
44+
* The heartbeat interval. If no new spans are started or open spans are finished within 3 heartbeats,
45+
* the transaction will be finished.
46+
* Time is in ms.
47+
*
48+
* Default: 5000
49+
*/
50+
heartbeatInterval: number;
51+
4352
/**
4453
* Flag to enable/disable creation of `navigation` transaction on history changes.
4554
*
@@ -105,6 +114,7 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions {
105114
const DEFAULT_BROWSER_TRACING_OPTIONS = {
106115
idleTimeout: DEFAULT_IDLE_TIMEOUT,
107116
finalTimeout: DEFAULT_FINAL_TIMEOUT,
117+
heartbeatInterval: DEFAULT_HEARTBEAT_INTERVAL,
108118
markBackgroundTransactions: true,
109119
routingInstrumentation: instrumentRoutingWithDefaults,
110120
startTransactionOnLocationChange: true,
@@ -213,7 +223,7 @@ export class BrowserTracing implements Integration {
213223
}
214224

215225
// eslint-disable-next-line @typescript-eslint/unbound-method
216-
const { beforeNavigate, idleTimeout, finalTimeout } = this.options;
226+
const { beforeNavigate, idleTimeout, finalTimeout, heartbeatInterval } = this.options;
217227

218228
const isPageloadTransaction = context.op === 'pageload';
219229

@@ -264,6 +274,7 @@ export class BrowserTracing implements Integration {
264274
finalTimeout,
265275
true,
266276
{ location }, // for use in the tracesSampler
277+
heartbeatInterval,
267278
);
268279
idleTransaction.registerBeforeFinishCallback(transaction => {
269280
addPerformanceEntries(transaction);

packages/tracing/src/hubextensions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,12 @@ export function startIdleTransaction(
188188
finalTimeout: number,
189189
onScope?: boolean,
190190
customSamplingContext?: CustomSamplingContext,
191+
heartbeatInterval?: number,
191192
): IdleTransaction {
192193
const client = hub.getClient();
193194
const options: Partial<ClientOptions> = (client && client.getOptions()) || {};
194195

195-
let transaction = new IdleTransaction(transactionContext, hub, idleTimeout, finalTimeout, onScope);
196+
let transaction = new IdleTransaction(transactionContext, hub, idleTimeout, finalTimeout, heartbeatInterval, onScope);
196197
transaction = sample(transaction, options, {
197198
parentSampled: transactionContext.parentSampled,
198199
transactionContext,

packages/tracing/src/idletransaction.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Transaction } from './transaction';
88

99
export const DEFAULT_IDLE_TIMEOUT = 1000;
1010
export const DEFAULT_FINAL_TIMEOUT = 30000;
11-
export const HEARTBEAT_INTERVAL = 5000;
11+
export const DEFAULT_HEARTBEAT_INTERVAL = 5000;
1212

1313
/**
1414
* @inheritDoc
@@ -85,6 +85,7 @@ export class IdleTransaction extends Transaction {
8585
* The final value in ms that a transaction cannot exceed
8686
*/
8787
private readonly _finalTimeout: number = DEFAULT_FINAL_TIMEOUT,
88+
private readonly _heartbeatInterval: number = DEFAULT_HEARTBEAT_INTERVAL,
8889
// Whether or not the transaction should put itself on the scope when it starts and pop itself off when it ends
8990
private readonly _onScope: boolean = false,
9091
) {
@@ -287,7 +288,7 @@ export class IdleTransaction extends Transaction {
287288
__DEBUG_BUILD__ && logger.log(`pinging Heartbeat -> current counter: ${this._heartbeatCounter}`);
288289
setTimeout(() => {
289290
this._beat();
290-
}, HEARTBEAT_INTERVAL);
291+
}, this._heartbeatInterval);
291292
}
292293
}
293294

packages/tracing/test/browser/browsertracing.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import { BrowserTracing, BrowserTracingOptions, getMetaContent } from '../../src
88
import { defaultRequestInstrumentationOptions } from '../../src/browser/request';
99
import { instrumentRoutingWithDefaults } from '../../src/browser/router';
1010
import * as hubExtensions from '../../src/hubextensions';
11-
import { DEFAULT_FINAL_TIMEOUT, DEFAULT_IDLE_TIMEOUT, IdleTransaction } from '../../src/idletransaction';
11+
import {
12+
DEFAULT_FINAL_TIMEOUT,
13+
DEFAULT_HEARTBEAT_INTERVAL,
14+
DEFAULT_IDLE_TIMEOUT,
15+
IdleTransaction,
16+
} from '../../src/idletransaction';
1217
import { getActiveTransaction } from '../../src/utils';
1318
import { getDefaultBrowserClientOptions } from '../testutils';
1419

@@ -83,6 +88,7 @@ describe('BrowserTracing', () => {
8388
},
8489
idleTimeout: DEFAULT_IDLE_TIMEOUT,
8590
finalTimeout: DEFAULT_FINAL_TIMEOUT,
91+
heartbeatInterval: DEFAULT_HEARTBEAT_INTERVAL,
8692
markBackgroundTransactions: true,
8793
routingInstrumentation: instrumentRoutingWithDefaults,
8894
startTransactionOnLocationChange: true,
@@ -268,6 +274,7 @@ describe('BrowserTracing', () => {
268274
expect.any(Number),
269275
expect.any(Boolean),
270276
expect.any(Object),
277+
expect.any(Number),
271278
);
272279
});
273280

@@ -300,6 +307,23 @@ describe('BrowserTracing', () => {
300307
expect(mockFinish).toHaveBeenCalledTimes(1);
301308
});
302309
});
310+
311+
describe('heartbeatInterval', () => {
312+
it('can be a custom value', () => {
313+
const interval = 200;
314+
createBrowserTracing(true, { heartbeatInterval: interval, routingInstrumentation: customInstrumentRouting });
315+
const mockFinish = jest.fn();
316+
const transaction = getActiveTransaction(hub) as IdleTransaction;
317+
transaction.finish = mockFinish;
318+
319+
const span = transaction.startChild(); // activities = 1
320+
span.finish(); // activities = 0
321+
322+
expect(mockFinish).toHaveBeenCalledTimes(0);
323+
jest.advanceTimersByTime(interval * 3);
324+
expect(mockFinish).toHaveBeenCalledTimes(1);
325+
});
326+
});
303327
});
304328

305329
// Integration tests for the default routing instrumentation

packages/tracing/test/idletransaction.test.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { Hub } from '@sentry/hub';
33

44
import {
55
DEFAULT_FINAL_TIMEOUT,
6+
DEFAULT_HEARTBEAT_INTERVAL,
67
DEFAULT_IDLE_TIMEOUT,
7-
HEARTBEAT_INTERVAL,
88
IdleTransaction,
99
IdleTransactionSpanRecorder,
1010
} from '../src/idletransaction';
@@ -21,7 +21,14 @@ beforeEach(() => {
2121
describe('IdleTransaction', () => {
2222
describe('onScope', () => {
2323
it('sets the transaction on the scope on creation if onScope is true', () => {
24-
const transaction = new IdleTransaction({ name: 'foo' }, hub, DEFAULT_IDLE_TIMEOUT, DEFAULT_FINAL_TIMEOUT, true);
24+
const transaction = new IdleTransaction(
25+
{ name: 'foo' },
26+
hub,
27+
DEFAULT_IDLE_TIMEOUT,
28+
DEFAULT_FINAL_TIMEOUT,
29+
DEFAULT_HEARTBEAT_INTERVAL,
30+
true,
31+
);
2532
transaction.initSpanRecorder(10);
2633

2734
hub.configureScope(s => {
@@ -39,7 +46,14 @@ describe('IdleTransaction', () => {
3946
});
4047

4148
it('removes sampled transaction from scope on finish if onScope is true', () => {
42-
const transaction = new IdleTransaction({ name: 'foo' }, hub, DEFAULT_IDLE_TIMEOUT, DEFAULT_FINAL_TIMEOUT, true);
49+
const transaction = new IdleTransaction(
50+
{ name: 'foo' },
51+
hub,
52+
DEFAULT_IDLE_TIMEOUT,
53+
DEFAULT_FINAL_TIMEOUT,
54+
DEFAULT_HEARTBEAT_INTERVAL,
55+
true,
56+
);
4357
transaction.initSpanRecorder(10);
4458

4559
transaction.finish();
@@ -56,6 +70,7 @@ describe('IdleTransaction', () => {
5670
hub,
5771
DEFAULT_IDLE_TIMEOUT,
5872
DEFAULT_FINAL_TIMEOUT,
73+
DEFAULT_HEARTBEAT_INTERVAL,
5974
true,
6075
);
6176

@@ -248,17 +263,17 @@ describe('IdleTransaction', () => {
248263
expect(mockFinish).toHaveBeenCalledTimes(0);
249264

250265
// Beat 1
251-
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
266+
jest.advanceTimersByTime(DEFAULT_HEARTBEAT_INTERVAL);
252267
expect(transaction.status).not.toEqual('deadline_exceeded');
253268
expect(mockFinish).toHaveBeenCalledTimes(0);
254269

255270
// Beat 2
256-
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
271+
jest.advanceTimersByTime(DEFAULT_HEARTBEAT_INTERVAL);
257272
expect(transaction.status).not.toEqual('deadline_exceeded');
258273
expect(mockFinish).toHaveBeenCalledTimes(0);
259274

260275
// Beat 3
261-
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
276+
jest.advanceTimersByTime(DEFAULT_HEARTBEAT_INTERVAL);
262277
expect(transaction.status).not.toEqual('deadline_exceeded');
263278
expect(mockFinish).toHaveBeenCalledTimes(0);
264279
});
@@ -272,15 +287,15 @@ describe('IdleTransaction', () => {
272287
transaction.startChild({});
273288

274289
// Beat 1
275-
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
290+
jest.advanceTimersByTime(DEFAULT_HEARTBEAT_INTERVAL);
276291
expect(mockFinish).toHaveBeenCalledTimes(0);
277292

278293
// Beat 2
279-
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
294+
jest.advanceTimersByTime(DEFAULT_HEARTBEAT_INTERVAL);
280295
expect(mockFinish).toHaveBeenCalledTimes(0);
281296

282297
// Beat 3
283-
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
298+
jest.advanceTimersByTime(DEFAULT_HEARTBEAT_INTERVAL);
284299
expect(mockFinish).toHaveBeenCalledTimes(1);
285300
});
286301

@@ -293,42 +308,42 @@ describe('IdleTransaction', () => {
293308
transaction.startChild({});
294309

295310
// Beat 1
296-
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
311+
jest.advanceTimersByTime(DEFAULT_HEARTBEAT_INTERVAL);
297312
expect(mockFinish).toHaveBeenCalledTimes(0);
298313

299314
const span = transaction.startChild(); // push activity
300315

301316
// Beat 1
302-
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
317+
jest.advanceTimersByTime(DEFAULT_HEARTBEAT_INTERVAL);
303318
expect(mockFinish).toHaveBeenCalledTimes(0);
304319

305320
// Beat 2
306-
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
321+
jest.advanceTimersByTime(DEFAULT_HEARTBEAT_INTERVAL);
307322
expect(mockFinish).toHaveBeenCalledTimes(0);
308323

309324
transaction.startChild(); // push activity
310325
transaction.startChild(); // push activity
311326

312327
// Beat 1
313-
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
328+
jest.advanceTimersByTime(DEFAULT_HEARTBEAT_INTERVAL);
314329
expect(mockFinish).toHaveBeenCalledTimes(0);
315330

316331
// Beat 2
317-
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
332+
jest.advanceTimersByTime(DEFAULT_HEARTBEAT_INTERVAL);
318333
expect(mockFinish).toHaveBeenCalledTimes(0);
319334

320335
span.finish(); // pop activity
321336

322337
// Beat 1
323-
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
338+
jest.advanceTimersByTime(DEFAULT_HEARTBEAT_INTERVAL);
324339
expect(mockFinish).toHaveBeenCalledTimes(0);
325340

326341
// Beat 2
327-
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
342+
jest.advanceTimersByTime(DEFAULT_HEARTBEAT_INTERVAL);
328343
expect(mockFinish).toHaveBeenCalledTimes(0);
329344

330345
// Beat 3
331-
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
346+
jest.advanceTimersByTime(DEFAULT_HEARTBEAT_INTERVAL);
332347
expect(mockFinish).toHaveBeenCalledTimes(1);
333348

334349
// Heartbeat does not keep going after finish has been called

0 commit comments

Comments
 (0)