Skip to content

Commit

Permalink
feat: Add BrowserTracing integration (#2723)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
AbhiPrasad committed Jul 14, 2020
1 parent f0f288d commit 9587853
Show file tree
Hide file tree
Showing 17 changed files with 698 additions and 1,334 deletions.
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';

0 comments on commit 9587853

Please sign in to comment.