Skip to content

Commit 9587853

Browse files
committed
feat: Add BrowserTracing integration (#2723)
* test: remove hub.startSpan test * feat(tracing): Add BrowserTracing integration and tests * fix: defaultRoutingInstrumentation * ref: Remove static methods * multiple before finishes * ref: Routing Instrumentation * remove tracing
1 parent f0f288d commit 9587853

17 files changed

+698
-1334
lines changed

packages/tracing/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
},
2626
"devDependencies": {
2727
"@types/express": "^4.17.1",
28+
"@types/jsdom": "^16.2.3",
2829
"jest": "^24.7.1",
30+
"jsdom": "^16.2.2",
2931
"npm-run-all": "^4.1.2",
3032
"prettier": "^1.17.0",
3133
"prettier-check": "^2.0.0",
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { Hub } from '@sentry/hub';
2+
import { EventProcessor, Integration, Transaction as TransactionType, TransactionContext } from '@sentry/types';
3+
import { logger } from '@sentry/utils';
4+
5+
import { startIdleTransaction } from '../hubextensions';
6+
import { DEFAULT_IDLE_TIMEOUT } from '../idletransaction';
7+
import { Span } from '../span';
8+
9+
import { defaultBeforeNavigate, defaultRoutingInstrumentation } from './router';
10+
11+
/** Options for Browser Tracing integration */
12+
export interface BrowserTracingOptions {
13+
/**
14+
* The time to wait in ms until the transaction will be finished. The transaction will use the end timestamp of
15+
* the last finished span as the endtime for the transaction.
16+
* Time is in ms.
17+
*
18+
* Default: 1000
19+
*/
20+
idleTimeout: number;
21+
22+
/**
23+
* Flag to enable/disable creation of `navigation` transaction on history changes.
24+
*
25+
* Default: true
26+
*/
27+
startTransactionOnLocationChange: boolean;
28+
29+
/**
30+
* Flag to enable/disable creation of `pageload` transaction on first pageload.
31+
*
32+
* Default: true
33+
*/
34+
startTransactionOnPageLoad: boolean;
35+
36+
/**
37+
* beforeNavigate is called before a pageload/navigation transaction is created and allows for users
38+
* to set a custom navigation transaction name. Defaults behaviour is to return `window.location.pathname`.
39+
*
40+
* If undefined is returned, a pageload/navigation transaction will not be created.
41+
*/
42+
beforeNavigate(context: TransactionContext): TransactionContext | undefined;
43+
44+
/**
45+
* Instrumentation that creates routing change transactions. By default creates
46+
* pageload and navigation transactions.
47+
*/
48+
routingInstrumentation<T extends TransactionType>(
49+
startTransaction: (context: TransactionContext) => T | undefined,
50+
startTransactionOnPageLoad?: boolean,
51+
startTransactionOnLocationChange?: boolean,
52+
): void;
53+
}
54+
55+
/**
56+
* The Browser Tracing integration automatically instruments browser pageload/navigation
57+
* actions as transactions, and captures requests, metrics and errors as spans.
58+
*
59+
* The integration can be configured with a variety of options, and can be extended to use
60+
* any routing library. This integration uses {@see IdleTransaction} to create transactions.
61+
*/
62+
export class BrowserTracing implements Integration {
63+
/**
64+
* @inheritDoc
65+
*/
66+
public static id: string = 'BrowserTracing';
67+
68+
/** Browser Tracing integration options */
69+
public options: BrowserTracingOptions = {
70+
beforeNavigate: defaultBeforeNavigate,
71+
idleTimeout: DEFAULT_IDLE_TIMEOUT,
72+
routingInstrumentation: defaultRoutingInstrumentation,
73+
startTransactionOnLocationChange: true,
74+
startTransactionOnPageLoad: true,
75+
};
76+
77+
/**
78+
* @inheritDoc
79+
*/
80+
public name: string = BrowserTracing.id;
81+
82+
private _getCurrentHub?: () => Hub;
83+
84+
// navigationTransactionInvoker() -> Uses history API NavigationTransaction[]
85+
86+
public constructor(_options?: Partial<BrowserTracingOptions>) {
87+
this.options = {
88+
...this.options,
89+
..._options,
90+
};
91+
}
92+
93+
/**
94+
* @inheritDoc
95+
*/
96+
public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
97+
this._getCurrentHub = getCurrentHub;
98+
99+
const { routingInstrumentation, startTransactionOnLocationChange, startTransactionOnPageLoad } = this.options;
100+
101+
routingInstrumentation(
102+
(context: TransactionContext) => this._createRouteTransaction(context),
103+
startTransactionOnPageLoad,
104+
startTransactionOnLocationChange,
105+
);
106+
}
107+
108+
/** Create routing idle transaction. */
109+
private _createRouteTransaction(context: TransactionContext): TransactionType | undefined {
110+
if (!this._getCurrentHub) {
111+
logger.warn(`[Tracing] Did not creeate ${context.op} idleTransaction due to invalid _getCurrentHub`);
112+
return undefined;
113+
}
114+
115+
const { beforeNavigate, idleTimeout } = this.options;
116+
117+
// if beforeNavigate returns undefined, we should not start a transaction.
118+
const ctx = beforeNavigate({
119+
...context,
120+
...getHeaderContext(),
121+
});
122+
123+
if (ctx === undefined) {
124+
logger.log(`[Tracing] Did not create ${context.op} idleTransaction due to beforeNavigate`);
125+
return undefined;
126+
}
127+
128+
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;
131+
}
132+
}
133+
134+
/**
135+
* Gets transaction context from a sentry-trace meta.
136+
*/
137+
function getHeaderContext(): Partial<TransactionContext> {
138+
const header = getMetaContent('sentry-trace');
139+
if (header) {
140+
const span = Span.fromTraceparent(header);
141+
if (span) {
142+
return {
143+
parentSpanId: span.parentSpanId,
144+
sampled: span.sampled,
145+
traceId: span.traceId,
146+
};
147+
}
148+
}
149+
150+
return {};
151+
}
152+
153+
/** Returns the value of a meta tag */
154+
export function getMetaContent(metaName: string): string | null {
155+
const el = document.querySelector(`meta[name=${metaName}]`);
156+
return el ? el.getAttribute('content') : null;
157+
}

