Skip to content

Commit fdde150

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

File tree

10 files changed

+146
-144
lines changed

10 files changed

+146
-144
lines changed

MIGRATION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ For our efforts to reduce bundle size of the SDK we had to remove and refactor p
291291
- Rename `UserAgent` integration to `HttpContext`. (see [#5027](https://github.com/getsentry/sentry-javascript/pull/5027))
292292
- Remove `SDK_NAME` export from `@sentry/browser`, `@sentry/node`, `@sentry/tracing` and `@sentry/vue` packages.
293293
- Removed `eventStatusFromHttpCode` to save on bundle size.
294+
- Replace `BrowserTracing` `maxTransactionDuration` option with `finalTimeout` option
294295

295296
## Sentry Angular SDK Changes
296297

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/integration-tests/suites/tracing/browsertracing/backgroundtab-pageload/test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,5 @@ sentryTest('should finish pageload transaction when the page goes background', a
1717
expect(pageloadTransaction.contexts?.trace.status).toBe('cancelled');
1818
expect(pageloadTransaction.contexts?.trace.tags).toMatchObject({
1919
visibilitychange: 'document.hidden',
20-
finishReason: 'documentHidden',
2120
});
2221
});

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: 20 additions & 35 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,29 @@ 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
/**
23-
* 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.
21+
* The time to wait in ms until the transaction will be finished during an idle state. An idle state is defined
22+
* by a moment where there are no in-progress spans.
23+
*
24+
* The transaction will use the end timestamp of the last finished span as the endtime for the transaction.
25+
* If there are still active spans when this the `idleTimeout` is set, the `idleTimeout` will get reset.
2526
* Time is in ms.
2627
*
2728
* Default: 1000
2829
*/
2930
idleTimeout: number;
3031

32+
/**
33+
* The max duration for a transaction. If a transaction duration hits the `finalTimeout` value, it
34+
* will be finished.
35+
* Time is in ms.
36+
*
37+
* Default: 30000
38+
*/
39+
finalTimeout: number;
40+
3141
/**
3242
* Flag to enable/disable creation of `navigation` transaction on history changes.
3343
*
@@ -42,15 +52,6 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions {
4252
*/
4353
startTransactionOnPageLoad: boolean;
4454

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-
5455
/**
5556
* Flag Transactions where tabs moved to background with "cancelled". Browser background tab timing is
5657
* not suited towards doing precise measurements of operations. By default, we recommend that this option
@@ -94,8 +95,8 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions {
9495

9596
const DEFAULT_BROWSER_TRACING_OPTIONS = {
9697
idleTimeout: DEFAULT_IDLE_TIMEOUT,
98+
finalTimeout: DEFAULT_FINAL_TIMEOUT,
9799
markBackgroundTransactions: true,
98-
maxTransactionDuration: DEFAULT_MAX_TRANSACTION_DURATION_SECONDS,
99100
routingInstrumentation: instrumentRoutingWithDefaults,
100101
startTransactionOnLocationChange: true,
101102
startTransactionOnPageLoad: true,
@@ -129,14 +130,10 @@ export class BrowserTracing implements Integration {
129130

130131
private readonly _emitOptionsWarning?: boolean;
131132

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

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

210207
const parentContextFromHeader = context.op === 'pageload' ? getHeaderContext() : undefined;
211208

@@ -233,16 +230,14 @@ export class BrowserTracing implements Integration {
233230
hub,
234231
finalContext,
235232
idleTimeout,
233+
finalTimeout,
236234
true,
237235
{ location }, // for use in the tracesSampler
238236
);
239-
idleTransaction.registerBeforeFinishCallback((transaction, endTimestamp) => {
237+
idleTransaction.registerBeforeFinishCallback(transaction => {
240238
this._metrics.addPerformanceEntries(transaction);
241-
adjustTransactionDuration(secToMs(maxTransactionDuration), transaction, endTimestamp);
242239
});
243240

244-
idleTransaction.setTag('idleTimeout', this._configuredIdleTimeout);
245-
246241
return idleTransaction as Transaction;
247242
}
248243
}
@@ -266,13 +261,3 @@ export function getMetaContent(metaName: string): string | null {
266261
const el = getGlobalObject<Window>().document.querySelector(`meta[name=${metaName}]`);
267262
return el ? el.getAttribute('content') : null;
268263
}
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: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable max-lines */
12
import { getMainCarrier, Hub } from '@sentry/hub';
23
import {
34
ClientOptions,
@@ -196,14 +197,15 @@ function _startTransaction(
196197
export function startIdleTransaction(
197198
hub: Hub,
198199
transactionContext: TransactionContext,
199-
idleTimeout?: number,
200+
idleTimeout: number,
201+
finalTimeout: number,
200202
onScope?: boolean,
201203
customSamplingContext?: CustomSamplingContext,
202204
): IdleTransaction {
203205
const client = hub.getClient();
204206
const options: Partial<ClientOptions> = (client && client.getOptions()) || {};
205207

206-
let transaction = new IdleTransaction(transactionContext, hub, idleTimeout, onScope);
208+
let transaction = new IdleTransaction(transactionContext, hub, idleTimeout, finalTimeout, onScope);
207209
transaction = sample(transaction, options, {
208210
parentSampled: transactionContext.parentSampled,
209211
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)