Skip to content

feat: Add span creators to @sentry/tracing package #2736

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

Merged
32 changes: 32 additions & 0 deletions packages/tracing/src/browser/backgroundtab.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { getGlobalObject, logger } from '@sentry/utils';

import { IdleTransaction } from '../idletransaction';
import { SpanStatus } from '../spanstatus';

import { getActiveTransaction } from './utils';

const global = getGlobalObject<Window>();

/**
* Add a listener that cancels and finishes a transaction when the global
* document is hidden.
*/
export function registerBackgroundTabDetection(): void {
if (global && global.document) {
global.document.addEventListener('visibilitychange', () => {
const activeTransaction = getActiveTransaction() as IdleTransaction;
if (global.document.hidden && activeTransaction) {
logger.log(
`[Tracing] Transaction: ${SpanStatus.Cancelled} -> since tab moved to the background, op: ${
activeTransaction.op
}`,
);
activeTransaction.setStatus(SpanStatus.Cancelled);
activeTransaction.setTag('visibilitychange', 'document.hidden');
activeTransaction.finish();
}
});
} else {
logger.warn('[Tracing] Could not set up background tab detection due to lack of global document');
}
}
109 changes: 101 additions & 8 deletions packages/tracing/src/browser/browsertracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,25 @@ import { EventProcessor, Integration, Transaction as TransactionType, Transactio
import { logger } from '@sentry/utils';

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

import { SpanStatus } from '../spanstatus';

import { registerBackgroundTabDetection } from './backgroundtab';
import { registerErrorInstrumentation } from './errors';
import { MetricsInstrumentation } from './metrics';
import {
defaultRequestInstrumentionOptions,
registerRequestInstrumentation,
RequestInstrumentationOptions,
} from './request';
import { defaultBeforeNavigate, defaultRoutingInstrumentation } from './router';
import { secToMs } from './utils';

export const DEFAULT_MAX_TRANSACTION_DURATION_SECONDS = 600;

/** Options for Browser Tracing integration */
export interface BrowserTracingOptions {
export interface BrowserTracingOptions extends RequestInstrumentationOptions {
/**
* 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.
Expand Down Expand Up @@ -50,6 +62,24 @@ export interface BrowserTracingOptions {
startTransactionOnPageLoad?: boolean,
startTransactionOnLocationChange?: boolean,
): void;

/**
* The maximum duration of a transaction before it will be marked as "deadline_exceeded".
* If you never want to mark a transaction set it to 0.
* Time is in seconds.
*
* Default: 600
*/
maxTransactionDuration: number;

/**
* Flag Transactions where tabs moved to background with "cancelled". Browser background tab timing is
* not suited towards doing precise measurements of operations. By default, we recommend that this option
* be enabled as background transactions can mess up your statistics in nondeterministic ways.
*
* Default: true
*/
markBackgroundTransactions: boolean;
}

/**
Expand All @@ -69,9 +99,12 @@ export class BrowserTracing implements Integration {
public options: BrowserTracingOptions = {
beforeNavigate: defaultBeforeNavigate,
idleTimeout: DEFAULT_IDLE_TIMEOUT,
markBackgroundTransactions: true,
maxTransactionDuration: DEFAULT_MAX_TRANSACTION_DURATION_SECONDS,
routingInstrumentation: defaultRoutingInstrumentation,
startTransactionOnLocationChange: true,
startTransactionOnPageLoad: true,
...defaultRequestInstrumentionOptions,
};

/**
Expand All @@ -81,12 +114,28 @@ export class BrowserTracing implements Integration {

private _getCurrentHub?: () => Hub;

// navigationTransactionInvoker() -> Uses history API NavigationTransaction[]
private readonly _metrics: MetricsInstrumentation = new MetricsInstrumentation();

private readonly _emitOptionsWarning: boolean = false;

public constructor(_options?: Partial<BrowserTracingOptions>) {
let tracingOrigins = defaultRequestInstrumentionOptions.tracingOrigins;
// NOTE: Logger doesn't work in constructors, as it's initialized after integrations instances
if (
_options &&
_options.tracingOrigins &&
Array.isArray(_options.tracingOrigins) &&
_options.tracingOrigins.length !== 0
) {
tracingOrigins = _options.tracingOrigins;
} else {
this._emitOptionsWarning = true;
}

this.options = {
...this.options,
..._options,
tracingOrigins,
};
}

Expand All @@ -96,13 +145,40 @@ export class BrowserTracing implements Integration {
public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
this._getCurrentHub = getCurrentHub;

const { routingInstrumentation, startTransactionOnLocationChange, startTransactionOnPageLoad } = this.options;
if (this._emitOptionsWarning) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_emitOptionsWarning is based on tracingOrigins only, which we have access to here. Not sure if it's necessary to store it in the class.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I will refactor the logic here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this is here because we cannot call it in the constructor, and we want to warn the user if they haven't manually set the _option themselves.

logger.warn(
'[Tracing] You need to define `tracingOrigins` in the options. Set an array of urls or patterns to trace.',
);
logger.warn(
`[Tracing] We added a reasonable default for you: ${defaultRequestInstrumentionOptions.tracingOrigins}`,
);
}

const {
routingInstrumentation,
startTransactionOnLocationChange,
startTransactionOnPageLoad,
markBackgroundTransactions,
traceFetch,
traceXHR,
tracingOrigins,
shouldCreateSpanForRequest,
} = this.options;

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

// TODO: Should this be default behaviour?
registerErrorInstrumentation();

if (markBackgroundTransactions) {
registerBackgroundTabDetection();
}

registerRequestInstrumentation({ traceFetch, traceXHR, tracingOrigins, shouldCreateSpanForRequest });
}

/** Create routing idle transaction. */
Expand All @@ -112,12 +188,13 @@ export class BrowserTracing implements Integration {
return undefined;
}

const { beforeNavigate, idleTimeout } = this.options;
const { beforeNavigate, idleTimeout, maxTransactionDuration } = this.options;

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

if (ctx === undefined) {
Expand All @@ -126,8 +203,14 @@ export class BrowserTracing implements Integration {
}

const hub = this._getCurrentHub();
logger.log(`[Tracing] starting ${ctx.op} idleTransaction on scope with context:`, ctx);
return startIdleTransaction(hub, ctx, idleTimeout, true) as TransactionType;
logger.log(`[Tracing] starting ${ctx.op} idleTransaction on scope`);
const idleTransaction = startIdleTransaction(hub, ctx, idleTimeout, true);
idleTransaction.registerBeforeFinishCallback((transaction, endTimestamp) => {
this._metrics.addPerformanceEntires(transaction);
adjustTransactionDuration(secToMs(maxTransactionDuration), transaction, endTimestamp);
});

return idleTransaction as TransactionType;
}
}

Expand Down Expand Up @@ -155,3 +238,13 @@ export function getMetaContent(metaName: string): string | null {
const el = document.querySelector(`meta[name=${metaName}]`);
return el ? el.getAttribute('content') : null;
}

/** Adjusts transaction value based on max transaction duration */
function adjustTransactionDuration(maxDuration: number, transaction: IdleTransaction, endTimestamp: number): void {
const diff = endTimestamp - transaction.startTimestamp;
const isOutdatedTransaction = endTimestamp && (diff > maxDuration || diff < 0);
if (isOutdatedTransaction) {
transaction.setStatus(SpanStatus.DeadlineExceeded);
transaction.setTag('maxTransactionDurationExceeded', 'true');
}
}
30 changes: 30 additions & 0 deletions packages/tracing/src/browser/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { addInstrumentationHandler, logger } from '@sentry/utils';

import { SpanStatus } from '../spanstatus';

import { getActiveTransaction } from './utils';

/**
* Configures global error listeners
*/
export function registerErrorInstrumentation(): void {
addInstrumentationHandler({
callback: errorCallback,
type: 'error',
});
addInstrumentationHandler({
callback: errorCallback,
type: 'unhandledrejection',
});
}

/**
* If an error or unhandled promise occurs, we mark the active transaction as failed
*/
function errorCallback(): void {
const activeTransaction = getActiveTransaction();
if (activeTransaction) {
logger.log(`[Tracing] Transaction: ${SpanStatus.InternalError} -> Global error occured`);
activeTransaction.setStatus(SpanStatus.InternalError);
}
}
Loading