packages/tracing/src/browser/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { BrowserTracing } from './browsertracing';
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Transaction as TransactionType, TransactionContext } from '@sentry/types';
2+
import { addInstrumentationHandler, getGlobalObject, logger } from '@sentry/utils';
3+
4+
// type StartTransaction
5+
const global = getGlobalObject<Window>();
6+
7+
/**
8+
* Creates a default router based on
9+
*/
10+
export function defaultRoutingInstrumentation<T extends TransactionType>(
11+
startTransaction: (context: TransactionContext) => T | undefined,
12+
startTransactionOnPageLoad: boolean = true,
13+
startTransactionOnLocationChange: boolean = true,
14+
): void {
15+
if (!global || !global.location) {
16+
logger.warn('Could not initialize routing instrumentation due to invalid location');
17+
return;
18+
}
19+
20+
let startingUrl: string | undefined = global.location.href;
21+
22+
let activeTransaction: T | undefined;
23+
if (startTransactionOnPageLoad) {
24+
activeTransaction = startTransaction({ name: global.location.pathname, op: 'pageload' });
25+
}
26+
27+
if (startTransactionOnLocationChange) {
28+
addInstrumentationHandler({
29+
callback: ({ to, from }: { to: string; from?: string }) => {
30+
/**
31+
* This early return is there to account for some cases where navigation transaction
32+
* starts right after long running pageload. We make sure that if `from` is undefined
33+
* and that a valid `startingURL` exists, we don't uncessarily create a navigation transaction.
34+
*
35+
* This was hard to duplicate, but this behaviour stopped as soon as this fix
36+
* was applied. This issue might also only be caused in certain development environments
37+
* where the usage of a hot module reloader is causing errors.
38+
*/
39+
if (from === undefined && startingUrl && startingUrl.indexOf(to) !== -1) {
40+
startingUrl = undefined;
41+
return;
42+
}
43+
if (from !== to) {
44+
startingUrl = undefined;
45+
if (activeTransaction) {
46+
// We want to finish all current ongoing idle transactions as we
47+
// are navigating to a new page.
48+
activeTransaction.finish();
49+
}
50+
activeTransaction = startTransaction({ name: global.location.pathname, op: 'navigation' });
51+
}
52+
},
53+
type: 'history',
54+
});
55+
}
56+
}
57+
58+
/** default implementation of Browser Tracing before navigate */
59+
export function defaultBeforeNavigate(context: TransactionContext): TransactionContext | undefined {
60+
return context;
61+
}

