UNPKG

@sentry/browser

Version:
419 lines (415 loc) • 17.2 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const core = require('@sentry/core'); const browser = require('@sentry/core/browser'); const browserUtils = require('@sentry/browser-utils'); const attributes = require('@sentry/conventions/attributes'); const debugBuild = require('../debug-build.js'); const helpers = require('../helpers.js'); const webVitals = require('../integrations/webVitals.js'); const backgroundtab = require('./backgroundtab.js'); const linkedTraces = require('./linkedTraces.js'); const request = require('./request.js'); const op = require('@sentry/conventions/op'); const BROWSER_TRACING_INTEGRATION_ID = "BrowserTracing"; const DEFAULT_BROWSER_TRACING_OPTIONS = { ...core.TRACING_DEFAULTS, instrumentNavigation: true, instrumentBfcacheRestore: true, instrumentPageLoad: true, markBackgroundSpan: true, enableLongTask: true, enableLongAnimationFrame: true, // oxlint-disable-next-line typescript/no-deprecated -- still honoured until it is removed enableInp: true, ignoreResourceSpans: [], detectRedirects: true, linkPreviousTrace: "in-memory", consistentTraceSampling: false, enableReportPageLoaded: false, ...request.defaultRequestInstrumentationOptions }; const browserTracingIntegration = ((options = {}) => { if ("enableElementTiming" in options) { core.consoleSandbox(() => { console.warn( "[Sentry] `enableElementTiming` is deprecated and no longer has any effect. Use the standalone `elementTimingIntegration` instead." ); }); } const optionalWindowDocument = helpers.WINDOW.document; const { // oxlint-disable-next-line typescript/no-deprecated -- still honoured until it is removed enableInp, enableLongTask, enableLongAnimationFrame, webVitals: webVitals$1, beforeStartSpan, idleTimeout, finalTimeout, childSpanTimeout, markBackgroundSpan, traceFetch, traceXHR, shouldCreateSpanForRequest, enableHTTPTimings, ignoreResourceSpans, instrumentPageLoad, instrumentNavigation, instrumentBfcacheRestore, detectRedirects, linkPreviousTrace, consistentTraceSampling, enableReportPageLoaded, onRequestSpanStart, onRequestSpanEnd } = { ...DEFAULT_BROWSER_TRACING_OPTIONS, ...options }; const _isBot = browserUtils.isBotUserAgent(); let lastInteractionTimestamp; let _pageloadSpan; function _createRouteSpan(client, startSpanOptions, makeActive = true, url) { const isPageloadSpan = startSpanOptions.op === "pageload"; const initialSpanName = startSpanOptions.name; const finalStartSpanOptions = beforeStartSpan ? beforeStartSpan(startSpanOptions) : startSpanOptions; const urlObject = core.parseStringToURLObject(url || browserUtils.getLocationHref()); const attributes$1 = { ...urlObject?.pathname && { [attributes.URL_PATH]: urlObject.pathname }, ...urlObject && !core.isURLObjectRelative(urlObject) && { [attributes.URL_FULL]: core.filterCollectedUrl(urlObject.href) }, ...finalStartSpanOptions.attributes }; if (initialSpanName !== finalStartSpanOptions.name) { attributes$1[attributes.SENTRY_SEGMENT_NAME_SOURCE] = "custom"; } finalStartSpanOptions.attributes = attributes$1; if (!makeActive) { const now = core.dateTimestampInSeconds(); browser.startInactiveSpan({ ...finalStartSpanOptions, startTime: now }).end(now); return; } const idleSpan = browser.startIdleSpan(finalStartSpanOptions, { idleTimeout, finalTimeout, childSpanTimeout, // should wait for finish signal if it's a pageload transaction disableAutoFinish: isPageloadSpan, trimIdleSpanEndTimestamp: !enableReportPageLoaded }); if (isPageloadSpan && enableReportPageLoaded) { _pageloadSpan = idleSpan; } setActiveIdleSpan(client, idleSpan); function emitFinish() { if (optionalWindowDocument && ["interactive", "complete"].includes(optionalWindowDocument.readyState)) { client.emit("idleSpanEnableAutoFinish", idleSpan); optionalWindowDocument.removeEventListener("readystatechange", emitFinish); } } if (isPageloadSpan && !enableReportPageLoaded && optionalWindowDocument) { optionalWindowDocument.addEventListener("readystatechange", emitFinish); emitFinish(); } } return { name: BROWSER_TRACING_INTEGRATION_ID, setup(client) { if (_isBot) { debugBuild.DEBUG_BUILD && core.debug.log("[Tracing] Skipping browserTracingIntegration setup for bot user agent."); return; } core.registerSpanErrorInstrumentation(); if (enableLongAnimationFrame && core.GLOBAL_OBJ.PerformanceObserver && PerformanceObserver.supportedEntryTypes?.includes("long-animation-frame")) { browserUtils.startTrackingLongAnimationFrames(); } else if (enableLongTask) { browserUtils.startTrackingLongTasks(); } if (detectRedirects && optionalWindowDocument) { const interactionHandler = () => { lastInteractionTimestamp = core.timestampInSeconds(); }; addEventListener("click", interactionHandler, { capture: true }); addEventListener("keydown", interactionHandler, { capture: true, passive: true }); } function maybeEndActiveSpan() { const activeSpan = getActiveIdleSpan(client); if (activeSpan && !core.spanToJSON(activeSpan).end_timestamp) { debugBuild.DEBUG_BUILD && core.debug.log( `[Tracing] Finishing current active span with op: ${core.spanToJSON(activeSpan).attributes[attributes.SENTRY_OP]}` ); activeSpan.setAttribute(attributes.SENTRY_IDLE_SPAN_FINISH_REASON, "cancelled"); activeSpan.end(); } } client.on("beforeIdleSpanEnd", (span) => { if (getActiveIdleSpan(client) !== span) { return; } browserUtils.addPerformanceEntries(span, { ignoreResourceSpans, spanStreamingEnabled: core.hasSpanStreamingEnabled(client) }); setActiveIdleSpan(client, void 0); const scope = core.getCurrentScope(); const oldPropagationContext = scope.getPropagationContext(); scope.setPropagationContext({ ...oldPropagationContext, traceId: span.spanContext().traceId, sampled: core.spanIsSampled(span), dsc: core.getDynamicSamplingContextFromSpan(span) }); if (_pageloadSpan === span) { _pageloadSpan = void 0; } }); client.on("startNavigationSpan", (startSpanOptions, navigationOptions) => { if (core.getClient() !== client) { return; } if (navigationOptions?.isRedirect) { debugBuild.DEBUG_BUILD && core.debug.warn("[Tracing] Detected redirect, navigation span will not be the root span, but a child span."); _createRouteSpan( client, { op: op.NAVIGATION_REDIRECT, ...startSpanOptions }, false, navigationOptions.url ); return; } lastInteractionTimestamp = void 0; maybeEndActiveSpan(); const scope = core.getCurrentScope(); scope.setPropagationContext({ traceId: core.generateTraceId(), sampleRand: Math.random(), propagationSpanId: core.hasSpansEnabled() ? void 0 : core.generateSpanId() }); scope.setSDKProcessingMetadata({ normalizedRequest: void 0 }); _createRouteSpan( client, { op: op.NAVIGATION, ...startSpanOptions, // Navigation starts a new trace and is NOT parented under any active interaction (e.g. ui.action.click) parentSpan: null }, true, navigationOptions?.url ); }); client.on("startPageLoadSpan", (startSpanOptions, traceOptions = {}) => { if (core.getClient() !== client) { return; } maybeEndActiveSpan(); const sentryTrace = traceOptions.sentryTrace || getMetaContent("sentry-trace") || getServerTiming("sentry-trace"); const baggage = traceOptions.baggage || getMetaContent("baggage") || getServerTiming("baggage"); const propagationContext = core.propagationContextFromHeaders(sentryTrace, baggage); const scope = core.getCurrentScope(); scope.setPropagationContext(propagationContext); if (!core.hasSpansEnabled()) { scope.getPropagationContext().propagationSpanId = core.generateSpanId(); } scope.setSDKProcessingMetadata({ normalizedRequest: helpers.getHttpRequestData() }); _createRouteSpan(client, { op: op.PAGELOAD, ...startSpanOptions }); }); client.on("endPageloadSpan", () => { if (enableReportPageLoaded && _pageloadSpan) { _pageloadSpan.setAttribute(attributes.SENTRY_IDLE_SPAN_FINISH_REASON, "reportPageLoaded"); _pageloadSpan.end(); } }); helpers.WINDOW.addEventListener?.("pagehide", () => { const activeSpan = getActiveIdleSpan(client); if (activeSpan && !core.spanToJSON(activeSpan).end_timestamp) { activeSpan.setAttribute(attributes.SENTRY_IDLE_SPAN_FINISH_REASON, "documentHidden"); activeSpan.end(); } void client.flush(); }); }, afterAllSetup(client) { if (_isBot) { return; } browser._INTERNAL_ensureBrowserSpanStreaming(client); if (client.addIntegration && !client.getIntegrationByName?.(webVitals.WEB_VITALS_INTEGRATION_NAME)) { const ignore = webVitals$1?.ignore ?? []; client.addIntegration( webVitals.webVitalsIntegration({ ...webVitals$1, ignore: enableInp || ignore.includes("inp") ? ignore : [...ignore, "inp"] }) ); } let startingUrl = browserUtils.getLocationHref(); if (linkPreviousTrace !== "off") { linkedTraces.linkTraces(client, { linkPreviousTrace, consistentTraceSampling }); } if (helpers.WINDOW.location) { if (instrumentPageLoad) { startBrowserTracingPageLoadSpan(client, { // With span streaming, span names have to be low cardinality, and there is no route // information available here. name: core.hasSpanStreamingEnabled(client) ? core.PAGELOAD_SPAN_NAME_FALLBACK : helpers.WINDOW.location.pathname, attributes: { [attributes.SENTRY_SEGMENT_NAME_SOURCE]: "url", [attributes.SENTRY_ORIGIN]: "auto.pageload.browser" } }); } if (instrumentNavigation) { browserUtils.addHistoryInstrumentationHandler(({ to, from }) => { if (from === void 0 && startingUrl !== void 0 && startingUrl.indexOf(to) !== -1) { startingUrl = void 0; return; } startingUrl = void 0; const parsed = core.parseStringToURLObject(to); const activeSpan = getActiveIdleSpan(client); const navigationIsRedirect = activeSpan && detectRedirects && isRedirect(activeSpan, lastInteractionTimestamp); startBrowserTracingNavigationSpan( client, { // With span streaming, span names have to be low cardinality, and there is no route // information available here. name: core.hasSpanStreamingEnabled(client) ? core.NAVIGATION_SPAN_NAME_FALLBACK : parsed?.pathname || helpers.WINDOW.location.pathname, attributes: { [attributes.SENTRY_SEGMENT_NAME_SOURCE]: "url", [attributes.SENTRY_ORIGIN]: "auto.navigation.browser" } }, { url: to, isRedirect: navigationIsRedirect } ); }); } if (instrumentBfcacheRestore) { helpers.WINDOW.addEventListener?.("pageshow", (event) => { if (!event.persisted) { return; } startingUrl = void 0; startBrowserTracingNavigationSpan( client, { // Deliberately no `startTime`: the span starts now, at the restore. The // `PerformanceNavigationTiming` entry still describes the original document load and // would date the span to before the page was frozen. // // TODO(routing): resolve the parameterized route via the route provider (#23551) and set // the source from it. No router event fires on a restore, so in a framework app this is // the only navigation span still named from a raw pathname. name: core.hasSpanStreamingEnabled(client) ? core.NAVIGATION_SPAN_NAME_FALLBACK : helpers.WINDOW.location?.pathname || "/", attributes: { [attributes.SENTRY_SEGMENT_NAME_SOURCE]: "url", [attributes.SENTRY_ORIGIN]: "auto.navigation.browser.bfcache", // A bfcache restore is near-instant, so these spans would otherwise drag // navigation duration percentiles down with no way to tell them apart. [attributes.BROWSER_NAVIGATION_TYPE]: "back-forward-cache" } }, { url: helpers.WINDOW.location?.href } ); }); } } if (markBackgroundSpan) { backgroundtab.registerBackgroundTabDetection(); } request.instrumentOutgoingRequests(client, { traceFetch, traceXHR, tracePropagationTargets: client.getOptions().tracePropagationTargets, shouldCreateSpanForRequest, enableHTTPTimings, onRequestSpanStart, onRequestSpanEnd }); } }; }); function startBrowserTracingPageLoadSpan(client, spanOptions, traceOptions) { const isFallbackSpanName = spanOptions.name === core.PAGELOAD_SPAN_NAME_FALLBACK; core.getCurrentScope().setTransactionName(isFallbackSpanName ? helpers.WINDOW.location?.pathname : spanOptions.name); const timeOrigin = core.browserPerformanceTimeOrigin(); const pageloadSpanOptions = { ...spanOptions, // startTime needs to be in seconds, not ms startTime: spanOptions.startTime ?? (timeOrigin ? timeOrigin / 1e3 : void 0) }; client.emit("startPageLoadSpan", pageloadSpanOptions, traceOptions); const pageloadSpan = getActiveIdleSpan(client); if (pageloadSpan) { client.emit("afterStartPageLoadSpan", pageloadSpan); } return pageloadSpan; } function startBrowserTracingNavigationSpan(client, spanOptions, options) { const { url, isRedirect: isRedirect2 } = options || {}; client.emit("beforeStartNavigationSpan", spanOptions, { isRedirect: isRedirect2, url }); client.emit("startNavigationSpan", spanOptions, { isRedirect: isRedirect2, url }); const scope = core.getCurrentScope(); const isFallbackSpanName = spanOptions.name === core.NAVIGATION_SPAN_NAME_FALLBACK; scope.setTransactionName( isFallbackSpanName ? url && core.parseStringToURLObject(url)?.pathname || helpers.WINDOW.location?.pathname : spanOptions.name ); if (url && !isRedirect2) { scope.setSDKProcessingMetadata({ normalizedRequest: { ...helpers.getHttpRequestData(), url } }); } return getActiveIdleSpan(client); } function getMetaContent(metaName) { const optionalWindowDocument = helpers.WINDOW.document; const metaTag = optionalWindowDocument?.querySelector(`meta[name=${metaName}]`); return metaTag?.getAttribute("content") || void 0; } function getServerTiming(name) { const navigation = helpers.WINDOW.performance?.getEntriesByType?.("navigation")[0]; const entry = navigation?.serverTiming?.find((entry2) => entry2.name === name); return entry?.description; } 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); } const REDIRECT_THRESHOLD = 1.5; function isRedirect(activeSpan, lastInteractionTimestamp) { const spanData = core.spanToJSON(activeSpan); const now = core.dateTimestampInSeconds(); const startTimestamp = spanData.start_timestamp; if (now - startTimestamp > REDIRECT_THRESHOLD) { return false; } if (lastInteractionTimestamp && now - lastInteractionTimestamp <= REDIRECT_THRESHOLD) { return false; } return true; } exports.BROWSER_TRACING_INTEGRATION_ID = BROWSER_TRACING_INTEGRATION_ID; exports.browserTracingIntegration = browserTracingIntegration; exports.getMetaContent = getMetaContent; exports.getServerTiming = getServerTiming; exports.startBrowserTracingNavigationSpan = startBrowserTracingNavigationSpan; exports.startBrowserTracingPageLoadSpan = startBrowserTracingPageLoadSpan; //# sourceMappingURL=browserTracingIntegration.js.map