Skip to content

Commit 39be641

Browse files
committed
ref(tracing): Reset IdleTimeout based on activities count
1 parent 419416a commit 39be641

File tree

8 files changed

+141
-142
lines changed

8 files changed

+141
-142
lines changed

packages/integration-tests/suites/tracing/browsertracing/backgroundtab-custom/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,5 @@ sentryTest('should finish a custom transaction when the page goes background', a
3636
expect(id_before).toBe(id_after);
3737
expect(name_after).toBe(name_before);
3838
expect(status_after).toBe('cancelled');
39-
expect(tags_after).toStrictEqual({ finishReason: 'documentHidden', visibilitychange: 'document.hidden' });
39+
expect(tags_after).toStrictEqual({ visibilitychange: 'document.hidden' });
4040
});

packages/tracing/src/browser/backgroundtab.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { getGlobalObject, logger } from '@sentry/utils';
22

3-
import { FINISH_REASON_TAG, IDLE_TRANSACTION_FINISH_REASONS } from '../constants';
43
import { IS_DEBUG_BUILD } from '../flags';
54
import { IdleTransaction } from '../idletransaction';
65
import { SpanStatusType } from '../span';
@@ -29,7 +28,6 @@ export function registerBackgroundTabDetection(): void {
2928
activeTransaction.setStatus(statusType);
3029
}
3130
activeTransaction.setTag('visibilitychange', 'document.hidden');
32-
activeTransaction.setTag(FINISH_REASON_TAG, IDLE_TRANSACTION_FINISH_REASONS[2]);
3331
activeTransaction.finish();
3432
}
3533
});

packages/tracing/src/browser/browsertracing.ts

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { getGlobalObject, logger } from '@sentry/utils';
44

