Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); const utils = require('@sentry/utils'); const debugBuild = require('../common/debug-build.js'); const backgroundtab = require('./backgroundtab.js'); const index = require('./metrics/index.js'); const request = require('./request.js'); const types = require('./types.js'); const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing'; /** Options for Browser Tracing integration */ const DEFAULT_BROWSER_TRACING_OPTIONS = { ...core.TRACING_DEFAULTS, instrumentNavigation: true, instrumentPageLoad: true, markBackgroundSpan: true, enableLongTask: true, _experiments: {}, ...request.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. * * We explicitly export the proper type here, as this has to be extended in some cases. */ const browserTracingIntegration = ((_options = {}) => { const _hasSetTracePropagationTargets = debugBuild.DEBUG_BUILD ? !!( // eslint-disable-next-line deprecation/deprecation (_options.tracePropagationTargets || _options.tracingOrigins) ) : false; core.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 = index.startTrackingWebVitals(); if (options.enableLongTask) { index.startTrackingLongTasks(); } if (options._experiments.enableInteractions) { index.startTrackingInteractions(); } let latestRouteName; let latestRouteSource; /** Create routing idle transaction. */ function _createRouteTransaction(context) { // eslint-disable-next-line deprecation/deprecation const hub = core.getCurrentHub(); const { beforeStartSpan, idleTimeout, finalTimeout, heartbeatInterval } = 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 } = utils.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 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; latestRouteSource = getSource(finalContext); if (finalContext.sampled === false) { debugBuild.DEBUG_BUILD && utils.logger.log(`[Tracing] Will not send ${finalContext.op} transaction because of beforeNavigate.`); } debugBuild.DEBUG_BUILD && utils.logger.log(`[Tracing] Starting ${finalContext.op} transaction on scope`); const { location } = types.WINDOW; const idleTransaction = core.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) { types.WINDOW.document.addEventListener('readystatechange', () => { if (['interactive', 'complete'].includes(types.WINDOW.document.readyState)) { idleTransaction.sendAutoFinishSignal(); } }); if (['interactive', 'complete'].includes(types.WINDOW.document.readyState)) { idleTransaction.sendAutoFinishSignal(); } } idleTransaction.registerBeforeFinishCallback(transaction => { _collectWebVitals(); index.addPerformanceEntries(transaction); }); return idleTransaction ; } return { name: BROWSER_TRACING_INTEGRATION_ID, // eslint-disable-next-line @typescript-eslint/no-empty-function setupOnce: () => {}, afterAllSetup(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 (debugBuild.DEBUG_BUILD && _hasSetTracePropagationTargets && clientOptionsTracePropagationTargets) { utils.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; let startingUrl = types.WINDOW.location.href; if (client.on) { client.on('startNavigationSpan', (context) => { if (activeSpan) { debugBuild.DEBUG_BUILD && utils.logger.log(`[Tracing] Finishing current transaction with op: ${core.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({ op: 'navigation', ...context, }); }); client.on('startPageLoadSpan', (context) => { if (activeSpan) { debugBuild.DEBUG_BUILD && utils.logger.log(`[Tracing] Finishing current transaction with op: ${core.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({ op: 'pageload', ...context, }); }); } if (options.instrumentPageLoad && client.emit) { const context = { name: types.WINDOW.location.pathname, // pageload should always start at timeOrigin (and needs to be in s, not ms) startTimestamp: utils.browserPerformanceTimeOrigin ? utils.browserPerformanceTimeOrigin / 1000 : undefined, origin: 'auto.pageload.browser', attributes: { [core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, }; startBrowserTracingPageLoadSpan(client, context); } if (options.instrumentNavigation && client.emit) { utils.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; const context = { name: types.WINDOW.location.pathname, origin: 'auto.navigation.browser', attributes: { [core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, }; startBrowserTracingNavigationSpan(client, context); } }); } if (markBackgroundSpan) { backgroundtab.registerBackgroundTabDetection(); } if (_experiments.enableInteractions) { registerInteractionListener(options, latestRouteName, latestRouteSource); } request.instrumentOutgoingRequests({ traceFetch, traceXHR, tracePropagationTargets, shouldCreateSpanForRequest, enableHTTPTimings, }); }, // TODO v8: Remove this again // This is private API that we use to fix converted BrowserTracing integrations in Next.js & SvelteKit options, }; }) ; /** * Manually start a page load span. * This will only do something if the BrowserTracing integration has been setup. */ function startBrowserTracingPageLoadSpan(client, spanOptions) { if (!client.emit) { return; } client.emit('startPageLoadSpan', spanOptions); const span = core.getActiveSpan(); const op = span && core.spanToJSON(span).op; return op === 'pageload' ? span : undefined; } /** * Manually start a navigation span. * This will only do something if the BrowserTracing integration has been setup. */ function startBrowserTracingNavigationSpan(client, spanOptions) { if (!client.emit) { return; } client.emit('startNavigationSpan', spanOptions); const span = core.getActiveSpan(); const op = span && core.spanToJSON(span).op; return op === 'navigation' ? span : undefined; } /** 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 = utils.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, latestRouteName, latestRouteSource, ) { let inflightInteractionTransaction; const registerInteractionTransaction = () => { const { idleTimeout, finalTimeout, heartbeatInterval } = options; const op = 'ui.action.click'; // eslint-disable-next-line deprecation/deprecation const currentTransaction = core.getActiveTransaction(); if (currentTransaction && currentTransaction.op && ['navigation', 'pageload'].includes(currentTransaction.op)) { debugBuild.DEBUG_BUILD && utils.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) { debugBuild.DEBUG_BUILD && utils.logger.warn(`[Tracing] Did not create ${op} transaction because _latestRouteName is missing.`); return undefined; } const { location } = types.WINDOW; const context = { name: latestRouteName, op, trimEnd: true, data: { [core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: latestRouteSource || 'url', }, }; inflightInteractionTransaction = core.startIdleTransaction( // eslint-disable-next-line deprecation/deprecation core.getCurrentHub(), context, idleTimeout, finalTimeout, true, { location }, // for use in the tracesSampler heartbeatInterval, ); }; ['click'].forEach(type => { addEventListener(type, registerInteractionTransaction, { once: false, capture: true }); }); } function getSource(context) { const sourceFromAttributes = context.attributes && context.attributes[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; // eslint-disable-next-line deprecation/deprecation const sourceFromData = context.data && context.data[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; // eslint-disable-next-line deprecation/deprecation const sourceFromMetadata = context.metadata && context.metadata.source; return sourceFromAttributes || sourceFromData || sourceFromMetadata; } exports.BROWSER_TRACING_INTEGRATION_ID = BROWSER_TRACING_INTEGRATION_ID; exports.browserTracingIntegration = browserTracingIntegration; exports.getMetaContent = getMetaContent; exports.startBrowserTracingNavigationSpan = startBrowserTracingNavigationSpan; exports.startBrowserTracingPageLoadSpan = startBrowserTracingPageLoadSpan; //# sourceMappingURL=browserTracingIntegration.js.map