Skip to content

Commit c8612c1

Browse files
committed
ref: Routing Instrumentation
1 parent 27f9d0a commit c8612c1

File tree

8 files changed

+324
-355
lines changed

8 files changed

+324
-355
lines changed

packages/tracing/src/browser/browsertracing.ts

Lines changed: 47 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,15 @@
11
import { Hub } from '@sentry/hub';
2-
import { EventProcessor, Integration, Severity, TransactionContext } from '@sentry/types';
3-
import { addInstrumentationHandler, getGlobalObject, logger, safeJoin } from '@sentry/utils';
2+
import { EventProcessor, Integration, Transaction as TransactionType, TransactionContext } from '@sentry/types';
3+
import { logger } from '@sentry/utils';
44

55
import { startIdleTransaction } from '../hubextensions';
6-
import { DEFAULT_IDLE_TIMEOUT, IdleTransaction } from '../idletransaction';
6+
import { DEFAULT_IDLE_TIMEOUT } from '../idletransaction';
77
import { Span } from '../span';
8-
import { Location as LocationType } from '../types';
98

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-
};
9+
import { defaultRoutingInstrumentation, defaultBeforeNavigate } from './router';
3310

3411
/** Options for Browser Tracing integration */
3512
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-
5013
/**
5114
* The time to wait in ms until the transaction will be finished. The transaction will use the end timestamp of
5215
* the last finished span as the endtime for the transaction.
@@ -76,15 +39,17 @@ export interface BrowserTracingOptions {
7639
*
7740
* If undefined is returned, a pageload/navigation transaction will not be created.
7841
*/
79-
beforeNavigate(location: LocationType): string | undefined;
42+
beforeNavigate(context: TransactionContext): TransactionContext | undefined;
8043

8144
/**
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.
45+
* Instrumentation that creates routing change transactions. By default creates
46+
* pageload and navigation transactions.
8447
*/
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[];
48+
routingInstrumentation<T extends TransactionType>(
49+
startTransaction: (context: TransactionContext) => T | undefined,
50+
startTransactionOnPageLoad?: boolean,
51+
startTransactionOnLocationChange?: boolean,
52+
): void;
8853
}
8954

