import { TRACING_DEFAULTS, addTracingExtensions, startIdleTransaction, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveTransaction } from '@sentry/core'; import { logger, propagationContextFromHeaders, getDomElement } from '@sentry/utils'; import { DEBUG_BUILD } from '../common/debug-build.js'; import { registerBackgroundTabDetection } from './backgroundtab.js'; import { startTrackingWebVitals, startTrackingLongTasks, startTrackingInteractions, addPerformanceEntries } from './metrics/index.js'; import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request.js'; import { instrumentRoutingWithDefaults } from './router.js'; import { WINDOW } from './types.js'; const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing'; /** Options for Browser Tracing integration */ const DEFAULT_BROWSER_TRACING_OPTIONS = { ...TRACING_DEFAULTS, markBackgroundTransactions: true, routingInstrumentation: instrumentRoutingWithDefaults, startTransactionOnLocationChange: true, startTransactionOnPageLoad: 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. */ class BrowserTracing { // This class currently doesn't have a static `id` field like the other integration classes, because it prevented // @sentry/tracing from being treeshaken. Tree shakers do not like static fields, because they behave like side effects. // TODO: Come up with a better plan, than using static fields on integration classes, and use that plan on all // integrations. /** Browser Tracing integration options */ /** * @inheritDoc */ constructor(_options) { this.name = BROWSER_TRACING_INTEGRATION_ID; this._hasSetTracePropagationTargets = false; addTracingExtensions(); if (DEBUG_BUILD) { this._hasSetTracePropagationTargets = !!( _options && // eslint-disable-next-line deprecation/deprecation (_options.tracePropagationTargets || _options.tracingOrigins) ); } this.options = { ...DEFAULT_BROWSER_TRACING_OPTIONS, ..._options, }; // Special case: enableLongTask can be set in _experiments // TODO (v8): Remove this in v8 if (this.options._experiments.enableLongTask !== undefined) { this.options.enableLongTask = this.options._experiments.enableLongTask; } // 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 && !_options.tracePropagationTargets && _options.tracingOrigins) { // eslint-disable-next-line deprecation/deprecation this.options.tracePropagationTargets = _options.tracingOrigins; } this._collectWebVitals = startTrackingWebVitals(); if (this.options.enableLongTask) { startTrackingLongTasks(); } if (this.options._experiments.enableInteractions) { startTrackingInteractions(); } } /** * @inheritDoc */ setupOnce(_, getCurrentHub) { this._getCurrentHub = getCurrentHub; const hub = getCurrentHub(); // eslint-disable-next-line deprecation/deprecation const client = hub.getClient(); const clientOptions = client && client.getOptions(); const { routingInstrumentation: instrumentRouting, startTransactionOnLocationChange, startTransactionOnPageLoad, markBackgroundTransactions, traceFetch, traceXHR, shouldCreateSpanForRequest, enableHTTPTimings, _experiments, } = this.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 || this.options.tracePropagationTargets; if (DEBUG_BUILD && this._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.', ); } instrumentRouting( (context) => { const transaction = this._createRouteTransaction(context); this.options._experiments.onStartRouteTransaction && this.options._experiments.onStartRouteTransaction(transaction, context, getCurrentHub); return transaction; }, startTransactionOnPageLoad, startTransactionOnLocationChange, ); if (markBackgroundTransactions) { registerBackgroundTabDetection(); } if (_experiments.enableInteractions) { this._registerInteractionListener(); } instrumentOutgoingRequests({ traceFetch, traceXHR, tracePropagationTargets, shouldCreateSpanForRequest, enableHTTPTimings, }); } /** Create routing idle transaction. */ _createRouteTransaction(context) { if (!this._getCurrentHub) { DEBUG_BUILD && logger.warn(`[Tracing] Did not create ${context.op} transaction because _getCurrentHub is invalid.`); return undefined; } const hub = this._getCurrentHub(); const { beforeNavigate, idleTimeout, finalTimeout, heartbeatInterval } = this.options; const isPageloadTransaction = context.op === 'pageload'; let expandedContext; if (isPageloadTransaction) { const sentryTrace = isPageloadTransaction ? getMetaContent('sentry-trace') : ''; const baggage = isPageloadTransaction ? getMetaContent('baggage') : undefined; const { traceId, dsc, parentSpanId, sampled } = propagationContextFromHeaders(sentryTrace, baggage); expandedContext = { traceId, parentSpanId, parentSampled: sampled, ...context, metadata: { // eslint-disable-next-line deprecation/deprecation ...context.metadata, dynamicSamplingContext: dsc, }, trimEnd: true, }; } else { expandedContext = { trimEnd: true, ...context, }; } const modifiedContext = typeof beforeNavigate === 'function' ? beforeNavigate(expandedContext) : expandedContext; // For backwards compatibility reasons, beforeNavigate can return undefined to "drop" the transaction (prevent it // from being sent to Sentry). const finalContext = modifiedContext === undefined ? { ...expandedContext, sampled: false } : modifiedContext; // If `beforeNavigate` 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; this._latestRouteName = finalContext.name; this._latestRouteSource = getSource(finalContext); // 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(); } } idleTransaction.registerBeforeFinishCallback(transaction => { this._collectWebVitals(); addPerformanceEntries(transaction); }); return idleTransaction ; } /** Start listener for interaction transactions */ _registerInteractionListener() { let inflightInteractionTransaction; const registerInteractionTransaction = () => { const { idleTimeout, finalTimeout, heartbeatInterval } = this.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 (!this._getCurrentHub) { DEBUG_BUILD && logger.warn(`[Tracing] Did not create ${op} transaction because _getCurrentHub is invalid.`); return undefined; } if (!this._latestRouteName) { DEBUG_BUILD && logger.warn(`[Tracing] Did not create ${op} transaction because _latestRouteName is missing.`); return undefined; } const hub = this._getCurrentHub(); const { location } = WINDOW; const context = { name: this._latestRouteName, op, trimEnd: true, data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: this._latestRouteSource || 'url', }, }; inflightInteractionTransaction = startIdleTransaction( hub, context, idleTimeout, finalTimeout, true, { location }, // for use in the tracesSampler heartbeatInterval, ); }; ['click'].forEach(type => { addEventListener(type, registerInteractionTransaction, { once: false, capture: true }); }); } } /** Returns the value of a meta tag */ function getMetaContent(metaName) { // 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; } function getSource(context) { const sourceFromAttributes = context.attributes && context.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; // eslint-disable-next-line deprecation/deprecation const sourceFromData = context.data && context.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; // eslint-disable-next-line deprecation/deprecation const sourceFromMetadata = context.metadata && context.metadata.source; return sourceFromAttributes || sourceFromData || sourceFromMetadata; } export { BROWSER_TRACING_INTEGRATION_ID, BrowserTracing, getMetaContent }; //# sourceMappingURL=browsertracing.js.map