Skip to content

Commit 2aa8cdb

Browse files
committed
fix(tracing-internal): Delay pageload transaction finish until document is interactive
1 parent e50cc8a commit 2aa8cdb

File tree

3 files changed

+62
-10
lines changed

3 files changed

+62
-10
lines changed

packages/core/src/tracing/hubextensions.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,22 @@ export function startIdleTransaction(
8989
onScope?: boolean,
9090
customSamplingContext?: CustomSamplingContext,
9191
heartbeatInterval?: number,
92+
delayAutoFinishUntilSignal: boolean = false,
9293
): IdleTransaction {
9394
// eslint-disable-next-line deprecation/deprecation
9495
const client = hub.getClient();
9596
const options: Partial<ClientOptions> = (client && client.getOptions()) || {};
9697

9798
// eslint-disable-next-line deprecation/deprecation
98-
let transaction = new IdleTransaction(transactionContext, hub, idleTimeout, finalTimeout, heartbeatInterval, onScope);
99+
let transaction = new IdleTransaction(
100+
transactionContext,
101+
hub,
102+
idleTimeout,
103+
finalTimeout,
104+
heartbeatInterval,
105+
onScope,
106+
delayAutoFinishUntilSignal,
107+
);
99108
transaction = sampleTransaction(transaction, options, {
100109
parentSampled: transactionContext.parentSampled,
101110
transactionContext,

packages/core/src/tracing/idletransaction.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export class IdleTransaction extends Transaction {
9595

9696
private _finishReason: (typeof IDLE_TRANSACTION_FINISH_REASONS)[number];
9797

98+
private _autoFinishAllowed: boolean;
99+
98100
/**
99101
* @deprecated Transactions will be removed in v8. Use spans instead.
100102
*/
@@ -113,6 +115,15 @@ export class IdleTransaction extends Transaction {
113115
private readonly _heartbeatInterval: number = TRACING_DEFAULTS.heartbeatInterval,
114116
// Whether or not the transaction should put itself on the scope when it starts and pop itself off when it ends
115117
private readonly _onScope: boolean = false,
118+
/**
119+
* When set to `true`, will disable the idle timeout (`_idleTimeout` option) and heartbeat mechanisms (`_heartbeatInterval`
120+
* option) until the `sendAutoFinishSignal()` method is called. The final timeout mechanism (`_finalTimeout` option)
121+
* will not be affected by this option, meaning the transaction will definitely be finished when the final timeout is
122+
* reached, no matter what this option is configured to.
123+
*
124+
* Defaults to `false`.
125+
*/
126+
delayAutoFinishUntilSignal: boolean = false,
116127
) {
117128
super(transactionContext, _idleHub);
118129

@@ -122,6 +133,7 @@ export class IdleTransaction extends Transaction {
122133
this._idleTimeoutCanceledPermanently = false;
123134
this._beforeFinishCallbacks = [];
124135
this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[4];
136+
this._autoFinishAllowed = !delayAutoFinishUntilSignal;
125137

126138
if (_onScope) {
127139
// We set the transaction here on the scope so error events pick up the trace
@@ -131,7 +143,10 @@ export class IdleTransaction extends Transaction {
131143
_idleHub.getScope().setSpan(this);
132144
}
133145

134-
this._restartIdleTimeout();
146+
if (!delayAutoFinishUntilSignal) {
147+
this._restartIdleTimeout();
148+
}
149+
135150
setTimeout(() => {
136151
if (!this._finished) {
137152
this.setStatus('deadline_exceeded');
@@ -215,7 +230,7 @@ export class IdleTransaction extends Transaction {
215230
}
216231

217232
/**
218-
* Register a callback function that gets excecuted before the transaction finishes.
233+
* Register a callback function that gets executed before the transaction finishes.
219234
* Useful for cleanup or if you want to add any additional spans based on current context.
220235
*
221236
* This is exposed because users have no other way of running something before an idle transaction
@@ -296,13 +311,24 @@ export class IdleTransaction extends Transaction {
296311
this._finishReason = reason;
297312
}
298313

314+
/**
315+
* Permits the IdleTransaction to automatically end itself via the idle timeout and heartbeat mechanisms when the `delayAutoFinishUntilSignal` option was set to `true`.
316+
*/
317+
public sendAutoFinishSignal(): void {
318+
if (!this._autoFinishAllowed) {
319+
DEBUG_BUILD && logger.log('[Tracing] Received finish signal for idle transaction.');
320+
this._restartIdleTimeout();
321+
this._autoFinishAllowed = true;
322+
}
323+
}
324+
299325
/**
300326
* Restarts idle timeout, if there is no running idle timeout it will start one.
301327
*/
302328
private _restartIdleTimeout(endTimestamp?: Parameters<IdleTransaction['end']>[0]): void {
303329
this.cancelIdleTimeout();
304330
this._idleTimeoutID = setTimeout(() => {
305-
if (!this._finished && Object.keys(this.activities).length === 0) {
331+
if (!this._finished && Object.keys(this.activities).length === 0 && this._autoFinishAllowed) {
306332
this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[1];
307333
this.end(endTimestamp);
308334
}
@@ -335,8 +361,10 @@ export class IdleTransaction extends Transaction {
335361
if (Object.keys(this.activities).length === 0) {
336362
const endTimestamp = timestampInSeconds();
337363
if (this._idleTimeoutCanceledPermanently) {
338-
this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[5];
339-
this.end(endTimestamp);
364+
if (this._autoFinishAllowed) {
365+
this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[5];
366+
this.end(endTimestamp);
367+
}
340368
} else {
341369
// We need to add the timeout here to have the real endtimestamp of the transaction
342370
// Remember timestampInSeconds is in seconds, timeout is in ms
@@ -366,10 +394,12 @@ export class IdleTransaction extends Transaction {
366394
this._prevHeartbeatString = heartbeatString;
367395

368396
if (this._heartbeatCounter >= 3) {
369-
DEBUG_BUILD && logger.log('[Tracing] Transaction finished because of no change for 3 heart beats');
370-
this.setStatus('deadline_exceeded');
371-
this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[0];
372-
this.end();
397+
if (this._autoFinishAllowed) {
398+
DEBUG_BUILD && logger.log('[Tracing] Transaction finished because of no change for 3 heart beats');
399+
this.setStatus('deadline_exceeded');
400+
this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[0];
401+
this.end();
402+
}
373403
} else {
374404
this._pingHeartbeat();
375405
}

packages/tracing-internal/src/browser/browsertracing.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,8 +368,21 @@ export class BrowserTracing implements Integration {
368368
true,
369369
{ location }, // for use in the tracesSampler
370370
heartbeatInterval,
371+
isPageloadTransaction, // should wait for finish signal if it is a pageload transaction
371372
);
372373

374+
if (isPageloadTransaction) {
375+
WINDOW.document.addEventListener('readystatechange', () => {
376+
if (WINDOW.document.readyState === 'interactive') {
377+
idleTransaction.sendFinishSignal();
378+
}
379+
});
380+
381+
if (WINDOW.document.readyState === 'interactive') {
382+
idleTransaction.sendFinishSignal();
383+
}
384+
}
385+
373386
// eslint-disable-next-line deprecation/deprecation
374387
const scope = hub.getScope();
375388

0 commit comments

Comments
 (0)