packages/tracing/src/hubextensions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,13 @@ function startTransaction(this: Hub, context: TransactionContext): Transaction {
5353
* Create new idle transaction.
5454
*/
5555
export function startIdleTransaction(
56-
this: Hub,
56+
hub: Hub,
5757
context: TransactionContext,
5858
idleTimeout?: number,
5959
onScope?: boolean,
6060
): IdleTransaction {
61-
const transaction = new IdleTransaction(context, this, idleTimeout, onScope);
62-
return sample(this, transaction);
61+
const transaction = new IdleTransaction(context, hub, idleTimeout, onScope);
62+
return sample(hub, transaction);
6363
}
6464

6565
/**

packages/tracing/src/idletransaction.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Span } from './span';
77
import { SpanStatus } from './spanstatus';
88
import { SpanRecorder, Transaction } from './transaction';
99

10-
const DEFAULT_IDLE_TIMEOUT = 1000;
10+
export const DEFAULT_IDLE_TIMEOUT = 1000;
1111

1212
/**
1313
* @inheritDoc
@@ -45,6 +45,8 @@ export class IdleTransactionSpanRecorder extends SpanRecorder {
4545
}
4646
}
4747

48+
export type BeforeFinishCallback = (transactionSpan: IdleTransaction) => void;
49+
4850
/**
4951
* An IdleTransaction is a transaction that automatically finishes. It does this by tracking child spans as activities.
5052
* You can have multiple IdleTransactions active, but if the `onScope` option is specified, the idle transaction will
@@ -66,7 +68,7 @@ export class IdleTransaction extends Transaction {
6668
// We should not use heartbeat if we finished a transaction
6769
private _finished: boolean = false;
6870

69-
private _finishCallback?: (transactionSpan: IdleTransaction) => void;
71+
private readonly _beforeFinishCallbacks: BeforeFinishCallback[] = [];
7072

7173
public constructor(
7274
transactionContext: TransactionContext,
@@ -119,7 +121,7 @@ export class IdleTransaction extends Transaction {
119121
);
120122
this.setStatus(SpanStatus.DeadlineExceeded);
121123
this.setTag('heartbeat', 'failed');
122-
this.finishIdleTransaction(timestampWithMs());
124+
this.finish();
123125
} else {
124126
this._pingHeartbeat();
125127
}
@@ -135,15 +137,13 @@ export class IdleTransaction extends Transaction {
135137
}, 5000) as any) as number;
136138
}
137139

138-
/**
139-
* Finish the current active idle transaction
140-
*/
141-
public finishIdleTransaction(endTimestamp: number): void {
140+
/** {@inheritDoc} */
141+
public finish(endTimestamp: number = timestampWithMs()): string | undefined {
142142
if (this.spanRecorder) {
143143
logger.log('[Tracing] finishing IdleTransaction', new Date(endTimestamp * 1000).toISOString(), this.op);
144144

145-
if (this._finishCallback) {
146-
this._finishCallback(this);
145+
for (const callback of this._beforeFinishCallbacks) {
146+
callback(this);
147147
}
148148

149149
this.spanRecorder.spans = this.spanRecorder.spans.filter((span: Span) => {
@@ -177,10 +177,11 @@ export class IdleTransaction extends Transaction {
177177
}
178178

179179
logger.log('[Tracing] flushing IdleTransaction');
180-
this.finish(endTimestamp);
181180
} else {
182181
logger.log('[Tracing] No active IdleTransaction');
183182
}
183+
184+
return super.finish(endTimestamp);
184185
}
185186

186187
/**
@@ -212,7 +213,7 @@ export class IdleTransaction extends Transaction {
212213
const end = timestampWithMs() + timeout / 1000;
213214

214215
setTimeout(() => {
215-
this.finishIdleTransaction(end);
216+
this.finish(end);
216217
}, timeout);
217218
}
218219
}
@@ -224,8 +225,8 @@ export class IdleTransaction extends Transaction {
224225
* This is exposed because users have no other way of running something before an idle transaction
225226
* finishes.
226227
*/
227-
public beforeFinish(callback: (transactionSpan: IdleTransaction) => void): void {
228-
this._finishCallback = callback;
228+
public registerBeforeFinishCallback(callback: BeforeFinishCallback): void {
229+
this._beforeFinishCallbacks.push(callback);
229230
}
230231

231232
/**

packages/tracing/src/index.bundle.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ export { SDK_NAME, SDK_VERSION } from '@sentry/browser';
5252
import { Integrations as BrowserIntegrations } from '@sentry/browser';
5353
import { getGlobalObject } from '@sentry/utils';
5454

55+
import { BrowserTracing } from './browser';
5556
import { addExtensionMethods } from './hubextensions';
56-
import * as ApmIntegrations from './integrations';
5757

5858
export { Span, TRACEPARENT_REGEXP } from './span';
5959

@@ -70,7 +70,7 @@ if (_window.Sentry && _window.Sentry.Integrations) {
7070
const INTEGRATIONS = {
7171
...windowIntegrations,
7272
...BrowserIntegrations,
73-
Tracing: ApmIntegrations.Tracing,
73+
...BrowserTracing,
7474
};
7575

7676
export { INTEGRATIONS as Integrations };

packages/tracing/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import { BrowserTracing } from './browser';
12
import { addExtensionMethods } from './hubextensions';
23
import * as ApmIntegrations from './integrations';
34

4-
export { ApmIntegrations as Integrations };
5+
// tslint:disable-next-line: variable-name
6+
const Integrations = { ...ApmIntegrations, BrowserTracing };
7+
8+
export { Integrations };
59
export { Span, TRACEPARENT_REGEXP } from './span';
610
export { Transaction } from './transaction';
711

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
export { Express } from './express';
2-
export { Tracing } from './tracing';

0 commit comments

Comments
 (0)