55
import { IS_DEBUG_BUILD } from '../flags';
66
import { startIdleTransaction } from '../hubextensions';
7-
import { DEFAULT_IDLE_TIMEOUT, IdleTransaction } from '../idletransaction';
8-
import { extractTraceparentData, secToMs } from '../utils';
7+
import { DEFAULT_FINAL_TIMEOUT, DEFAULT_IDLE_TIMEOUT } from '../idletransaction';
8+
import { extractTraceparentData } from '../utils';
99
import { registerBackgroundTabDetection } from './backgroundtab';
1010
import { MetricsInstrumentation } from './metrics';
1111
import {
@@ -15,19 +15,27 @@ import {
1515
} from './request';
1616
import { instrumentRoutingWithDefaults } from './router';
1717

18-
export const DEFAULT_MAX_TRANSACTION_DURATION_SECONDS = 600;
19-
2018
/** Options for Browser Tracing integration */
2119
export interface BrowserTracingOptions extends RequestInstrumentationOptions {
2220
/**
2321
* The time to wait in ms until the transaction will be finished. The transaction will use the end timestamp of
24-
* the last finished span as the endtime for the transaction.
22+
* the last finished span as the endtime for the transaction. If there are still active spans when this the
23+
* `idleTimeout` is set, the `idleTimeout` will get reset.
2524
* Time is in ms.
2625
*
2726
* Default: 1000
2827
*/
2928
idleTimeout: number;
3029

30+
/**
31+
* The max duration for a transaction. If a transaction duration hits the `finalTimeout` value, it
32+
* will be finished.
33+
* Time is in ms.
34+
*
35+
* Default: 30000
36+
*/
37+
finalTimeout: number;
38+
3139
/**
3240
* Flag to enable/disable creation of `navigation` transaction on history changes.
3341
*
@@ -42,15 +50,6 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions {
4250
*/
4351
startTransactionOnPageLoad: boolean;
4452

45-
/**
46-
* The maximum duration of a transaction before it will be marked as "deadline_exceeded".
47-
* If you never want to mark a transaction set it to 0.
48-
* Time is in seconds.
49-
*
50-
* Default: 600
51-
*/
52-
maxTransactionDuration: number;
53-
5453
/**
5554
* Flag Transactions where tabs moved to background with "cancelled". Browser background tab timing is
5655
* not suited towards doing precise measurements of operations. By default, we recommend that this option
@@ -94,8 +93,8 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions {
9493

9594
const DEFAULT_BROWSER_TRACING_OPTIONS = {
9695
idleTimeout: DEFAULT_IDLE_TIMEOUT,
96+
finalTimeout: DEFAULT_FINAL_TIMEOUT,
9797
markBackgroundTransactions: true,
98-
maxTransactionDuration: DEFAULT_MAX_TRANSACTION_DURATION_SECONDS,
9998
routingInstrumentation: instrumentRoutingWithDefaults,
10099
startTransactionOnLocationChange: true,
101100
startTransactionOnPageLoad: true,
@@ -129,14 +128,10 @@ export class BrowserTracing implements Integration {
129128

130129
private readonly _emitOptionsWarning?: boolean;
131130

132-
/** Store configured idle timeout so that it can be added as a tag to transactions */
133-
private _configuredIdleTimeout: BrowserTracingOptions['idleTimeout'] | undefined = undefined;
134-
135131
public constructor(_options?: Partial<BrowserTracingOptions>) {
136132
let tracingOrigins = defaultRequestInstrumentationOptions.tracingOrigins;
137133
// NOTE: Logger doesn't work in constructors, as it's initialized after integrations instances
138134
if (_options) {
139-
this._configuredIdleTimeout = _options.idleTimeout;
140135
if (_options.tracingOrigins && Array.isArray(_options.tracingOrigins) && _options.tracingOrigins.length !== 0) {
141136
tracingOrigins = _options.tracingOrigins;
142137
} else {
@@ -205,7 +200,7 @@ export class BrowserTracing implements Integration {
205200
}
206201

207202
// eslint-disable-next-line @typescript-eslint/unbound-method
208-
const { beforeNavigate, idleTimeout, maxTransactionDuration } = this.options;
203+
const { beforeNavigate, idleTimeout, finalTimeout } = this.options;
209204

210205
const parentContextFromHeader = context.op === 'pageload' ? getHeaderContext() : undefined;
211206

@@ -233,16 +228,14 @@ export class BrowserTracing implements Integration {
233228
hub,
234229
finalContext,
235230
idleTimeout,
231+
finalTimeout,
236232
true,
237233
{ location }, // for use in the tracesSampler
238234
);
239-
idleTransaction.registerBeforeFinishCallback((transaction, endTimestamp) => {
235+
idleTransaction.registerBeforeFinishCallback(transaction => {
240236
this._metrics.addPerformanceEntries(transaction);
241-
adjustTransactionDuration(secToMs(maxTransactionDuration), transaction, endTimestamp);
242237
});
243238

244-
idleTransaction.setTag('idleTimeout', this._configuredIdleTimeout);
245-
246239
return idleTransaction as Transaction;
247240
}
248241
}
@@ -266,13 +259,3 @@ export function getMetaContent(metaName: string): string | null {
266259
const el = getGlobalObject<Window>().document.querySelector(`meta[name=${metaName}]`);
267260
return el ? el.getAttribute('content') : null;
268261
}
269-
270-
/** Adjusts transaction value based on max transaction duration */
271-
function adjustTransactionDuration(maxDuration: number, transaction: IdleTransaction, endTimestamp: number): void {
272-
const diff = endTimestamp - transaction.startTimestamp;
273-
const isOutdatedTransaction = endTimestamp && (diff > maxDuration || diff < 0);
274-
if (isOutdatedTransaction) {
275-
transaction.setStatus('deadline_exceeded');
276-
transaction.setTag('maxTransactionDurationExceeded', 'true');
277-
}
278-
}

packages/tracing/src/constants.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/tracing/src/hubextensions.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,15 @@ function _startTransaction(
196196
export function startIdleTransaction(
197197
hub: Hub,
198198
transactionContext: TransactionContext,
199-
idleTimeout?: number,
199+
idleTimeout: number,
200+
finalTimeout: number,
200201
onScope?: boolean,
201202
customSamplingContext?: CustomSamplingContext,
202203
): IdleTransaction {
203204
const client = hub.getClient();
204205
const options: Partial<ClientOptions> = (client && client.getOptions()) || {};
205206

206-
let transaction = new IdleTransaction(transactionContext, hub, idleTimeout, onScope);
207+
let transaction = new IdleTransaction(transactionContext, hub, idleTimeout, finalTimeout, onScope);
207208
transaction = sample(transaction, options, {
208209
parentSampled: transactionContext.parentSampled,
209210
transactionContext,

packages/tracing/src/idletransaction.ts

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
1+
/* eslint-disable max-lines */
12
import { Hub } from '@sentry/hub';
23
import { TransactionContext } from '@sentry/types';
3-
import { logger, timestampWithMs } from '@sentry/utils';
4+
import { getGlobalObject, logger, timestampWithMs } from '@sentry/utils';
45

5-
import { FINISH_REASON_TAG, IDLE_TRANSACTION_FINISH_REASONS } from './constants';
66
import { IS_DEBUG_BUILD } from './flags';
77
import { Span, SpanRecorder } from './span';
88
import { Transaction } from './transaction';
99

1010
export const DEFAULT_IDLE_TIMEOUT = 1000;
11+
export const DEFAULT_FINAL_TIMEOUT = 30000;
1112
export const HEARTBEAT_INTERVAL = 5000;
1213

14+
const global = getGlobalObject<Window>();
15+
1316
/**
1417
* @inheritDoc
1518
*/
1619
export class IdleTransactionSpanRecorder extends SpanRecorder {
1720
public constructor(
1821
private readonly _pushActivity: (id: string) => void,
1922
private readonly _popActivity: (id: string) => void,
20-
public transactionSpanId: string = '',
23+
public transactionSpanId: string,
2124
maxlen?: number,
2225
) {
2326
super(maxlen);
@@ -69,25 +72,27 @@ export class IdleTransaction extends Transaction {
6972
private readonly _beforeFinishCallbacks: BeforeFinishCallback[] = [];
7073

7174
/**
72-
* If a transaction is created and no activities are added, we want to make sure that
73-
* it times out properly. This is cleared and not used when activities are added.
75+
* Timer that tracks a
7476
*/
75-
private _initTimeout: ReturnType<typeof setTimeout> | undefined;
77+
private _idleTimeoutID: ReturnType<typeof global.setTimeout> | undefined;
7678

7779
public constructor(
7880
transactionContext: TransactionContext,
79-
private readonly _idleHub?: Hub,
81+
private readonly _idleHub: Hub,
8082
/**
8183
* The time to wait in ms until the idle transaction will be finished.
82-
* @default 1000
8384
*/
8485
private readonly _idleTimeout: number = DEFAULT_IDLE_TIMEOUT,
86+
/**
87+
* The final value in ms that a transaction cannot exceed
88+
*/
89+
private readonly _finalTimeout: number = DEFAULT_FINAL_TIMEOUT,
8590
// Whether or not the transaction should put itself on the scope when it starts and pop itself off when it ends
8691
private readonly _onScope: boolean = false,
8792
) {
8893
super(transactionContext, _idleHub);
8994

90-
if (_idleHub && _onScope) {
95+
if (_onScope) {
9196
// There should only be one active transaction on the scope
9297
clearActiveTransaction(_idleHub);
9398

@@ -97,16 +102,19 @@ export class IdleTransaction extends Transaction {
97102
_idleHub.configureScope(scope => scope.setSpan(this));
98103
}
99104

100-
this._initTimeout = setTimeout(() => {
105+
this._startIdleTimeout();
106+
global.setTimeout(() => {
101107
if (!this._finished) {
108+
this.setStatus('deadline_exceeded');
102109
this.finish();
103110
}
104-
}, this._idleTimeout);
111+
}, this._finalTimeout);
105112
}
106113

107114
/** {@inheritDoc} */
108115
public finish(endTimestamp: number = timestampWithMs()): string | undefined {
109116
this._finished = true;
117+
this._cancelIdleTimeout();
110118
this.activities = {};
111119

112120
if (this.spanRecorder) {
@@ -193,15 +201,34 @@ export class IdleTransaction extends Transaction {
193201
this.spanRecorder.add(this);
194202
}
195203

204+
/**
205+
* Cancels the existing idletimeout, if there is one
206+
*/
207+
private _cancelIdleTimeout(): void {
208+
if (this._idleTimeoutID) {
209+
global.clearTimeout(this._idleTimeoutID);
210+
this._idleTimeoutID = undefined;
211+
}
212+
}
213+
214+
/**
215+
* Creates an idletimeout
216+
*/
217+
private _startIdleTimeout(endTimestamp?: Parameters<IdleTransaction['finish']>[0]): void {
218+
this._cancelIdleTimeout();
219+
this._idleTimeoutID = global.setTimeout(() => {
220+
if (!this._finished && Object.keys(this.activities).length === 0) {
221+
this.finish(endTimestamp);
222+
}
223+
}, this._idleTimeout);
224+
}
225+
196226
/**
197227
* Start tracking a specific activity.
198228
* @param spanId The span id that represents the activity
199229
*/
200230
private _pushActivity(spanId: string): void {
201-
if (this._initTimeout) {
202-
clearTimeout(this._initTimeout);
203-
this._initTimeout = undefined;
204-
}
231+
this._cancelIdleTimeout();
205232
IS_DEBUG_BUILD && logger.log(`[Tracing] pushActivity: ${spanId}`);
206233
this.activities[spanId] = true;
207234
IS_DEBUG_BUILD && logger.log('[Tracing] new activities count', Object.keys(this.activities).length);
@@ -220,17 +247,10 @@ export class IdleTransaction extends Transaction {
220247
}
221248

222249
if (Object.keys(this.activities).length === 0) {
223-
const timeout = this._idleTimeout;
224250
// We need to add the timeout here to have the real endtimestamp of the transaction
225251
// Remember timestampWithMs is in seconds, timeout is in ms
226-
const end = timestampWithMs() + timeout / 1000;
227-
228-
setTimeout(() => {
229-
if (!this._finished) {
230-
this.setTag(FINISH_REASON_TAG, IDLE_TRANSACTION_FINISH_REASONS[1]);
231-
this.finish(end);
232-
}
233-
}, timeout);
252+
const endTimestamp = timestampWithMs() + this._idleTimeout / 1000;
253+
this._startIdleTimeout(endTimestamp);
234254
}
235255
}
236256

@@ -257,7 +277,6 @@ export class IdleTransaction extends Transaction {
257277
if (this._heartbeatCounter >= 3) {
258278
IS_DEBUG_BUILD && logger.log('[Tracing] Transaction finished because of no change for 3 heart beats');
259279
this.setStatus('deadline_exceeded');
260-
this.setTag(FINISH_REASON_TAG, IDLE_TRANSACTION_FINISH_REASONS[0]);
261280
this.finish();
262281
} else {
263282
this._pingHeartbeat();
@@ -269,7 +288,7 @@ export class IdleTransaction extends Transaction {
269288
*/
270289
private _pingHeartbeat(): void {
271290
IS_DEBUG_BUILD && logger.log(`pinging Heartbeat -> current counter: ${this._heartbeatCounter}`);
272-
setTimeout(() => {
291+
global.setTimeout(() => {
273292
this._beat();
274293
}, HEARTBEAT_INTERVAL);
275294
}

0 commit comments

Comments
 (0)