9055
/**
@@ -102,14 +67,9 @@ export class BrowserTracing implements Integration {
10267

10368
/** Browser Tracing integration options */
10469
public options: BrowserTracingOptions = {
105-
beforeNavigate(location: LocationType): string | undefined {
106-
return location.pathname;
107-
},
108-
debug: {
109-
writeAsBreadcrumbs: false,
110-
},
70+
beforeNavigate: defaultBeforeNavigate,
11171
idleTimeout: DEFAULT_IDLE_TIMEOUT,
112-
routingInstrumentationProcessors: [],
72+
routingInstrumentation: defaultRoutingInstrumentation,
11373
startTransactionOnLocationChange: true,
11474
startTransactionOnPageLoad: true,
11575
};
@@ -119,8 +79,6 @@ export class BrowserTracing implements Integration {
11979
*/
12080
public name: string = BrowserTracing.id;
12181

122-
private _activeTransaction?: IdleTransaction;
123-
12482
private _getCurrentHub?: () => Hub;
12583

12684
// navigationTransactionInvoker() -> Uses history API NavigationTransaction[]
@@ -138,116 +96,58 @@ export class BrowserTracing implements Integration {
13896
public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
13997
this._getCurrentHub = getCurrentHub;
14098

141-
if (!global || !global.location) {
142-
return;
143-
}
99+
const { routingInstrumentation, startTransactionOnLocationChange, startTransactionOnPageLoad } = this.options;
144100

145-
this._initRoutingInstrumentation();
101+
routingInstrumentation(
102+
(context: TransactionContext) => this._createRouteTransaction(context),
103+
startTransactionOnPageLoad,
104+
startTransactionOnLocationChange,
105+
);
146106
}
147107

148-
/** Start routing instrumentation */
149-
private _initRoutingInstrumentation(): void {
150-
const { startTransactionOnPageLoad, startTransactionOnLocationChange } = this.options;
151-
152-
// TODO: is it fine that this is mutable operation? Could also do = [...routingInstr, setHeaderContext]?
153-
this.options.routingInstrumentationProcessors.push(setHeaderContext);
154-
155-
if (startTransactionOnPageLoad) {
156-
this._activeTransaction = this._createRouteTransaction('pageload');
157-
}
158-
159-
let startingUrl: string | undefined = global.location.href;
160-
161-
// Could this be the one that changes?
162-
addInstrumentationHandler({
163-
callback: ({ to, from }: { to: string; from?: string }) => {
164-
/**
165-
* This early return is there to account for some cases where navigation transaction
166-
* starts right after long running pageload. We make sure that if `from` is undefined
167-
* and that a valid `startingURL` exists, we don't uncessarily create a navigation transaction.
168-
*
169-
* This was hard to duplicate, but this behaviour stopped as soon as this fix
170-
* was applied. This issue might also only be caused in certain development environments
171-
* where the usage of a hot module reloader is causing errors.
172-
*/
173-
if (from === undefined && startingUrl && startingUrl.indexOf(to) !== -1) {
174-
startingUrl = undefined;
175-
return;
176-
}
177-
if (startTransactionOnLocationChange && from !== to) {
178-
startingUrl = undefined;
179-
if (this._activeTransaction) {
180-
// We want to finish all current ongoing idle transactions as we
181-
// are navigating to a new page.
182-
this._activeTransaction.finishIdleTransaction();
183-
}
184-
this._activeTransaction = this._createRouteTransaction('navigation');
185-
}
186-
},
187-
type: 'history',
188-
});
189-
}
190-
191-
/** Create pageload/navigation idle transaction. */
192-
private _createRouteTransaction(
193-
op: 'pageload' | 'navigation',
194-
context?: TransactionContext,
195-
): IdleTransaction | undefined {
108+
/** Create routing idle transaction. */
109+
private _createRouteTransaction(context: TransactionContext): TransactionType | undefined {
196110
if (!this._getCurrentHub) {
111+
logger.warn(`[Tracing] Did not creeate ${context.op} idleTransaction due to invalid _getCurrentHub`);
197112
return undefined;
198113
}
199114

200-
const { beforeNavigate, idleTimeout, routingInstrumentationProcessors } = this.options;
115+
const { beforeNavigate, idleTimeout } = this.options;
201116

202117
// if beforeNavigate returns undefined, we should not start a transaction.
203-
const name = beforeNavigate(global.location);
204-
if (name === undefined) {
205-
this._log(`[Tracing] Cancelling ${op} idleTransaction due to beforeNavigate:`);
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`);
206125
return undefined;
207126
}
208127

209-
const ctx = createContextFromProcessors({ name, op, ...context }, routingInstrumentationProcessors);
210-
211128
const hub = this._getCurrentHub();
212-
this._log(`[Tracing] starting ${op} idleTransaction on scope with context:`, ctx);
213-
const activeTransaction = startIdleTransaction(hub, ctx, idleTimeout, true);
214-
215-
return activeTransaction;
216-
}
217-
218-
/**
219-
* Uses logger.log to log things in the SDK or as breadcrumbs if defined in options
220-
*/
221-
private _log(...args: any[]): void {
222-
if (this.options && this.options.debug && this.options.debug.writeAsBreadcrumbs) {
223-
const _getCurrentHub = this._getCurrentHub;
224-
if (_getCurrentHub) {
225-
_getCurrentHub().addBreadcrumb({
226-
category: 'tracing',
227-
level: Severity.Debug,
228-
message: safeJoin(args, ' '),
229-
type: 'debug',
230-
});
231-
}
232-
}
233-
logger.log(...args);
129+
logger.log(`[Tracing] starting ${ctx.op} idleTransaction on scope with context:`, ctx);
130+
return startIdleTransaction(hub, ctx, idleTimeout, true) as TransactionType;
234131
}
235132
}
236133

237-
/** Creates transaction context from a set of processors */
238-
export function createContextFromProcessors(
239-
context: TransactionContext,
240-
processors: routingInstrumentationProcessor[],
241-
): TransactionContext {
242-
let ctx = context;
243-
for (const processor of processors) {
244-
const newContext = processor(context);
245-
if (newContext && newContext.name && newContext.op) {
246-
ctx = newContext;
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+
};
247147
}
248148
}
249149

250-
return ctx;
150+
return {};
251151
}
252152

253153
/** Returns the value of a meta tag */
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/idletransaction.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export class IdleTransaction extends Transaction {
121121
);
122122
this.setStatus(SpanStatus.DeadlineExceeded);
123123
this.setTag('heartbeat', 'failed');
124-
this.finishIdleTransaction(timestampWithMs());
124+
this.finish();
125125
} else {
126126
this._pingHeartbeat();
127127
}
@@ -137,10 +137,8 @@ export class IdleTransaction extends Transaction {
137137
}, 5000) as any) as number;
138138
}
139139

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

@@ -179,10 +177,11 @@ export class IdleTransaction extends Transaction {
179177
}
180178

181179
logger.log('[Tracing] flushing IdleTransaction');
182-
this.finish(endTimestamp);
183180
} else {
184181
logger.log('[Tracing] No active IdleTransaction');
185182
}
183+
184+
return super.finish(endTimestamp);
186185
}
187186

188187
/**
@@ -214,7 +213,7 @@ export class IdleTransaction extends Transaction {
214213
const end = timestampWithMs() + timeout / 1000;
215214

216215
setTimeout(() => {
217-
this.finishIdleTransaction(end);
216+
this.finish(end);
218217
}, timeout);
219218
}
220219
}

0 commit comments

Comments
 (0)