diff --git a/MIGRATION.md b/MIGRATION.md index 4c0ea3eddc91..fb51ce14e235 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -10,6 +10,33 @@ npx @sentry/migr8@latest This will let you select which updates to run, and automatically update your code. Make sure to still review all code changes! +## Deprecate `new BrowserTracing()` in favor of `browserTracingIntegration()` + +In v8, you have to use the functional style for the browser tracing integration. This works mostly the same, but some of +the options have changed: + +- `startTransactionOnPageLoad` --> `instrumentPageLoad` +- `startTransactionOnLocationChange` --> `instrumentNavigation` +- `markBackgroundTransactions` --> `markBackgroundSpan` +- `beforeNavigate` --> `beforeStartSpan` + +Finally, instead of `routingInstrumentation`, you have to disable instrumentation via e.g. +`instrumentNavigation: false`, and can then manually emit events like this: + +```js +// Example router event +router.on('routeChange', route => { + Sentry.getClient().emit('startNavigationSpan', { + name: route.name, + op: 'navigation', + }); + + const activeSpan = Sentry.getActiveSpan(); // <-- this will hold the navigation span +}); +``` + +The new `browserTracingIntegration()` will pick these up and create the correct spans. + ## Deprecate using `getClient()` to check if the SDK was initialized In v8, `getClient()` will stop returning `undefined` if `Sentry.init()` was not called. For cases where this may be used diff --git a/packages/angular/README.md b/packages/angular/README.md index 302b060bdb39..62c6edd1ec85 100644 --- a/packages/angular/README.md +++ b/packages/angular/README.md @@ -93,14 +93,13 @@ Registering a Trace Service is a 3-step process. instrumentation: ```javascript -import { init, instrumentAngularRouting, BrowserTracing } from '@sentry/angular'; +import { init, browserTracingIntegration } from '@sentry/angular'; init({ dsn: '__DSN__', integrations: [ - new BrowserTracing({ + browserTracingIntegration({ tracingOrigins: ['localhost', 'https://yourserver.io/api'], - routingInstrumentation: instrumentAngularRouting, }), ], tracesSampleRate: 1, diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts index f7f0536463a2..d1b3ae9b7207 100644 --- a/packages/angular/src/index.ts +++ b/packages/angular/src/index.ts @@ -10,6 +10,7 @@ export { // TODO `instrumentAngularRouting` is just an alias for `routingInstrumentation`; deprecate the latter at some point instrumentAngularRouting, // new name routingInstrumentation, // legacy name + browserTracingIntegration, TraceClassDecorator, TraceMethodDecorator, TraceDirective, diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index efd2c840420b..1d40e27800fc 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -7,9 +7,19 @@ import type { ActivatedRouteSnapshot, Event, RouterState } from '@angular/router // eslint-disable-next-line @typescript-eslint/consistent-type-imports import { NavigationCancel, NavigationError, Router } from '@angular/router'; import { NavigationEnd, NavigationStart, ResolveEnd } from '@angular/router'; -import { WINDOW, getCurrentScope } from '@sentry/browser'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; -import type { Span, Transaction, TransactionContext } from '@sentry/types'; +import { + WINDOW, + browserTracingIntegration as originalBrowserTracingIntegration, + getCurrentScope, +} from '@sentry/browser'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + getActiveSpan, + getClient, + spanToJSON, + startInactiveSpan, +} from '@sentry/core'; +import type { Integration, Span, Transaction, TransactionContext } from '@sentry/types'; import { logger, stripUrlQueryAndFragment, timestampInSeconds } from '@sentry/utils'; import type { Observable } from 'rxjs'; import { Subscription } from 'rxjs'; @@ -23,6 +33,8 @@ let instrumentationInitialized: boolean; let stashedStartTransaction: (context: TransactionContext) => Transaction | undefined; let stashedStartTransactionOnLocationChange: boolean; +let hooksBasedInstrumentation = false; + /** * Creates routing instrumentation for Angular Router. */ @@ -49,6 +61,23 @@ export function routingInstrumentation( export const instrumentAngularRouting = routingInstrumentation; +/** + * A custom BrowserTracing integration for Angular. + */ +export function browserTracingIntegration( + options?: Parameters[0], +): Integration { + instrumentationInitialized = true; + hooksBasedInstrumentation = true; + + return originalBrowserTracingIntegration({ + ...options, + instrumentPageLoad: true, + // We handle this manually + instrumentNavigation: false, + }); +} + /** * Grabs active transaction off scope. * @@ -74,7 +103,44 @@ export class TraceService implements OnDestroy { return; } + if (this._routingSpan) { + this._routingSpan.end(); + this._routingSpan = null; + } + const strippedUrl = stripUrlQueryAndFragment(navigationEvent.url); + + const client = getClient(); + if (hooksBasedInstrumentation && client && client.emit) { + if (!getActiveSpan()) { + client.emit('startNavigationSpan', { + name: strippedUrl, + op: 'navigation', + origin: 'auto.navigation.angular', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, + }); + } + + // eslint-disable-next-line deprecation/deprecation + this._routingSpan = + startInactiveSpan({ + name: `${navigationEvent.url}`, + op: ANGULAR_ROUTING_OP, + origin: 'auto.ui.angular', + tags: { + 'routing.instrumentation': '@sentry/angular', + url: strippedUrl, + ...(navigationEvent.navigationTrigger && { + navigationTrigger: navigationEvent.navigationTrigger, + }), + }, + }) || null; + + return; + } + // eslint-disable-next-line deprecation/deprecation let activeTransaction = getActiveTransaction(); @@ -90,9 +156,6 @@ export class TraceService implements OnDestroy { } if (activeTransaction) { - if (this._routingSpan) { - this._routingSpan.end(); - } // eslint-disable-next-line deprecation/deprecation this._routingSpan = activeTransaction.startChild({ description: `${navigationEvent.url}`, diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index d59d596e0b82..8de4f9d5a346 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -26,6 +26,7 @@ import type { SessionAggregates, Severity, SeverityLevel, + StartSpanOptions, Transaction, TransactionEvent, Transport, @@ -481,6 +482,12 @@ export abstract class BaseClient implements Client { callback: (feedback: FeedbackEvent, options?: { includeReplay: boolean }) => void, ): void; + /** @inheritdoc */ + public on(hook: 'startPageLoadSpan', callback: (options: StartSpanOptions) => void): void; + + /** @inheritdoc */ + public on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): void; + /** @inheritdoc */ public on(hook: string, callback: unknown): void { if (!this._hooks[hook]) { @@ -521,6 +528,12 @@ export abstract class BaseClient implements Client { /** @inheritdoc */ public emit(hook: 'beforeSendFeedback', feedback: FeedbackEvent, options?: { includeReplay: boolean }): void; + /** @inheritdoc */ + public emit(hook: 'startPageLoadSpan', options: StartSpanOptions): void; + + /** @inheritdoc */ + public emit(hook: 'startNavigationSpan', options: StartSpanOptions): void; + /** @inheritdoc */ public emit(hook: string, ...rest: unknown[]): void { if (this._hooks[hook]) { diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts new file mode 100644 index 000000000000..ca097c1db466 --- /dev/null +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -0,0 +1,480 @@ +/* eslint-disable max-lines, complexity */ +import type { IdleTransaction } from '@sentry/core'; +import { defineIntegration, getCurrentHub } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + TRACING_DEFAULTS, + addTracingExtensions, + getActiveTransaction, + spanIsSampled, + spanToJSON, + startIdleTransaction, +} from '@sentry/core'; +import type { + IntegrationFn, + StartSpanOptions, + Transaction, + TransactionContext, + TransactionSource, +} from '@sentry/types'; +import type { Span } from '@sentry/types'; +import { + addHistoryInstrumentationHandler, + browserPerformanceTimeOrigin, + getDomElement, + logger, + tracingContextFromHeaders, +} from '@sentry/utils'; + +import { DEBUG_BUILD } from '../common/debug-build'; +import { registerBackgroundTabDetection } from './backgroundtab'; +import { + addPerformanceEntries, + startTrackingInteractions, + startTrackingLongTasks, + startTrackingWebVitals, +} from './metrics'; +import type { RequestInstrumentationOptions } from './request'; +import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request'; +import { WINDOW } from './types'; + +export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing'; + +/** Options for Browser Tracing integration */ +export interface BrowserTracingOptions extends RequestInstrumentationOptions { + /** + * The time to wait in ms until the transaction will be finished during an idle state. An idle state is defined + * by a moment where there are no in-progress spans. + * + * The transaction will use the end timestamp of the last finished span as the endtime for the transaction. + * If there are still active spans when this the `idleTimeout` is set, the `idleTimeout` will get reset. + * Time is in ms. + * + * Default: 1000 + */ + idleTimeout: number; + + /** + * The max duration for a transaction. If a transaction duration hits the `finalTimeout` value, it + * will be finished. + * Time is in ms. + * + * Default: 30000 + */ + finalTimeout: number; + + /** + * The heartbeat interval. If no new spans are started or open spans are finished within 3 heartbeats, + * the transaction will be finished. + * Time is in ms. + * + * Default: 5000 + */ + heartbeatInterval: number; + + /** + * If a span should be created on page load. + * Default: true + */ + instrumentPageLoad: boolean; + + /** + * If a span should be created on navigation (history change). + * Default: true + */ + instrumentNavigation: boolean; + + /** + * Flag spans 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 + */ + markBackgroundSpan: boolean; + + /** + * If true, Sentry will capture long tasks and add them to the corresponding transaction. + * + * Default: true + */ + enableLongTask: boolean; + + /** + * _metricOptions allows the user to send options to change how metrics are collected. + * + * _metricOptions is currently experimental. + * + * Default: undefined + */ + _metricOptions?: Partial<{ + /** + * @deprecated This property no longer has any effect and will be removed in v8. + */ + _reportAllChanges: boolean; + }>; + + /** + * _experiments allows the user to send options to define how this integration works. + * Note that the `enableLongTask` options is deprecated in favor of the option at the top level, and will be removed in v8. + * + * TODO (v8): Remove enableLongTask + * + * Default: undefined + */ + _experiments: Partial<{ + enableInteractions: boolean; + }>; + + /** + * A callback which is called before a span for a pageload or navigation is started. + * It receives the options passed to `startSpan`, and expects to return an updated options object. + */ + beforeStartSpan?: (options: StartSpanOptions) => StartSpanOptions; +} + +const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { + ...TRACING_DEFAULTS, + instrumentNavigation: true, + instrumentPageLoad: true, + markBackgroundSpan: true, + enableLongTask: true, + _experiments: {}, + ...defaultRequestInstrumentationOptions, +}; + +/** + * 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 const _browserTracingIntegration = ((_options: Partial = {}) => { + const _hasSetTracePropagationTargets = DEBUG_BUILD + ? !!( + // eslint-disable-next-line deprecation/deprecation + (_options.tracePropagationTargets || _options.tracingOrigins) + ) + : false; + + addTracingExtensions(); + + // TODO (v8): remove this block after tracingOrigins is removed + // Set tracePropagationTargets to tracingOrigins if specified by the user + // In case both are specified, tracePropagationTargets takes precedence + // eslint-disable-next-line deprecation/deprecation + if (!_options.tracePropagationTargets && _options.tracingOrigins) { + // eslint-disable-next-line deprecation/deprecation + _options.tracePropagationTargets = _options.tracingOrigins; + } + + const options = { + ...DEFAULT_BROWSER_TRACING_OPTIONS, + ..._options, + }; + + const _collectWebVitals = startTrackingWebVitals(); + + if (options.enableLongTask) { + startTrackingLongTasks(); + } + if (options._experiments.enableInteractions) { + startTrackingInteractions(); + } + + let latestRouteName: string | undefined; + let latestRouteSource: TransactionSource | undefined; + + /** Create routing idle transaction. */ + function _createRouteTransaction(context: TransactionContext): Transaction | undefined { + // eslint-disable-next-line deprecation/deprecation + const hub = getCurrentHub(); + + const { beforeStartSpan, idleTimeout, finalTimeout, heartbeatInterval } = options; + + const isPageloadTransaction = context.op === 'pageload'; + + const sentryTrace = isPageloadTransaction ? getMetaContent('sentry-trace') : ''; + const baggage = isPageloadTransaction ? getMetaContent('baggage') : ''; + const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( + sentryTrace, + baggage, + ); + + const expandedContext: TransactionContext = { + ...context, + ...traceparentData, + metadata: { + // eslint-disable-next-line deprecation/deprecation + ...context.metadata, + dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, + }, + trimEnd: true, + }; + + const finalContext = beforeStartSpan ? beforeStartSpan(expandedContext) : expandedContext; + + // If `beforeStartSpan` set a custom name, record that fact + // eslint-disable-next-line deprecation/deprecation + finalContext.metadata = + finalContext.name !== expandedContext.name + ? // eslint-disable-next-line deprecation/deprecation + { ...finalContext.metadata, source: 'custom' } + : // eslint-disable-next-line deprecation/deprecation + finalContext.metadata; + + latestRouteName = finalContext.name; + + // eslint-disable-next-line deprecation/deprecation + const sourceFromData = context.data && context.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + // eslint-disable-next-line deprecation/deprecation + const sourceFromMetadata = finalContext.metadata && finalContext.metadata.source; + + latestRouteSource = sourceFromData || sourceFromMetadata; + + // eslint-disable-next-line deprecation/deprecation + if (finalContext.sampled === false) { + DEBUG_BUILD && logger.log(`[Tracing] Will not send ${finalContext.op} transaction because of beforeNavigate.`); + } + + DEBUG_BUILD && logger.log(`[Tracing] Starting ${finalContext.op} transaction on scope`); + + const { location } = WINDOW; + + const idleTransaction = startIdleTransaction( + hub, + finalContext, + idleTimeout, + finalTimeout, + true, + { location }, // for use in the tracesSampler + heartbeatInterval, + isPageloadTransaction, // should wait for finish signal if it's a pageload transaction + ); + + if (isPageloadTransaction) { + WINDOW.document.addEventListener('readystatechange', () => { + if (['interactive', 'complete'].includes(WINDOW.document.readyState)) { + idleTransaction.sendAutoFinishSignal(); + } + }); + + if (['interactive', 'complete'].includes(WINDOW.document.readyState)) { + idleTransaction.sendAutoFinishSignal(); + } + } + + // eslint-disable-next-line deprecation/deprecation + const scope = hub.getScope(); + + // If it's a pageload and there is a meta tag set + // use the traceparentData as the propagation context + if (isPageloadTransaction && traceparentData) { + scope.setPropagationContext(propagationContext); + } else { + // Navigation transactions should set a new propagation context based on the + // created idle transaction. + scope.setPropagationContext({ + traceId: idleTransaction.spanContext().traceId, + spanId: idleTransaction.spanContext().spanId, + parentSpanId: spanToJSON(idleTransaction).parent_span_id, + sampled: spanIsSampled(idleTransaction), + }); + } + + idleTransaction.registerBeforeFinishCallback(transaction => { + _collectWebVitals(); + addPerformanceEntries(transaction); + }); + + return idleTransaction as Transaction; + } + + return { + name: BROWSER_TRACING_INTEGRATION_ID, + // eslint-disable-next-line @typescript-eslint/no-empty-function + setupOnce: () => {}, + setup(client) { + const clientOptions = client.getOptions(); + + const { markBackgroundSpan, traceFetch, traceXHR, shouldCreateSpanForRequest, enableHTTPTimings, _experiments } = + options; + + const clientOptionsTracePropagationTargets = clientOptions && clientOptions.tracePropagationTargets; + // There are three ways to configure tracePropagationTargets: + // 1. via top level client option `tracePropagationTargets` + // 2. via BrowserTracing option `tracePropagationTargets` + // 3. via BrowserTracing option `tracingOrigins` (deprecated) + // + // To avoid confusion, favour top level client option `tracePropagationTargets`, and fallback to + // BrowserTracing option `tracePropagationTargets` and then `tracingOrigins` (deprecated). + // This is done as it minimizes bundle size (we don't have to have undefined checks). + // + // If both 1 and either one of 2 or 3 are set (from above), we log out a warning. + // eslint-disable-next-line deprecation/deprecation + const tracePropagationTargets = clientOptionsTracePropagationTargets || options.tracePropagationTargets; + if (DEBUG_BUILD && _hasSetTracePropagationTargets && clientOptionsTracePropagationTargets) { + logger.warn( + '[Tracing] The `tracePropagationTargets` option was set in the BrowserTracing integration and top level `Sentry.init`. The top level `Sentry.init` value is being used.', + ); + } + + let activeSpan: Span | undefined; + let startingUrl: string | undefined = WINDOW.location.href; + + if (client.on) { + client.on('startNavigationSpan', (context: StartSpanOptions) => { + activeSpan = _createRouteTransaction(context); + }); + + client.on('startPageLoadSpan', (context: StartSpanOptions) => { + if (activeSpan) { + DEBUG_BUILD && logger.log(`[Tracing] Finishing current transaction with op: ${spanToJSON(activeSpan).op}`); + // If there's an open transaction on the scope, we need to finish it before creating an new one. + activeSpan.end(); + } + activeSpan = _createRouteTransaction(context); + }); + } + + if (options.instrumentPageLoad && client.emit) { + const context: StartSpanOptions = { + name: WINDOW.location.pathname, + // pageload should always start at timeOrigin (and needs to be in s, not ms) + startTimestamp: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined, + op: 'pageload', + origin: 'auto.pageload.browser', + metadata: { source: 'url' }, + }; + client.emit('startPageLoadSpan', context); + } + + if (options.instrumentNavigation && client.emit) { + addHistoryInstrumentationHandler(({ to, from }) => { + /** + * This early return is there to account for some cases where a navigation transaction starts right after + * long-running pageload. We make sure that if `from` is undefined and a valid `startingURL` exists, we don't + * create an uneccessary navigation transaction. + * + * This was hard to duplicate, but this behavior 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 (activeSpan) { + DEBUG_BUILD && + logger.log(`[Tracing] Finishing current transaction with op: ${spanToJSON(activeSpan).op}`); + // If there's an open transaction on the scope, we need to finish it before creating an new one. + activeSpan.end(); + } + const context: StartSpanOptions = { + name: WINDOW.location.pathname, + op: 'navigation', + origin: 'auto.navigation.browser', + metadata: { source: 'url' }, + }; + + // We know this is fine because we checked above... + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + client.emit!('startNavigationSpan', context); + } + }); + } + + if (markBackgroundSpan) { + registerBackgroundTabDetection(); + } + + if (_experiments.enableInteractions) { + registerInteractionListener(options, latestRouteName, latestRouteSource); + } + + instrumentOutgoingRequests({ + traceFetch, + traceXHR, + tracePropagationTargets, + shouldCreateSpanForRequest, + enableHTTPTimings, + }); + }, + }; +}) as IntegrationFn; + +export const browserTracingIntegration = defineIntegration(_browserTracingIntegration); + +/** Returns the value of a meta tag */ +export function getMetaContent(metaName: string): string | undefined { + // Can't specify generic to `getDomElement` because tracing can be used + // in a variety of environments, have to disable `no-unsafe-member-access` + // as a result. + const metaTag = getDomElement(`meta[name=${metaName}]`); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return metaTag ? metaTag.getAttribute('content') : undefined; +} + +/** Start listener for interaction transactions */ +function registerInteractionListener( + options: BrowserTracingOptions, + latestRouteName: string | undefined, + latestRouteSource: TransactionSource | undefined, +): void { + let inflightInteractionTransaction: IdleTransaction | undefined; + const registerInteractionTransaction = (): void => { + const { idleTimeout, finalTimeout, heartbeatInterval } = options; + const op = 'ui.action.click'; + + // eslint-disable-next-line deprecation/deprecation + const currentTransaction = getActiveTransaction(); + if (currentTransaction && currentTransaction.op && ['navigation', 'pageload'].includes(currentTransaction.op)) { + DEBUG_BUILD && + logger.warn( + `[Tracing] Did not create ${op} transaction because a pageload or navigation transaction is in progress.`, + ); + return undefined; + } + + if (inflightInteractionTransaction) { + inflightInteractionTransaction.setFinishReason('interactionInterrupted'); + inflightInteractionTransaction.end(); + inflightInteractionTransaction = undefined; + } + + if (!latestRouteName) { + DEBUG_BUILD && logger.warn(`[Tracing] Did not create ${op} transaction because _latestRouteName is missing.`); + return undefined; + } + + const { location } = WINDOW; + + const context: TransactionContext = { + name: latestRouteName, + op, + trimEnd: true, + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: latestRouteSource || 'url', + }, + }; + + inflightInteractionTransaction = startIdleTransaction( + // eslint-disable-next-line deprecation/deprecation + getCurrentHub(), + context, + idleTimeout, + finalTimeout, + true, + { location }, // for use in the tracesSampler + heartbeatInterval, + ); + }; + + ['click'].forEach(type => { + addEventListener(type, registerInteractionTransaction, { once: false, capture: true }); + }); +} diff --git a/packages/tracing-internal/src/browser/browsertracing.ts b/packages/tracing-internal/src/browser/browsertracing.ts index e9f61c73c0f3..bfe638ae17ef 100644 --- a/packages/tracing-internal/src/browser/browsertracing.ts +++ b/packages/tracing-internal/src/browser/browsertracing.ts @@ -60,16 +60,38 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions { heartbeatInterval: number; /** - * Flag to enable/disable creation of `navigation` transaction on history changes. + * If a span should be created on location (history) changes. + * Default: true + */ + instrumentNavigation: boolean; + + /** + * If a span should be created on pageload. + * Default: true + */ + instrumentPageLoad: boolean; + + /** + * Flag spans 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 */ startTransactionOnLocationChange: boolean; + /** + * Flag to enable/disable creation of `navigation` transaction on history changes. + * Default: true + * @deprecated Configure `instrumentNavigation` instead. + */ + startTransactionOnLocationChange?: boolean; + /** * Flag to enable/disable creation of `pageload` transaction on first pageload. * * Default: true + * @deprecated Configure `instrumentPageLoad` instead. */ startTransactionOnPageLoad: boolean; @@ -145,19 +167,16 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { ...TRACING_DEFAULTS, markBackgroundTransactions: true, routingInstrumentation: instrumentRoutingWithDefaults, - startTransactionOnLocationChange: true, - startTransactionOnPageLoad: true, + instrumentNavigation: true, + instrumentPageLoad: true, + markBackgroundSpan: true, enableLongTask: true, _experiments: {}, ...defaultRequestInstrumentationOptions, }; /** - * 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. + * @deprecated Use `browserTracingIntegration()` instead. */ export class BrowserTracing implements Integration { // This class currently doesn't have a static `id` field like the other integration classes, because it prevented @@ -196,6 +215,29 @@ export class BrowserTracing implements Integration { ); } + // Migrate legacy options + // TODO v8: Remove this + /* eslint-disable deprecation/deprecation */ + if (typeof _options.startTransactionOnPageLoad === 'boolean') { + _options.instrumentPageLoad = _options.startTransactionOnPageLoad; + } + if (typeof _options.startTransactionOnLocationChange === 'boolean') { + _options.instrumentNavigation = _options.startTransactionOnLocationChange; + } + if (typeof _options.markBackgroundTransactions === 'boolean') { + _options.markBackgroundSpan = _options.markBackgroundTransactions; + } + /* eslint-enable deprecation/deprecation */ + + // TODO (v8): remove this block after tracingOrigins is removed + // Set tracePropagationTargets to tracingOrigins if specified by the user + // In case both are specified, tracePropagationTargets takes precedence + // eslint-disable-next-line deprecation/deprecation + if (!_options.tracePropagationTargets && _options.tracingOrigins) { + // eslint-disable-next-line deprecation/deprecation + _options.tracePropagationTargets = _options.tracingOrigins; + } + this.options = { ...DEFAULT_BROWSER_TRACING_OPTIONS, ..._options, @@ -237,9 +279,9 @@ export class BrowserTracing implements Integration { const { routingInstrumentation: instrumentRouting, - startTransactionOnLocationChange, - startTransactionOnPageLoad, - markBackgroundTransactions, + instrumentNavigation, + instrumentPageLoad, + markBackgroundSpan, traceFetch, traceXHR, shouldCreateSpanForRequest, @@ -275,8 +317,8 @@ export class BrowserTracing implements Integration { return transaction; }, - startTransactionOnPageLoad, - startTransactionOnLocationChange, + instrumentPageLoad, + instrumentNavigation, ); if (markBackgroundTransactions) { diff --git a/packages/tracing-internal/src/browser/index.ts b/packages/tracing-internal/src/browser/index.ts index 5b30bc519404..2e92747620e4 100644 --- a/packages/tracing-internal/src/browser/index.ts +++ b/packages/tracing-internal/src/browser/index.ts @@ -2,7 +2,14 @@ export * from '../exports'; export type { RequestInstrumentationOptions } from './request'; -export { BrowserTracing, BROWSER_TRACING_INTEGRATION_ID } from './browsertracing'; +export { + // eslint-disable-next-line deprecation/deprecation + BrowserTracing, + BROWSER_TRACING_INTEGRATION_ID, +} from './browsertracing'; + +export { browserTracingIntegration } from './browserTracingIntegration'; + export { instrumentOutgoingRequests, defaultRequestInstrumentationOptions } from './request'; export { diff --git a/packages/tracing-internal/test/browser/browsertracing.test.ts b/packages/tracing-internal/test/browser/browsertracing.test.ts index b9830b8d754c..5265888d3302 100644 --- a/packages/tracing-internal/test/browser/browsertracing.test.ts +++ b/packages/tracing-internal/test/browser/browsertracing.test.ts @@ -699,4 +699,58 @@ conditionalTest({ min: 10 })('BrowserTracing', () => { ); }); }); + + describe('options', () => { + // These are important enough to check with a test as incorrect defaults could + // break a lot of users' configurations. + it('is created with default settings', () => { + const browserTracing = createBrowserTracing(); + + expect(browserTracing.options).toEqual({ + enableLongTask: true, + _experiments: {}, + ...TRACING_DEFAULTS, + routingInstrumentation: instrumentRoutingWithDefaults, + spanOnLocationChange: true, + spanOnPageLoad: true, + markBackgroundSpan: true, + ...defaultRequestInstrumentationOptions, + }); + }); + + it('handles legacy `startTransactionOnLocationChange` option', () => { + const integration = new BrowserTracing({ startTransactionOnLocationChange: false }); + expect(integration.options.instrumentNavigation).toBe(false); + }); + + it('handles legacy `startTransactionOnPageLoad` option', () => { + const integration = new BrowserTracing({ startTransactionOnPageLoad: false }); + expect(integration.options.instrumentPageLoad).toBe(false); + }); + + it('handles legacy `markBackgroundTransactions` option', () => { + const integration = new BrowserTracing({ markBackgroundTransactions: false }); + expect(integration.options.markBackgroundSpan).toBe(false); + }); + + it('allows to disable enableLongTask via _experiments', () => { + const browserTracing = createBrowserTracing(false, { + _experiments: { + enableLongTask: false, + }, + }); + + expect(browserTracing.options.enableLongTask).toBe(false); + expect(browserTracing.options._experiments.enableLongTask).toBe(false); + }); + + it('allows to disable enableLongTask', () => { + const browserTracing = createBrowserTracing(false, { + enableLongTask: false, + }); + + expect(browserTracing.options.enableLongTask).toBe(false); + expect(browserTracing.options._experiments.enableLongTask).toBe(undefined); + }); + }); }); diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index d8d09ec1431b..5db008b0ba37 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -15,6 +15,7 @@ import type { Scope } from './scope'; import type { SdkMetadata } from './sdkmetadata'; import type { Session, SessionAggregates } from './session'; import type { Severity, SeverityLevel } from './severity'; +import type { StartSpanOptions } from './startSpanOptions'; import type { Transaction } from './transaction'; import type { Transport, TransportMakeRequestResponse } from './transport'; @@ -272,6 +273,16 @@ export interface Client { callback: (feedback: FeedbackEvent, options?: { includeReplay?: boolean }) => void, ): void; + /** + * A hook for BrowserTracing to trigger a span start for a page load. + */ + on?(hook: 'startPageLoadSpan', callback: (options: StartSpanOptions) => void): void; + + /** + * A hook for BrowserTracing to trigger a span for a navigation. + */ + on?(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): void; + /** * Fire a hook event for transaction start. * Expects to be given a transaction as the second argument. @@ -333,5 +344,15 @@ export interface Client { */ emit?(hook: 'beforeSendFeedback', feedback: FeedbackEvent, options?: { includeReplay?: boolean }): void; + /** + * Emit a hook event for BrowserTracing to trigger a span start for a page load. + */ + emit?(hook: 'startPageLoadSpan', options: StartSpanOptions): void; + + /** + * Emit a hook event for BrowserTracing to trigger a span for a navigation. + */ + emit?(hook: 'startNavigationSpan', options: StartSpanOptions): void; + /* eslint-enable @typescript-eslint/unified-signatures */ }