Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add BrowserTracing integration #2723

Merged
merged 7 commits into from
Jul 9, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/tracing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
},
"devDependencies": {
"@types/express": "^4.17.1",
"@types/jsdom": "^16.2.3",
"jest": "^24.7.1",
"jsdom": "^16.2.2",
"npm-run-all": "^4.1.2",
"prettier": "^1.17.0",
"prettier-check": "^2.0.0",
Expand Down
241 changes: 241 additions & 0 deletions packages/tracing/src/browser/browsertracing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import { Hub } from '@sentry/hub';
import { EventProcessor, Integration, Severity, TransactionContext } from '@sentry/types';
import { addInstrumentationHandler, getGlobalObject, logger, safeJoin } from '@sentry/utils';

import { startIdleTransaction } from '../hubextensions';
import { DEFAULT_IDLE_TIMEOUT, IdleTransaction } from '../idletransaction';
import { Span } from '../span';
import { Location as LocationType } from '../types';

const global = getGlobalObject<Window>();

type routingInstrumentationProcessor = (context: TransactionContext) => TransactionContext;

/**
* Gets transaction context from a sentry-trace meta.
*/
const setHeaderContext: routingInstrumentationProcessor = ctx => {
const header = getMetaContent('sentry-trace');
if (header) {
const span = Span.fromTraceparent(header);
if (span) {
return {
...ctx,
parentSpanId: span.parentSpanId,
sampled: span.sampled,
traceId: span.traceId,
};
}
}

return ctx;
};

/** Options for Browser Tracing integration */
export interface BrowserTracingOptions {
/**
* This is only if you want to debug in prod.
* writeAsBreadcrumbs: Instead of having console.log statements we log messages to breadcrumbs
* so you can investigate whats happening in production with your users to figure why things might not appear the
* way you expect them to.
*
* Default: {
* writeAsBreadcrumbs: false;
* }
*/
debug: {
writeAsBreadcrumbs: boolean;
};

/**
* The time to wait in ms until the transaction will be finished. The transaction will use the end timestamp of
* the last finished span as the endtime for the transaction.
* Time is in ms.
*
* Default: 1000
*/
idleTimeout: number;

/**
* Flag to enable/disable creation of `navigation` transaction on history changes.
*
* Default: true
*/
startTransactionOnLocationChange: boolean;

/**
* Flag to enable/disable creation of `pageload` transaction on first pageload.
*
* Default: true
*/
startTransactionOnPageLoad: boolean;

/**
* beforeNavigate is called before a pageload/navigation transaction is created and allows for users
* to set a custom navigation transaction name. Defaults behaviour is to return `window.location.pathname`.
*
* If undefined is returned, a pageload/navigation transaction will not be created.
*/
beforeNavigate(location: LocationType): string | undefined;

/**
* Set to adjust transaction context before creation of transaction. Useful to set name/data/tags before
* a transaction is sent. This option should be used by routing libraries to set context on transactions.
*/
// TODO: Should this be an option, or a static class variable and passed
// in and we use something like `BrowserTracing.addRoutingProcessor()`
routingInstrumentationProcessors: routingInstrumentationProcessor[];
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* The Browser Tracing integration automatically instruments browser pageload/navigation
* actions as transactions, and captures requests, metrics and errors as spans.
*
* The integration can be configured with a variety of options, and can be extended to use
* any routing library. This integration uses {@see IdleTransaction} to create transactions.
*/
export class BrowserTracing implements Integration {
/**
* @inheritDoc
*/
public static id: string = 'BrowserTracing';

/** Browser Tracing integration options */
public static options: BrowserTracingOptions;

/**
* @inheritDoc
*/
public name: string = BrowserTracing.id;

private static _activeTransaction?: IdleTransaction;

private static _getCurrentHub?: () => Hub;

public constructor(_options?: Partial<BrowserTracingOptions>) {
const defaults: BrowserTracingOptions = {
beforeNavigate(location: LocationType): string | undefined {
return location.pathname;
},
debug: {
writeAsBreadcrumbs: false,
},
idleTimeout: DEFAULT_IDLE_TIMEOUT,
routingInstrumentationProcessors: [],
startTransactionOnLocationChange: true,
startTransactionOnPageLoad: true,
};
BrowserTracing.options = {
...defaults,
..._options,
};
}

/**
* @inheritDoc
*/
public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
BrowserTracing._getCurrentHub = getCurrentHub;

if (!global || !global.location) {
return;
}

// TODO: is it fine that this is mutable operation? Could also do = [...routingInstr, setHeaderContext]?
BrowserTracing.options.routingInstrumentationProcessors.push(setHeaderContext);
BrowserTracing._initRoutingInstrumentation();
}

/** Start routing instrumentation */
private static _initRoutingInstrumentation(): void {
const { startTransactionOnPageLoad, startTransactionOnLocationChange } = BrowserTracing.options;

if (startTransactionOnPageLoad) {
BrowserTracing._activeTransaction = BrowserTracing._createRouteTransaction('pageload');
}

let startingUrl: string | undefined = global.location.href;

addInstrumentationHandler({
callback: ({ to, from }: { to: string; from?: string }) => {
/**
* This early return is there to account for some cases where navigation transaction
* starts right after long running pageload. We make sure that if from is undefined
* and that a valid startingURL exists, we don't uncessarily create a navigation transaction.
*
* This was hard to duplicate, but this behaviour stopped as soon as this fix
* was applied. This issue might also only be caused in certain development environments
* where the usage of a hot module reloader is causing errors.
*/
if (from === undefined && startingUrl && startingUrl.indexOf(to) !== -1) {
startingUrl = undefined;
return;
}
if (startTransactionOnLocationChange && from !== to) {
startingUrl = undefined;
if (BrowserTracing._activeTransaction) {
// We want to finish all current ongoing idle transactions as we
// are navigating to a new page.
BrowserTracing._activeTransaction.finishIdleTransaction();
}
BrowserTracing._activeTransaction = BrowserTracing._createRouteTransaction('navigation');
}
},
type: 'history',
});
}

/** Create pageload/navigation idle transaction. */
private static _createRouteTransaction(op: 'pageload' | 'navigation'): IdleTransaction | undefined {
if (!BrowserTracing._getCurrentHub) {
return undefined;
}

const { beforeNavigate, idleTimeout, routingInstrumentationProcessors } = BrowserTracing.options;

// if beforeNavigate returns undefined, we should not start a transaction.
const name = beforeNavigate(global.location);
if (name === undefined) {
return undefined;
}

let context: TransactionContext = { name, op };
if (routingInstrumentationProcessors) {
for (const processor of routingInstrumentationProcessors) {
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
context = processor(context);
}
}

const hub = BrowserTracing._getCurrentHub();
BrowserTracing._log(`[Tracing] starting ${op} idleTransaction on scope with context:`, context);
const activeTransaction = startIdleTransaction(hub, context, idleTimeout, true);

return activeTransaction;
}

/**
* Uses logger.log to log things in the SDK or as breadcrumbs if defined in options
*/
private static _log(...args: any[]): void {
if (BrowserTracing.options && BrowserTracing.options.debug && BrowserTracing.options.debug.writeAsBreadcrumbs) {
const _getCurrentHub = BrowserTracing._getCurrentHub;
if (_getCurrentHub) {
_getCurrentHub().addBreadcrumb({
category: 'tracing',
level: Severity.Debug,
message: safeJoin(args, ' '),
type: 'debug',
});
}
}
logger.log(...args);
}
}

/**
* Returns the value of a meta tag
*/
export function getMetaContent(metaName: string): string | null {
const el = document.querySelector(`meta[name=${metaName}]`);
return el ? el.getAttribute('content') : null;
}
6 changes: 3 additions & 3 deletions packages/tracing/src/hubextensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ function startTransaction(this: Hub, context: TransactionContext): Transaction {
* Create new idle transaction.
*/
export function startIdleTransaction(
this: Hub,
hub: Hub,
context: TransactionContext,
idleTimeout?: number,
onScope?: boolean,
): IdleTransaction {
const transaction = new IdleTransaction(context, this, idleTimeout, onScope);
return sample(this, transaction);
const transaction = new IdleTransaction(context, hub, idleTimeout, onScope);
return sample(hub, transaction);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/tracing/src/idletransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Span } from './span';
import { SpanStatus } from './spanstatus';
import { SpanRecorder, Transaction } from './transaction';

const DEFAULT_IDLE_TIMEOUT = 1000;
export const DEFAULT_IDLE_TIMEOUT = 1000;

/**
* @inheritDoc
Expand Down Expand Up @@ -138,7 +138,7 @@ export class IdleTransaction extends Transaction {
/**
* Finish the current active idle transaction
*/
public finishIdleTransaction(endTimestamp: number): void {
public finishIdleTransaction(endTimestamp: number = timestampWithMs()): void {
if (this.spanRecorder) {
logger.log('[Tracing] finishing IdleTransaction', new Date(endTimestamp * 1000).toISOString(), this.op);

Expand Down
6 changes: 5 additions & 1 deletion packages/tracing/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { BrowserTracing } from './browser/browsertracing';
import { addExtensionMethods } from './hubextensions';
import * as ApmIntegrations from './integrations';

export { ApmIntegrations as Integrations };
// tslint:disable-next-line: variable-name
const Integrations = { ...ApmIntegrations, BrowserTracing };

export { Integrations };
export { Span, TRACEPARENT_REGEXP } from './span';
export { Transaction } from './transaction';

Expand Down
3 changes: 1 addition & 2 deletions packages/tracing/src/integrations/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import {
import { Span as SpanClass } from '../span';
import { SpanStatus } from '../spanstatus';
import { Transaction } from '../transaction';

import { Location } from './types';
import { Location } from '../types';

/**
* Options for Tracing integration
Expand Down