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 all 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
157 changes: 157 additions & 0 deletions packages/tracing/src/browser/browsertracing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { Hub } from '@sentry/hub';
import { EventProcessor, Integration, Transaction as TransactionType, TransactionContext } from '@sentry/types';
import { logger } from '@sentry/utils';

import { startIdleTransaction } from '../hubextensions';
import { DEFAULT_IDLE_TIMEOUT } from '../idletransaction';
import { Span } from '../span';

import { defaultBeforeNavigate, defaultRoutingInstrumentation } from './router';

/** Options for Browser Tracing integration */
export interface BrowserTracingOptions {
/**
* 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(context: TransactionContext): TransactionContext | undefined;

/**
* Instrumentation that creates routing change transactions. By default creates
* pageload and navigation transactions.
*/
routingInstrumentation<T extends TransactionType>(
startTransaction: (context: TransactionContext) => T | undefined,
startTransactionOnPageLoad?: boolean,
startTransactionOnLocationChange?: boolean,
): void;
}

/**
* 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 options: BrowserTracingOptions = {
beforeNavigate: defaultBeforeNavigate,
idleTimeout: DEFAULT_IDLE_TIMEOUT,
routingInstrumentation: defaultRoutingInstrumentation,
startTransactionOnLocationChange: true,
startTransactionOnPageLoad: true,
};

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

private _getCurrentHub?: () => Hub;

// navigationTransactionInvoker() -> Uses history API NavigationTransaction[]

public constructor(_options?: Partial<BrowserTracingOptions>) {
this.options = {
...this.options,
..._options,
};
}

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

const { routingInstrumentation, startTransactionOnLocationChange, startTransactionOnPageLoad } = this.options;

routingInstrumentation(
(context: TransactionContext) => this._createRouteTransaction(context),
startTransactionOnPageLoad,
startTransactionOnLocationChange,
);
}

/** Create routing idle transaction. */
private _createRouteTransaction(context: TransactionContext): TransactionType | undefined {
if (!this._getCurrentHub) {
logger.warn(`[Tracing] Did not creeate ${context.op} idleTransaction due to invalid _getCurrentHub`);
return undefined;
}

const { beforeNavigate, idleTimeout } = this.options;

// if beforeNavigate returns undefined, we should not start a transaction.
const ctx = beforeNavigate({
...context,
...getHeaderContext(),
});

if (ctx === undefined) {
logger.log(`[Tracing] Did not create ${context.op} idleTransaction due to beforeNavigate`);
return undefined;
}

const hub = this._getCurrentHub();
logger.log(`[Tracing] starting ${ctx.op} idleTransaction on scope with context:`, ctx);
return startIdleTransaction(hub, ctx, idleTimeout, true) as TransactionType;
}
}

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

return {};
}

/** 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;
}
1 change: 1 addition & 0 deletions packages/tracing/src/browser/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { BrowserTracing } from './browsertracing';
61 changes: 61 additions & 0 deletions packages/tracing/src/browser/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Transaction as TransactionType, TransactionContext } from '@sentry/types';
import { addInstrumentationHandler, getGlobalObject, logger } from '@sentry/utils';

// type StartTransaction
const global = getGlobalObject<Window>();

/**
* Creates a default router based on
*/
export function defaultRoutingInstrumentation<T extends TransactionType>(
startTransaction: (context: TransactionContext) => T | undefined,
startTransactionOnPageLoad: boolean = true,
startTransactionOnLocationChange: boolean = true,
): void {
if (!global || !global.location) {
logger.warn('Could not initialize routing instrumentation due to invalid location');
return;
}

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

let activeTransaction: T | undefined;
if (startTransactionOnPageLoad) {
activeTransaction = startTransaction({ name: global.location.pathname, op: 'pageload' });
}

if (startTransactionOnLocationChange) {
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 (from !== to) {
startingUrl = undefined;
if (activeTransaction) {
// We want to finish all current ongoing idle transactions as we
// are navigating to a new page.
activeTransaction.finish();
}
activeTransaction = startTransaction({ name: global.location.pathname, op: 'navigation' });
}
},
type: 'history',
});
}
}

/** default implementation of Browser Tracing before navigate */
export function defaultBeforeNavigate(context: TransactionContext): TransactionContext | undefined {
return context;
}
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
27 changes: 14 additions & 13 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 @@ -45,6 +45,8 @@ export class IdleTransactionSpanRecorder extends SpanRecorder {
}
}

export type BeforeFinishCallback = (transactionSpan: IdleTransaction) => void;

/**
* An IdleTransaction is a transaction that automatically finishes. It does this by tracking child spans as activities.
* You can have multiple IdleTransactions active, but if the `onScope` option is specified, the idle transaction will
Expand All @@ -66,7 +68,7 @@ export class IdleTransaction extends Transaction {
// We should not use heartbeat if we finished a transaction
private _finished: boolean = false;

private _finishCallback?: (transactionSpan: IdleTransaction) => void;
private readonly _beforeFinishCallbacks: BeforeFinishCallback[] = [];

public constructor(
transactionContext: TransactionContext,
Expand Down Expand Up @@ -119,7 +121,7 @@ export class IdleTransaction extends Transaction {
);
this.setStatus(SpanStatus.DeadlineExceeded);
this.setTag('heartbeat', 'failed');
this.finishIdleTransaction(timestampWithMs());
this.finish();
} else {
this._pingHeartbeat();
}
Expand All @@ -135,15 +137,13 @@ export class IdleTransaction extends Transaction {
}, 5000) as any) as number;
}

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

if (this._finishCallback) {
this._finishCallback(this);
for (const callback of this._beforeFinishCallbacks) {
callback(this);
}

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

logger.log('[Tracing] flushing IdleTransaction');
this.finish(endTimestamp);
} else {
logger.log('[Tracing] No active IdleTransaction');
}

return super.finish(endTimestamp);
}

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

setTimeout(() => {
this.finishIdleTransaction(end);
this.finish(end);
}, timeout);
}
}
Expand All @@ -224,8 +225,8 @@ export class IdleTransaction extends Transaction {
* This is exposed because users have no other way of running something before an idle transaction
* finishes.
*/
public beforeFinish(callback: (transactionSpan: IdleTransaction) => void): void {
this._finishCallback = callback;
public registerBeforeFinishCallback(callback: BeforeFinishCallback): void {
this._beforeFinishCallbacks.push(callback);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/tracing/src/index.bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ export { SDK_NAME, SDK_VERSION } from '@sentry/browser';
import { Integrations as BrowserIntegrations } from '@sentry/browser';
import { getGlobalObject } from '@sentry/utils';

import { BrowserTracing } from './browser';
import { addExtensionMethods } from './hubextensions';
import * as ApmIntegrations from './integrations';

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

Expand All @@ -70,7 +70,7 @@ if (_window.Sentry && _window.Sentry.Integrations) {
const INTEGRATIONS = {
...windowIntegrations,
...BrowserIntegrations,
Tracing: ApmIntegrations.Tracing,
...BrowserTracing,
};

export { INTEGRATIONS as Integrations };
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';
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
1 change: 0 additions & 1 deletion packages/tracing/src/integrations/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { Express } from './express';
export { Tracing } from './tracing';