UNPKG

@sentry/browser

Version:
422 lines (357 loc) 14.5 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const browserUtils = require('@sentry-internal/browser-utils'); const core = require('@sentry/core'); const debugBuild = require('../debug-build.js'); const helpers = require('../helpers.js'); const backgroundtab = require('./backgroundtab.js'); const request = require('./request.js'); const previousTrace = require('./previousTrace.js'); /* eslint-disable max-lines */ const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing'; const DEFAULT_BROWSER_TRACING_OPTIONS = { ...core.TRACING_DEFAULTS, instrumentNavigation: true, instrumentPageLoad: true, markBackgroundSpan: true, enableLongTask: true, enableLongAnimationFrame: true, enableInp: true, linkPreviousTrace: 'in-memory', _experiments: {}, ...request.defaultRequestInstrumentationOptions, }; let _hasBeenInitialized = false; /** * 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. * * We explicitly export the proper type here, as this has to be extended in some cases. */ const browserTracingIntegration = ((_options = {}) => { if (_hasBeenInitialized) { core.consoleSandbox(() => { // eslint-disable-next-line no-console console.warn('Multiple browserTracingIntegration instances are not supported.'); }); } _hasBeenInitialized = true; /** * This is just a small wrapper that makes `document` optional. * We want to be extra-safe and always check that this exists, to ensure weird environments do not blow up. */ const optionalWindowDocument = helpers.WINDOW.document ; core.registerSpanErrorInstrumentation(); const { enableInp, enableLongTask, enableLongAnimationFrame, _experiments: { enableInteractions, enableStandaloneClsSpans }, beforeStartSpan, idleTimeout, finalTimeout, childSpanTimeout, markBackgroundSpan, traceFetch, traceXHR, trackFetchStreamPerformance, shouldCreateSpanForRequest, enableHTTPTimings, instrumentPageLoad, instrumentNavigation, linkPreviousTrace, onRequestSpanStart, } = { ...DEFAULT_BROWSER_TRACING_OPTIONS, ..._options, }; const _collectWebVitals = browserUtils.startTrackingWebVitals({ recordClsStandaloneSpans: enableStandaloneClsSpans || false }); if (enableInp) { browserUtils.startTrackingINP(); } if ( enableLongAnimationFrame && core.GLOBAL_OBJ.PerformanceObserver && PerformanceObserver.supportedEntryTypes && PerformanceObserver.supportedEntryTypes.includes('long-animation-frame') ) { browserUtils.startTrackingLongAnimationFrames(); } else if (enableLongTask) { browserUtils.startTrackingLongTasks(); } if (enableInteractions) { browserUtils.startTrackingInteractions(); } const latestRoute = { name: undefined, source: undefined, }; /** Create routing idle transaction. */ function _createRouteSpan(client, startSpanOptions) { const isPageloadTransaction = startSpanOptions.op === 'pageload'; const finalStartSpanOptions = beforeStartSpan ? beforeStartSpan(startSpanOptions) : startSpanOptions; const attributes = finalStartSpanOptions.attributes || {}; // If `finalStartSpanOptions.name` is different than `startSpanOptions.name` // it is because `beforeStartSpan` set a custom name. Therefore we set the source to 'custom'. if (startSpanOptions.name !== finalStartSpanOptions.name) { attributes[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = 'custom'; finalStartSpanOptions.attributes = attributes; } latestRoute.name = finalStartSpanOptions.name; latestRoute.source = attributes[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; const idleSpan = core.startIdleSpan(finalStartSpanOptions, { idleTimeout, finalTimeout, childSpanTimeout, // should wait for finish signal if it's a pageload transaction disableAutoFinish: isPageloadTransaction, beforeSpanEnd: span => { _collectWebVitals(); browserUtils.addPerformanceEntries(span, { recordClsOnPageloadSpan: !enableStandaloneClsSpans }); setActiveIdleSpan(client, undefined); // A trace should stay consistent over the entire timespan of one route - even after the pageload/navigation ended. // Only when another navigation happens, we want to create a new trace. // This way, e.g. errors that occur after the pageload span ended are still associated to the pageload trace. const scope = core.getCurrentScope(); const oldPropagationContext = scope.getPropagationContext(); scope.setPropagationContext({ ...oldPropagationContext, traceId: idleSpan.spanContext().traceId, sampled: core.spanIsSampled(idleSpan), dsc: core.getDynamicSamplingContextFromSpan(span), }); }, }); setActiveIdleSpan(client, idleSpan); function emitFinish() { if (optionalWindowDocument && ['interactive', 'complete'].includes(optionalWindowDocument.readyState)) { client.emit('idleSpanEnableAutoFinish', idleSpan); } } if (isPageloadTransaction && optionalWindowDocument) { optionalWindowDocument.addEventListener('readystatechange', () => { emitFinish(); }); emitFinish(); } } return { name: BROWSER_TRACING_INTEGRATION_ID, afterAllSetup(client) { let startingUrl = core.getLocationHref(); function maybeEndActiveSpan() { const activeSpan = getActiveIdleSpan(client); if (activeSpan && !core.spanToJSON(activeSpan).timestamp) { debugBuild.DEBUG_BUILD && core.logger.log(`[Tracing] Finishing current active span with op: ${core.spanToJSON(activeSpan).op}`); // If there's an open active span, we need to finish it before creating an new one. activeSpan.end(); } } client.on('startNavigationSpan', startSpanOptions => { if (core.getClient() !== client) { return; } maybeEndActiveSpan(); core.getIsolationScope().setPropagationContext({ traceId: core.generateTraceId(), sampleRand: Math.random() }); core.getCurrentScope().setPropagationContext({ traceId: core.generateTraceId(), sampleRand: Math.random() }); _createRouteSpan(client, { op: 'navigation', ...startSpanOptions, }); }); client.on('startPageLoadSpan', (startSpanOptions, traceOptions = {}) => { if (core.getClient() !== client) { return; } maybeEndActiveSpan(); const sentryTrace = traceOptions.sentryTrace || getMetaContent('sentry-trace'); const baggage = traceOptions.baggage || getMetaContent('baggage'); const propagationContext = core.propagationContextFromHeaders(sentryTrace, baggage); core.getCurrentScope().setPropagationContext(propagationContext); _createRouteSpan(client, { op: 'pageload', ...startSpanOptions, }); }); if (linkPreviousTrace !== 'off') { let inMemoryPreviousTraceInfo = undefined; client.on('spanStart', span => { if (core.getRootSpan(span) !== span) { return; } if (linkPreviousTrace === 'session-storage') { const updatedPreviousTraceInfo = previousTrace.addPreviousTraceSpanLink(previousTrace.getPreviousTraceFromSessionStorage(), span); previousTrace.storePreviousTraceInSessionStorage(updatedPreviousTraceInfo); } else { inMemoryPreviousTraceInfo = previousTrace.addPreviousTraceSpanLink(inMemoryPreviousTraceInfo, span); } }); } if (helpers.WINDOW.location) { if (instrumentPageLoad) { const origin = core.browserPerformanceTimeOrigin(); startBrowserTracingPageLoadSpan(client, { name: helpers.WINDOW.location.pathname, // pageload should always start at timeOrigin (and needs to be in s, not ms) startTime: origin ? origin / 1000 : undefined, attributes: { [core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', [core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', }, }); } if (instrumentNavigation) { browserUtils.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?.indexOf(to) !== -1) { startingUrl = undefined; return; } if (from !== to) { startingUrl = undefined; startBrowserTracingNavigationSpan(client, { name: helpers.WINDOW.location.pathname, attributes: { [core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', [core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', }, }); } }); } } if (markBackgroundSpan) { backgroundtab.registerBackgroundTabDetection(); } if (enableInteractions) { registerInteractionListener(client, idleTimeout, finalTimeout, childSpanTimeout, latestRoute); } if (enableInp) { browserUtils.registerInpInteractionListener(); } request.instrumentOutgoingRequests(client, { traceFetch, traceXHR, trackFetchStreamPerformance, tracePropagationTargets: client.getOptions().tracePropagationTargets, shouldCreateSpanForRequest, enableHTTPTimings, onRequestSpanStart, }); }, }; }) ; /** * Manually start a page load span. * This will only do something if a browser tracing integration integration has been setup. * * If you provide a custom `traceOptions` object, it will be used to continue the trace * instead of the default behavior, which is to look it up on the <meta> tags. */ function startBrowserTracingPageLoadSpan( client, spanOptions, traceOptions, ) { client.emit('startPageLoadSpan', spanOptions, traceOptions); core.getCurrentScope().setTransactionName(spanOptions.name); return getActiveIdleSpan(client); } /** * Manually start a navigation span. * This will only do something if a browser tracing integration has been setup. */ function startBrowserTracingNavigationSpan(client, spanOptions) { client.emit('startNavigationSpan', spanOptions); core.getCurrentScope().setTransactionName(spanOptions.name); return getActiveIdleSpan(client); } /** Returns the value of a meta tag */ function getMetaContent(metaName) { /** * This is just a small wrapper that makes `document` optional. * We want to be extra-safe and always check that this exists, to ensure weird environments do not blow up. */ const optionalWindowDocument = helpers.WINDOW.document ; const metaTag = optionalWindowDocument?.querySelector(`meta[name=${metaName}]`); return metaTag?.getAttribute('content') || undefined; } /** Start listener for interaction transactions */ function registerInteractionListener( client, idleTimeout, finalTimeout, childSpanTimeout, latestRoute, ) { /** * This is just a small wrapper that makes `document` optional. * We want to be extra-safe and always check that this exists, to ensure weird environments do not blow up. */ const optionalWindowDocument = helpers.WINDOW.document ; let inflightInteractionSpan; const registerInteractionTransaction = () => { const op = 'ui.action.click'; const activeIdleSpan = getActiveIdleSpan(client); if (activeIdleSpan) { const currentRootSpanOp = core.spanToJSON(activeIdleSpan).op; if (['navigation', 'pageload'].includes(currentRootSpanOp )) { debugBuild.DEBUG_BUILD && core.logger.warn(`[Tracing] Did not create ${op} span because a pageload or navigation span is in progress.`); return undefined; } } if (inflightInteractionSpan) { inflightInteractionSpan.setAttribute(core.SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, 'interactionInterrupted'); inflightInteractionSpan.end(); inflightInteractionSpan = undefined; } if (!latestRoute.name) { debugBuild.DEBUG_BUILD && core.logger.warn(`[Tracing] Did not create ${op} transaction because _latestRouteName is missing.`); return undefined; } inflightInteractionSpan = core.startIdleSpan( { name: latestRoute.name, op, attributes: { [core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: latestRoute.source || 'url', }, }, { idleTimeout, finalTimeout, childSpanTimeout, }, ); }; if (optionalWindowDocument) { addEventListener('click', registerInteractionTransaction, { once: false, capture: true }); } } // We store the active idle span on the client object, so we can access it from exported functions const ACTIVE_IDLE_SPAN_PROPERTY = '_sentry_idleSpan'; function getActiveIdleSpan(client) { return (client )[ACTIVE_IDLE_SPAN_PROPERTY]; } function setActiveIdleSpan(client, span) { core.addNonEnumerableProperty(client, ACTIVE_IDLE_SPAN_PROPERTY, span); } 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