Skip to content

Commit a51d717

Browse files
committed
feat: Add span creators to @sentry/tracing package (#2736)
1 parent 9587853 commit a51d717

File tree

13 files changed

+1090
-77
lines changed

13 files changed

+1090
-77
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { getGlobalObject, logger } from '@sentry/utils';
2+
3+
import { IdleTransaction } from '../idletransaction';
4+
import { SpanStatus } from '../spanstatus';
5+
6+
import { getActiveTransaction } from './utils';
7+
8+
const global = getGlobalObject<Window>();
9+
10+
/**
11+
* Add a listener that cancels and finishes a transaction when the global
12+
* document is hidden.
13+
*/
14+
export function registerBackgroundTabDetection(): void {
15+
if (global && global.document) {
16+
global.document.addEventListener('visibilitychange', () => {
17+
const activeTransaction = getActiveTransaction() as IdleTransaction;
18+
if (global.document.hidden && activeTransaction) {
19+
logger.log(
20+
`[Tracing] Transaction: ${SpanStatus.Cancelled} -> since tab moved to the background, op: ${
21+
activeTransaction.op
22+
}`,
23+
);
24+
activeTransaction.setStatus(SpanStatus.Cancelled);
25+
activeTransaction.setTag('visibilitychange', 'document.hidden');
26+
activeTransaction.finish();
27+
}
28+
});
29+
} else {
30+
logger.warn('[Tracing] Could not set up background tab detection due to lack of global document');
31+
}
32+
}

packages/tracing/src/browser/browsertracing.ts

Lines changed: 101 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,25 @@ import { EventProcessor, Integration, Transaction as TransactionType, Transactio
33
import { logger } from '@sentry/utils';
44

55
import { startIdleTransaction } from '../hubextensions';
6-
import { DEFAULT_IDLE_TIMEOUT } from '../idletransaction';
6+
import { DEFAULT_IDLE_TIMEOUT, IdleTransaction } from '../idletransaction';
77
import { Span } from '../span';
8-
8+
import { SpanStatus } from '../spanstatus';
9+
10+
import { registerBackgroundTabDetection } from './backgroundtab';
11+
import { registerErrorInstrumentation } from './errors';
12+
import { MetricsInstrumentation } from './metrics';
13+
import {
14+
defaultRequestInstrumentionOptions,
15+
registerRequestInstrumentation,
16+
RequestInstrumentationOptions,
17+
} from './request';
918
import { defaultBeforeNavigate, defaultRoutingInstrumentation } from './router';
19+
import { secToMs } from './utils';
20+
21+
export const DEFAULT_MAX_TRANSACTION_DURATION_SECONDS = 600;
1022

1123
/** Options for Browser Tracing integration */
12-
export interface BrowserTracingOptions {
24+
export interface BrowserTracingOptions extends RequestInstrumentationOptions {
1325
/**
1426
* The time to wait in ms until the transaction will be finished. The transaction will use the end timestamp of
1527
* the last finished span as the endtime for the transaction.
@@ -50,6 +62,24 @@ export interface BrowserTracingOptions {
5062
startTransactionOnPageLoad?: boolean,
5163
startTransactionOnLocationChange?: boolean,
5264
): void;
65+
66+
/**
67+
* The maximum duration of a transaction before it will be marked as "deadline_exceeded".
68+
* If you never want to mark a transaction set it to 0.
69+
* Time is in seconds.
70+
*
71+
* Default: 600
72+
*/
73+
maxTransactionDuration: number;
74+
75+
/**
76+
* Flag Transactions where tabs moved to background with "cancelled". Browser background tab timing is
77+
* not suited towards doing precise measurements of operations. By default, we recommend that this option
78+
* be enabled as background transactions can mess up your statistics in nondeterministic ways.
79+
*
80+
* Default: true
81+
*/
82+
markBackgroundTransactions: boolean;
5383
}
5484

5585
/**
@@ -69,9 +99,12 @@ export class BrowserTracing implements Integration {
6999
public options: BrowserTracingOptions = {
70100
beforeNavigate: defaultBeforeNavigate,
71101
idleTimeout: DEFAULT_IDLE_TIMEOUT,
102+
markBackgroundTransactions: true,
103+
maxTransactionDuration: DEFAULT_MAX_TRANSACTION_DURATION_SECONDS,
72104
routingInstrumentation: defaultRoutingInstrumentation,
73105
startTransactionOnLocationChange: true,
74106
startTransactionOnPageLoad: true,
107+
...defaultRequestInstrumentionOptions,
75108
};
76109

77110
/**
@@ -81,12 +114,28 @@ export class BrowserTracing implements Integration {
81114

82115
private _getCurrentHub?: () => Hub;
83116

84-
// navigationTransactionInvoker() -> Uses history API NavigationTransaction[]
117+
private readonly _metrics: MetricsInstrumentation = new MetricsInstrumentation();
118+
119+
private readonly _emitOptionsWarning: boolean = false;
85120

86121
public constructor(_options?: Partial<BrowserTracingOptions>) {
122+
let tracingOrigins = defaultRequestInstrumentionOptions.tracingOrigins;
123+
// NOTE: Logger doesn't work in constructors, as it's initialized after integrations instances
124+
if (
125+
_options &&
126+
_options.tracingOrigins &&
127+
Array.isArray(_options.tracingOrigins) &&
128+
_options.tracingOrigins.length !== 0
129+
) {
130+
tracingOrigins = _options.tracingOrigins;
131+
} else {
132+
this._emitOptionsWarning = true;
133+
}
134+
87135
this.options = {
88136
...this.options,
89137
..._options,
138+
tracingOrigins,
90139
};
91140
}
92141

@@ -96,13 +145,40 @@ export class BrowserTracing implements Integration {
96145
public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
97146
this._getCurrentHub = getCurrentHub;
98147

99-
const { routingInstrumentation, startTransactionOnLocationChange, startTransactionOnPageLoad } = this.options;
148+
if (this._emitOptionsWarning) {
149+
logger.warn(
150+
'[Tracing] You need to define `tracingOrigins` in the options. Set an array of urls or patterns to trace.',
151+
);
152+
logger.warn(
153+
`[Tracing] We added a reasonable default for you: ${defaultRequestInstrumentionOptions.tracingOrigins}`,
154+
);
155+
}
156+
157+
const {
158+
routingInstrumentation,
159+
startTransactionOnLocationChange,
160+
startTransactionOnPageLoad,
161+
markBackgroundTransactions,
162+
traceFetch,
163+
traceXHR,
164+
tracingOrigins,
165+
shouldCreateSpanForRequest,
166+
} = this.options;
100167

101168
routingInstrumentation(
102169
(context: TransactionContext) => this._createRouteTransaction(context),
103170
startTransactionOnPageLoad,
104171
startTransactionOnLocationChange,
105172
);
173+
174+
// TODO: Should this be default behaviour?
175+
registerErrorInstrumentation();
176+
177+
if (markBackgroundTransactions) {
178+
registerBackgroundTabDetection();
179+
}
180+
181+
registerRequestInstrumentation({ traceFetch, traceXHR, tracingOrigins, shouldCreateSpanForRequest });
106182
}
107183

108184
/** Create routing idle transaction. */
@@ -112,12 +188,13 @@ export class BrowserTracing implements Integration {
112188
return undefined;
113189
}
114190

115-
const { beforeNavigate, idleTimeout } = this.options;
191+
const { beforeNavigate, idleTimeout, maxTransactionDuration } = this.options;
116192

117193
// if beforeNavigate returns undefined, we should not start a transaction.
118194
const ctx = beforeNavigate({
119195
...context,
120196
...getHeaderContext(),
197+
trimEnd: true,
121198
});
122199

123200
if (ctx === undefined) {
@@ -126,8 +203,14 @@ export class BrowserTracing implements Integration {
126203
}
127204

128205
const hub = this._getCurrentHub();
129-
logger.log(`[Tracing] starting ${ctx.op} idleTransaction on scope with context:`, ctx);
130-
return startIdleTransaction(hub, ctx, idleTimeout, true) as TransactionType;
206+
logger.log(`[Tracing] starting ${ctx.op} idleTransaction on scope`);
207+
const idleTransaction = startIdleTransaction(hub, ctx, idleTimeout, true);
208+
idleTransaction.registerBeforeFinishCallback((transaction, endTimestamp) => {
209+
this._metrics.addPerformanceEntires(transaction);
210+
adjustTransactionDuration(secToMs(maxTransactionDuration), transaction, endTimestamp);
211+
});
212+
213+
return idleTransaction as TransactionType;
131214
}
132215
}
133216

@@ -155,3 +238,13 @@ export function getMetaContent(metaName: string): string | null {
155238
const el = document.querySelector(`meta[name=${metaName}]`);
156239
return el ? el.getAttribute('content') : null;
157240
}
241+
242+
/** Adjusts transaction value based on max transaction duration */
243+
function adjustTransactionDuration(maxDuration: number, transaction: IdleTransaction, endTimestamp: number): void {
244+
const diff = endTimestamp - transaction.startTimestamp;
245+
const isOutdatedTransaction = endTimestamp && (diff > maxDuration || diff < 0);
246+
if (isOutdatedTransaction) {
247+
transaction.setStatus(SpanStatus.DeadlineExceeded);
248+
transaction.setTag('maxTransactionDurationExceeded', 'true');
249+
}
250+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { addInstrumentationHandler, logger } from '@sentry/utils';
2+
3+
import { SpanStatus } from '../spanstatus';
4+
5+
import { getActiveTransaction } from './utils';
6+
7+
/**
8+
* Configures global error listeners
9+
*/
10+
export function registerErrorInstrumentation(): void {
11+
addInstrumentationHandler({
12+
callback: errorCallback,
13+
type: 'error',
14+
});
15+
addInstrumentationHandler({
16+
callback: errorCallback,
17+
type: 'unhandledrejection',
18+
});
19+
}
20+
21+
/**
22+
* If an error or unhandled promise occurs, we mark the active transaction as failed
23+
*/
24+
function errorCallback(): void {
25+
const activeTransaction = getActiveTransaction();
26+
if (activeTransaction) {
27+
logger.log(`[Tracing] Transaction: ${SpanStatus.InternalError} -> Global error occured`);
28+
activeTransaction.setStatus(SpanStatus.InternalError);
29+
}
30+
}

0 commit comments

Comments
 (0)