Skip to content

Commit 5717c15

Browse files
committed
feat(tracing): Add BrowserTracing integration and tests
1 parent 467c846 commit 5717c15

File tree

9 files changed

+533
-9
lines changed

9 files changed

+533
-9
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: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import { Hub } from '@sentry/hub';
2+
import { EventProcessor, Integration, Severity, TransactionContext } from '@sentry/types';
3+
import { addInstrumentationHandler, getGlobalObject, logger, safeJoin } from '@sentry/utils';
4+
5+
import { startIdleTransaction } from '../hubextensions';
6+
import { DEFAULT_IDLE_TIMEOUT, IdleTransaction } from '../idletransaction';
7+
import { Span } from '../span';
8+
import { Location as LocationType } from '../types';
9+
10+
const global = getGlobalObject<Window>();
11+
12+
type routingInstrumentationProcessor = (context: TransactionContext) => TransactionContext;
13+
14+
/**
15+
* Gets transaction context from a sentry-trace meta.
16+
*/
17+
const setHeaderContext: routingInstrumentationProcessor = ctx => {
18+
const header = getMetaContent('sentry-trace');
19+
if (header) {
20+
const span = Span.fromTraceparent(header);
21+
if (span) {
22+
return {
23+
...ctx,
24+
parentSpanId: span.parentSpanId,
25+
sampled: span.sampled,
26+
traceId: span.traceId,
27+
};
28+
}
29+
}
30+
31+
return ctx;
32+
};
33+
34+
/** Options for Browser Tracing integration */
35+
export interface BrowserTracingOptions {
36+
/**
37+
* This is only if you want to debug in prod.
38+
* writeAsBreadcrumbs: Instead of having console.log statements we log messages to breadcrumbs
39+
* so you can investigate whats happening in production with your users to figure why things might not appear the
40+
* way you expect them to.
41+
*
42+
* Default: {
43+
* writeAsBreadcrumbs: false;
44+
* }
45+
*/
46+
debug: {
47+
writeAsBreadcrumbs: boolean;
48+
};
49+
50+
/**
51+
* The time to wait in ms until the transaction will be finished. The transaction will use the end timestamp of
52+
* the last finished span as the endtime for the transaction.
53+
* Time is in ms.
54+
*
55+
* Default: 1000
56+
*/
57+
idleTimeout: number;
58+
59+
/**
60+
* Flag to enable/disable creation of `navigation` transaction on history changes.
61+
*
62+
* Default: true
63+
*/
64+
startTransactionOnLocationChange: boolean;
65+
66+
/**
67+
* Flag to enable/disable creation of `pageload` transaction on first pageload.
68+
*
69+
* Default: true
70+
*/
71+
startTransactionOnPageLoad: boolean;
72+
73+
/**
74+
* beforeNavigate is called before a pageload/navigation transaction is created and allows for users
75+
* to set a custom navigation transaction name. Defaults behaviour is to return `window.location.pathname`.
76+
*
77+
* If undefined is returned, a pageload/navigation transaction will not be created.
78+
*/
79+
beforeNavigate(location: LocationType): string | undefined;
80+
81+
/**
82+
* Set to adjust transaction context before creation of transaction. Useful to set name/data/tags before
83+
* a transaction is sent. This option should be used by routing libraries to set context on transactions.
84+
*/
85+
// TODO: Should this be an option, or a static class variable and passed
86+
// in and we use something like `BrowserTracing.addRoutingProcessor()`
87+
routingInstrumentationProcessors: routingInstrumentationProcessor[];
88+
}
89+
90+
/**
91+
* The Browser Tracing integration automatically instruments browser pageload/navigation
92+
* actions as transactions, and captures requests, metrics and errors as spans.
93+
*
94+
* The integration can be configured with a variety of options, and can be extended to use
95+
* any routing library. This integration uses {@see IdleTransaction} to create transactions.
96+
*/
97+
export class BrowserTracing implements Integration {
98+
/**
99+
* @inheritDoc
100+
*/
101+
public static id: string = 'BrowserTracing';
102+
103+
/** Browser Tracing integration options */
104+
public static options: BrowserTracingOptions;
105+
106+
/**
107+
* @inheritDoc
108+
*/
109+
public name: string = BrowserTracing.id;
110+
111+
private static _activeTransaction?: IdleTransaction;
112+
113+
private static _getCurrentHub?: () => Hub;
114+
115+
public constructor(_options?: Partial<BrowserTracingOptions>) {
116+
const defaults: BrowserTracingOptions = {
117+
beforeNavigate(location: LocationType): string | undefined {
118+
return location.pathname;
119+
},
120+
debug: {
121+
writeAsBreadcrumbs: false,
122+
},
123+
idleTimeout: DEFAULT_IDLE_TIMEOUT,
124+
routingInstrumentationProcessors: [],
125+
startTransactionOnLocationChange: true,
126+
startTransactionOnPageLoad: true,
127+
};
128+
BrowserTracing.options = {
129+
...defaults,
130+
..._options,
131+
};
132+
}
133+
134+
/**
135+
* @inheritDoc
136+
*/
137+
public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
138+
BrowserTracing._getCurrentHub = getCurrentHub;
139+
140+
if (!global || !global.location) {
141+
return;
142+
}
143+
144+
// TODO: is it fine that this is mutable operation? Could also do = [...routingInstr, setHeaderContext]?
145+
BrowserTracing.options.routingInstrumentationProcessors.push(setHeaderContext);
146+
BrowserTracing._initRoutingInstrumentation();
147+
}
148+
149+
/** Start routing instrumentation */
150+
private static _initRoutingInstrumentation(): void {
151+
const { startTransactionOnPageLoad, startTransactionOnLocationChange } = BrowserTracing.options;
152+
153+
if (startTransactionOnPageLoad) {
154+
BrowserTracing._activeTransaction = BrowserTracing._createRouteTransaction('pageload');
155+
}
156+
157+
let startingUrl: string | undefined = global.location.href;
158+
159+
addInstrumentationHandler({
160+
callback: ({ to, from }: { to: string; from?: string }) => {
161+
/**
162+
* This early return is there to account for some cases where navigation transaction
163+
* starts right after long running pageload. We make sure that if from is undefined
164+
* and that a valid startingURL exists, we don't uncessarily create a navigation transaction.
165+
*
166+
* This was hard to duplicate, but this behaviour stopped as soon as this fix
167+
* was applied. This issue might also only be caused in certain development environments
168+
* where the usage of a hot module reloader is causing errors.
169+
*/
170+
if (from === undefined && startingUrl && startingUrl.indexOf(to) !== -1) {
171+
startingUrl = undefined;
172+
return;
173+
}
174+
if (startTransactionOnLocationChange && from !== to) {
175+
startingUrl = undefined;
176+
if (BrowserTracing._activeTransaction) {
177+
// We want to finish all current ongoing idle transactions as we
178+
// are navigating to a new page.
179+
BrowserTracing._activeTransaction.finishIdleTransaction();
180+
}
181+
BrowserTracing._activeTransaction = BrowserTracing._createRouteTransaction('navigation');
182+
}
183+
},
184+
type: 'history',
185+
});
186+
}
187+
188+
/** Create pageload/navigation idle transaction. */
189+
private static _createRouteTransaction(op: 'pageload' | 'navigation'): IdleTransaction | undefined {
190+
if (!BrowserTracing._getCurrentHub) {
191+
return undefined;
192+
}
193+
194+
const { beforeNavigate, idleTimeout, routingInstrumentationProcessors } = BrowserTracing.options;
195+
196+
// if beforeNavigate returns undefined, we should not start a transaction.
197+
const name = beforeNavigate(global.location);
198+
if (name === undefined) {
199+
return undefined;
200+
}
201+
202+
let context: TransactionContext = { name, op };
203+
if (routingInstrumentationProcessors) {
204+
for (const processor of routingInstrumentationProcessors) {
205+
context = processor(context);
206+
}
207+
}
208+
209+
const hub = BrowserTracing._getCurrentHub();
210+
BrowserTracing._log(`[Tracing] starting ${op} idleTransaction on scope with context:`, context);
211+
const activeTransaction = startIdleTransaction(hub, context, idleTimeout, true);
212+
213+
return activeTransaction;
214+
}
215+
216+
/**
217+
* Uses logger.log to log things in the SDK or as breadcrumbs if defined in options
218+
*/
219+
private static _log(...args: any[]): void {
220+
if (BrowserTracing.options && BrowserTracing.options.debug && BrowserTracing.options.debug.writeAsBreadcrumbs) {
221+
const _getCurrentHub = BrowserTracing._getCurrentHub;
222+
if (_getCurrentHub) {
223+
_getCurrentHub().addBreadcrumb({
224+
category: 'tracing',
225+
level: Severity.Debug,
226+
message: safeJoin(args, ' '),
227+
type: 'debug',
228+
});
229+
}
230+
}
231+
logger.log(...args);
232+
}
233+
}
234+
235+
/**
236+
* Returns the value of a meta tag
237+
*/
238+
export function getMetaContent(metaName: string): string | null {
239+
const el = document.querySelector(`meta[name=${metaName}]`);
240+
return el ? el.getAttribute('content') : null;
241+
}

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: 2 additions & 2 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
@@ -138,7 +138,7 @@ export class IdleTransaction extends Transaction {
138138
/**
139139
* Finish the current active idle transaction
140140
*/
141-
public finishIdleTransaction(endTimestamp: number): void {
141+
public finishIdleTransaction(endTimestamp: number = timestampWithMs()): void {
142142
if (this.spanRecorder) {
143143
logger.log('[Tracing] finishing IdleTransaction', new Date(endTimestamp * 1000).toISOString(), this.op);
144144

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/browsertracing';
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

packages/tracing/src/integrations/tracing.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ import {
1515
import { Span as SpanClass } from '../span';
1616
import { SpanStatus } from '../spanstatus';
1717
import { Transaction } from '../transaction';
18-
19-
import { Location } from './types';
18+
import { Location } from '../types';
2019

2120
/**
2221
* Options for Tracing integration

0 commit comments

Comments
 (0)