Skip to content

Commit d941b7d

Browse files
committed
feat(tracing): Reset IdleTimeout based on activities count
1 parent e33d962 commit d941b7d

File tree

5 files changed

+87
-31
lines changed

5 files changed

+87
-31
lines changed

packages/tracing/src/browser/browsertracing.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { EventProcessor, Integration, Transaction, TransactionContext } from '@s
33
import { getGlobalObject, logger } from '@sentry/utils';
44

55
import { startIdleTransaction } from '../hubextensions';
6-
import { DEFAULT_IDLE_TIMEOUT, IdleTransaction } from '../idletransaction';
6+
import { DEFAULT_FINAL_TIMEOUT, DEFAULT_IDLE_TIMEOUT, IdleTransaction } from '../idletransaction';
77
import { extractTraceparentData, secToMs } from '../utils';
88
import { registerBackgroundTabDetection } from './backgroundtab';
99
import { MetricsInstrumentation } from './metrics';
@@ -21,12 +21,23 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions {
2121
/**
2222
* The time to wait in ms until the transaction will be finished. The transaction will use the end timestamp of
2323
* the last finished span as the endtime for the transaction.
24+
*
2425
* Time is in ms.
2526
*
2627
* Default: 1000
2728
*/
2829
idleTimeout: number;
2930

31+
/**
32+
* The max transaction duration for a transaction. If a transaction duration hits the `finalTimeout` value, it
33+
* will be finished.
34+
*
35+
* Time is in ms.
36+
*
37+
* Default: 30000
38+
*/
39+
finalTimeout: number;
40+
3041
/**
3142
* Flag to enable/disable creation of `navigation` transaction on history changes.
3243
*
@@ -93,6 +104,7 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions {
93104

94105
const DEFAULT_BROWSER_TRACING_OPTIONS = {
95106
idleTimeout: DEFAULT_IDLE_TIMEOUT,
107+
finalTimeout: DEFAULT_FINAL_TIMEOUT,
96108
markBackgroundTransactions: true,
97109
maxTransactionDuration: DEFAULT_MAX_TRANSACTION_DURATION_SECONDS,
98110
routingInstrumentation: instrumentRoutingWithDefaults,

packages/tracing/src/constants.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,9 @@
22
// Readonly type should enforce that this is not mutated.
33
export const FINISH_REASON_TAG = 'finishReason';
44

5-
export const IDLE_TRANSACTION_FINISH_REASONS = ['heartbeatFailed', 'idleTimeout', 'documentHidden'] as const;
5+
export const IDLE_TRANSACTION_FINISH_REASONS = [
6+
'heartbeatFailed',
7+
'idleTimeout',
8+
'documentHidden',
9+
'finalTimeout',
10+
] as const;

packages/tracing/src/idletransaction.ts

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable max-lines */
12
import { Hub } from '@sentry/hub';
23
import { TransactionContext } from '@sentry/types';
34
import { getGlobalObject, logger, timestampWithMs } from '@sentry/utils';
@@ -7,6 +8,7 @@ import { Span, SpanRecorder } from './span';
78
import { Transaction } from './transaction';
89

910
export const DEFAULT_IDLE_TIMEOUT = 1000;
11+
export const DEFAULT_FINAL_TIMEOUT = 30000;
1012
export const HEARTBEAT_INTERVAL = 5000;
1113

1214
const global = getGlobalObject<Window>();
@@ -73,18 +75,29 @@ export class IdleTransaction extends Transaction {
7375
* If a transaction is created and no activities are added, we want to make sure that
7476
* it times out properly. This is cleared and not used when activities are added.
7577
*/
76-
private _initTimeout: ReturnType<typeof global.setTimeout> | undefined;
78+
private _idleTimeoutID: ReturnType<typeof global.setTimeout> | undefined;
7779

7880
public constructor(
7981
transactionContext: TransactionContext,
8082
private readonly _idleHub?: Hub,
8183
/**
8284
* The time to wait in ms until the idle transaction will be finished.
8385
* @default 1000
86+
*
87+
* TODO: Make _idleTimeout and _finalTimeout required to reduce duplication when setting the options
88+
* in `BrowserTracing`. This is considered a breaking change to the IdleTransaction API,
89+
* so we need to make sure we communicate it with react native.
8490
*/
8591
private readonly _idleTimeout: number = DEFAULT_IDLE_TIMEOUT,
8692
// Whether or not the transaction should put itself on the scope when it starts and pop itself off when it ends
8793
private readonly _onScope: boolean = false,
94+
/**
95+
* The final value that a transaction cannot exceed
96+
* @default 15000
97+
* @experimental
98+
* @internal
99+
*/
100+
private readonly _finalTimeout: number = DEFAULT_FINAL_TIMEOUT,
88101
) {
89102
super(transactionContext, _idleHub);
90103

@@ -98,11 +111,13 @@ export class IdleTransaction extends Transaction {
98111
_idleHub.configureScope(scope => scope.setSpan(this));
99112
}
100113

101-
this._initTimeout = global.setTimeout(() => {
114+
this._startIdleTimeout();
115+
global.setTimeout(() => {
102116
if (!this._finished) {
117+
this.setTag(FINISH_REASON_TAG, IDLE_TRANSACTION_FINISH_REASONS[3]);
103118
this.finish();
104119
}
105-
}, this._idleTimeout);
120+
}, this._finalTimeout);
106121
}
107122

108123
/** {@inheritDoc} */
@@ -191,15 +206,35 @@ export class IdleTransaction extends Transaction {
191206
this.spanRecorder.add(this);
192207
}
193208

209+
/**
210+
* Creates an idletimeout
211+
*/
212+
private _cancelIdleTimeout(): void {
213+
if (this._idleTimeoutID) {
214+
global.clearTimeout(this._idleTimeoutID);
215+
this._idleTimeoutID = undefined;
216+
}
217+
}
218+
219+
/**
220+
* Creates an idletimeout
221+
*/
222+
private _startIdleTimeout(end?: Parameters<IdleTransaction['finish']>[0]): void {
223+
this._cancelIdleTimeout();
224+
this._idleTimeoutID = global.setTimeout(() => {
225+
if (!this._finished && Object.keys(this.activities).length === 0) {
226+
this.setTag(FINISH_REASON_TAG, IDLE_TRANSACTION_FINISH_REASONS[1]);
227+
this.finish(end);
228+
}
229+
}, this._idleTimeout);
230+
}
231+
194232
/**
195233
* Start tracking a specific activity.
196234
* @param spanId The span id that represents the activity
197235
*/
198236
private _pushActivity(spanId: string): void {
199-
if (this._initTimeout) {
200-
clearTimeout(this._initTimeout);
201-
this._initTimeout = undefined;
202-
}
237+
this._cancelIdleTimeout();
203238
logger.log(`[Tracing] pushActivity: ${spanId}`);
204239
this.activities[spanId] = true;
205240
logger.log('[Tracing] new activities count', Object.keys(this.activities).length);
@@ -222,13 +257,7 @@ export class IdleTransaction extends Transaction {
222257
// We need to add the timeout here to have the real endtimestamp of the transaction
223258
// Remember timestampWithMs is in seconds, timeout is in ms
224259
const end = timestampWithMs() + timeout / 1000;
225-
226-
global.setTimeout(() => {
227-
if (!this._finished) {
228-
this.setTag(FINISH_REASON_TAG, IDLE_TRANSACTION_FINISH_REASONS[1]);
229-
this.finish(end);
230-
}
231-
}, timeout);
260+
this._startIdleTimeout(end);
232261
}
233262
}
234263

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { MetricsInstrumentation } from '../../src/browser/metrics';
1414
import { defaultRequestInstrumentationOptions } from '../../src/browser/request';
1515
import { instrumentRoutingWithDefaults } from '../../src/browser/router';
1616
import * as hubExtensions from '../../src/hubextensions';
17-
import { DEFAULT_IDLE_TIMEOUT, IdleTransaction } from '../../src/idletransaction';
17+
import { DEFAULT_IDLE_TIMEOUT, IdleTransaction, DEFAULT_FINAL_TIMEOUT } from '../../src/idletransaction';
1818
import { getActiveTransaction, secToMs } from '../../src/utils';
1919

2020
let mockChangeHistory: ({ to, from }: { to: string; from?: string }) => void = () => undefined;
@@ -83,6 +83,7 @@ describe('BrowserTracing', () => {
8383

8484
expect(browserTracing.options).toEqual({
8585
idleTimeout: DEFAULT_IDLE_TIMEOUT,
86+
finalTimeout: DEFAULT_FINAL_TIMEOUT,
8687
markBackgroundTransactions: true,
8788
maxTransactionDuration: DEFAULT_MAX_TRANSACTION_DURATION_SECONDS,
8889
routingInstrumentation: instrumentRoutingWithDefaults,

packages/tracing/test/idletransaction.test.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,16 @@ import {
99
} from '../src/idletransaction';
1010
import { Span } from '../src/span';
1111

12-
export class SimpleTransport extends Transports.BaseTransport {}
12+
type TransportSendRequest = Transports.BaseTransport['_sendRequest'];
13+
14+
export class SimpleTransport extends Transports.BaseTransport {
15+
protected _sendRequest(
16+
_req: Parameters<TransportSendRequest>[0],
17+
_payload: Parameters<TransportSendRequest>[1],
18+
): ReturnType<TransportSendRequest> {
19+
throw new Error('Method not implemented.');
20+
}
21+
}
1322

1423
const dsn = 'https://[email protected]/42';
1524
let hub: Hub;
@@ -103,7 +112,7 @@ describe('IdleTransaction', () => {
103112

104113
expect(transaction.activities).toMatchObject({ [span.spanId]: true, [childSpan.spanId]: true });
105114
span.finish();
106-
jest.runOnlyPendingTimers();
115+
jest.advanceTimersByTime(DEFAULT_IDLE_TIMEOUT + 1);
107116

108117
expect(mockFinish).toHaveBeenCalledTimes(0);
109118
expect(transaction.activities).toMatchObject({ [childSpan.spanId]: true });
@@ -229,63 +238,63 @@ describe('IdleTransaction', () => {
229238
transaction.startChild({});
230239

231240
// Beat 1
232-
jest.runOnlyPendingTimers();
241+
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
233242
expect(mockFinish).toHaveBeenCalledTimes(0);
234243

235244
// Beat 2
236-
jest.runOnlyPendingTimers();
245+
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
237246
expect(mockFinish).toHaveBeenCalledTimes(0);
238247

239248
// Beat 3
240-
jest.runOnlyPendingTimers();
249+
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
241250
expect(mockFinish).toHaveBeenCalledTimes(1);
242251
});
243252

244253
it('resets after new activities are added', () => {
245-
const transaction = new IdleTransaction({ name: 'foo' }, hub, DEFAULT_IDLE_TIMEOUT);
254+
const transaction = new IdleTransaction({ name: 'foo' }, hub, DEFAULT_IDLE_TIMEOUT, false, 50000);
246255
const mockFinish = jest.spyOn(transaction, 'finish');
247256
transaction.initSpanRecorder(10);
248257

249258
expect(mockFinish).toHaveBeenCalledTimes(0);
250259
transaction.startChild({});
251260

252261
// Beat 1
253-
jest.runOnlyPendingTimers();
262+
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
254263
expect(mockFinish).toHaveBeenCalledTimes(0);
255264

256265
const span = transaction.startChild(); // push activity
257266

258267
// Beat 1
259-
jest.runOnlyPendingTimers();
268+
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
260269
expect(mockFinish).toHaveBeenCalledTimes(0);
261270

262271
// Beat 2
263-
jest.runOnlyPendingTimers();
272+
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
264273
expect(mockFinish).toHaveBeenCalledTimes(0);
265274

266275
transaction.startChild(); // push activity
267276
transaction.startChild(); // push activity
268277

269278
// Beat 1
270-
jest.runOnlyPendingTimers();
279+
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
271280
expect(mockFinish).toHaveBeenCalledTimes(0);
272281

273282
// Beat 2
274-
jest.runOnlyPendingTimers();
283+
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
275284
expect(mockFinish).toHaveBeenCalledTimes(0);
276285

277286
span.finish(); // pop activity
278287

279288
// Beat 1
280-
jest.runOnlyPendingTimers();
289+
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
281290
expect(mockFinish).toHaveBeenCalledTimes(0);
282291

283292
// Beat 2
284-
jest.runOnlyPendingTimers();
293+
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
285294
expect(mockFinish).toHaveBeenCalledTimes(0);
286295

287296
// Beat 3
288-
jest.runOnlyPendingTimers();
297+
jest.advanceTimersByTime(HEARTBEAT_INTERVAL);
289298
expect(mockFinish).toHaveBeenCalledTimes(1);
290299

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

0 commit comments

Comments
 (0)