UNPKG

@sentry/browser

Version:
430 lines (427 loc) 17 kB
import { TRACING_DEFAULTS, consoleSandbox, getLocationHref, browserPerformanceTimeOrigin, parseStringToURLObject, debug, registerSpanErrorInstrumentation, hasSpanStreamingEnabled, GLOBAL_OBJ, getClient, getIsolationScope, hasSpansEnabled, generateSpanId, generateTraceId, getCurrentScope, propagationContextFromHeaders, spanToJSON, dateTimestampInSeconds, timestampInSeconds, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, startInactiveSpan, startIdleSpan, getDynamicSamplingContextFromSpan, spanIsSampled, SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, addNonEnumerableProperty, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core/browser'; import { addHistoryInstrumentationHandler, registerInpInteractionListener, startTrackingWebVitals, trackLcpAsSpan, trackClsAsSpan, trackInpAsSpan, startTrackingINP, startTrackingLongAnimationFrames, startTrackingLongTasks, startTrackingInteractions, addPerformanceEntries } from '@sentry-internal/browser-utils'; import { DEBUG_BUILD } from '../debug-build.js'; import { WINDOW, getHttpRequestData } from '../helpers.js'; import { fetchStreamPerformanceIntegration } from '../integrations/fetchStreamPerformance.js'; import { registerBackgroundTabDetection } from './backgroundtab.js'; import { linkTraces } from './linkedTraces.js'; import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request.js'; const BROWSER_TRACING_INTEGRATION_ID = "BrowserTracing"; const BOT_USER_AGENT_RE = /Googlebot|Google-InspectionTool|Storebot-Google|Bingbot|Slurp|DuckDuckBot|Baiduspider|YandexBot|Facebot|facebookexternalhit|LinkedInBot|Twitterbot|Applebot/i; function isBotUserAgent() { const nav = WINDOW.navigator; if (!nav?.userAgent) { return false; } return BOT_USER_AGENT_RE.test(nav.userAgent); } const DEFAULT_BROWSER_TRACING_OPTIONS = { ...TRACING_DEFAULTS, instrumentNavigation: true, instrumentPageLoad: true, markBackgroundSpan: true, enableLongTask: true, enableLongAnimationFrame: true, enableInp: true, ignoreResourceSpans: [], ignorePerformanceApiSpans: [], detectRedirects: true, linkPreviousTrace: "in-memory", consistentTraceSampling: false, enableReportPageLoaded: false, _experiments: {}, ...defaultRequestInstrumentationOptions }; const browserTracingIntegration = ((options = {}) => { if ("enableElementTiming" in options) { consoleSandbox(() => { console.warn( "[Sentry] `enableElementTiming` is deprecated and no longer has any effect. Use the standalone `elementTimingIntegration` instead." ); }); } const latestRoute = { name: void 0, source: void 0 }; const optionalWindowDocument = WINDOW.document; const { enableInp, enableLongTask, enableLongAnimationFrame, _experiments: { enableInteractions, enableStandaloneClsSpans, enableStandaloneLcpSpans }, beforeStartSpan, idleTimeout, finalTimeout, childSpanTimeout, markBackgroundSpan, traceFetch, traceXHR, trackFetchStreamPerformance, shouldCreateSpanForRequest, enableHTTPTimings, ignoreResourceSpans, ignorePerformanceApiSpans, instrumentPageLoad, instrumentNavigation, detectRedirects, linkPreviousTrace, consistentTraceSampling, enableReportPageLoaded, onRequestSpanStart, onRequestSpanEnd } = { ...DEFAULT_BROWSER_TRACING_OPTIONS, ...options }; const _isBot = isBotUserAgent(); let _collectWebVitals; let lastInteractionTimestamp; let _pageloadSpan; function _createRouteSpan(client, startSpanOptions, makeActive = true) { const isPageloadSpan = startSpanOptions.op === "pageload"; const initialSpanName = startSpanOptions.name; const finalStartSpanOptions = beforeStartSpan ? beforeStartSpan(startSpanOptions) : startSpanOptions; const attributes = finalStartSpanOptions.attributes || {}; if (initialSpanName !== finalStartSpanOptions.name) { attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = "custom"; finalStartSpanOptions.attributes = attributes; } if (!makeActive) { const now = dateTimestampInSeconds(); startInactiveSpan({ ...finalStartSpanOptions, startTime: now }).end(now); return; } latestRoute.name = finalStartSpanOptions.name; latestRoute.source = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; const idleSpan = startIdleSpan(finalStartSpanOptions, { idleTimeout, finalTimeout, childSpanTimeout, // should wait for finish signal if it's a pageload transaction disableAutoFinish: isPageloadSpan, beforeSpanEnd: (span) => { _collectWebVitals?.(); const spanStreamingEnabled = hasSpanStreamingEnabled(client); addPerformanceEntries(span, { recordClsOnPageloadSpan: !spanStreamingEnabled && !enableStandaloneClsSpans, recordLcpOnPageloadSpan: !spanStreamingEnabled && !enableStandaloneLcpSpans, ignoreResourceSpans, ignorePerformanceApiSpans, spanStreamingEnabled }); setActiveIdleSpan(client, void 0); const scope = getCurrentScope(); const oldPropagationContext = scope.getPropagationContext(); scope.setPropagationContext({ ...oldPropagationContext, traceId: idleSpan.spanContext().traceId, sampled: spanIsSampled(idleSpan), dsc: getDynamicSamplingContextFromSpan(span) }); if (isPageloadSpan) { _pageloadSpan = void 0; } }, trimIdleSpanEndTimestamp: !enableReportPageLoaded }); if (isPageloadSpan && enableReportPageLoaded) { _pageloadSpan = idleSpan; } setActiveIdleSpan(client, idleSpan); function emitFinish() { if (optionalWindowDocument && ["interactive", "complete"].includes(optionalWindowDocument.readyState)) { client.emit("idleSpanEnableAutoFinish", idleSpan); } } if (isPageloadSpan && !enableReportPageLoaded && optionalWindowDocument) { optionalWindowDocument.addEventListener("readystatechange", () => { emitFinish(); }); emitFinish(); } } return { name: BROWSER_TRACING_INTEGRATION_ID, setup(client) { if (_isBot) { DEBUG_BUILD && debug.log("[Tracing] Skipping browserTracingIntegration setup for bot user agent."); return; } registerSpanErrorInstrumentation(); const spanStreamingEnabled = hasSpanStreamingEnabled(client); _collectWebVitals = startTrackingWebVitals({ recordClsStandaloneSpans: spanStreamingEnabled ? void 0 : enableStandaloneClsSpans || false, recordLcpStandaloneSpans: spanStreamingEnabled ? void 0 : enableStandaloneLcpSpans || false, client }); if (spanStreamingEnabled) { trackLcpAsSpan(client); trackClsAsSpan(client); if (enableInp) { trackInpAsSpan(); } } else if (enableInp) { startTrackingINP(); } if (enableLongAnimationFrame && GLOBAL_OBJ.PerformanceObserver && PerformanceObserver.supportedEntryTypes?.includes("long-animation-frame")) { startTrackingLongAnimationFrames(); } else if (enableLongTask) { startTrackingLongTasks(); } if (enableInteractions) { startTrackingInteractions(); } if (detectRedirects && optionalWindowDocument) { const interactionHandler = () => { lastInteractionTimestamp = timestampInSeconds(); }; addEventListener("click", interactionHandler, { capture: true }); addEventListener("keydown", interactionHandler, { capture: true, passive: true }); } function maybeEndActiveSpan() { const activeSpan = getActiveIdleSpan(client); if (activeSpan && !spanToJSON(activeSpan).timestamp) { DEBUG_BUILD && debug.log(`[Tracing] Finishing current active span with op: ${spanToJSON(activeSpan).op}`); activeSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, "cancelled"); activeSpan.end(); } } client.on("startNavigationSpan", (startSpanOptions, navigationOptions) => { if (getClient() !== client) { return; } if (navigationOptions?.isRedirect) { DEBUG_BUILD && debug.warn("[Tracing] Detected redirect, navigation span will not be the root span, but a child span."); _createRouteSpan( client, { op: "navigation.redirect", ...startSpanOptions }, false ); return; } lastInteractionTimestamp = void 0; maybeEndActiveSpan(); getIsolationScope().setPropagationContext({ traceId: generateTraceId(), sampleRand: Math.random(), propagationSpanId: hasSpansEnabled() ? void 0 : generateSpanId() }); const scope = getCurrentScope(); scope.setPropagationContext({ traceId: generateTraceId(), sampleRand: Math.random(), propagationSpanId: hasSpansEnabled() ? void 0 : generateSpanId() }); scope.setSDKProcessingMetadata({ normalizedRequest: void 0 }); _createRouteSpan(client, { op: "navigation", ...startSpanOptions, // Navigation starts a new trace and is NOT parented under any active interaction (e.g. ui.action.click) parentSpan: null, forceTransaction: true }); }); client.on("startPageLoadSpan", (startSpanOptions, traceOptions = {}) => { if (getClient() !== client) { return; } maybeEndActiveSpan(); const sentryTrace = traceOptions.sentryTrace || getMetaContent("sentry-trace") || getServerTiming("sentry-trace"); const baggage = traceOptions.baggage || getMetaContent("baggage") || getServerTiming("baggage"); const propagationContext = propagationContextFromHeaders(sentryTrace, baggage); const scope = getCurrentScope(); scope.setPropagationContext(propagationContext); if (!hasSpansEnabled()) { scope.getPropagationContext().propagationSpanId = generateSpanId(); } scope.setSDKProcessingMetadata({ normalizedRequest: getHttpRequestData() }); _createRouteSpan(client, { op: "pageload", ...startSpanOptions }); }); client.on("endPageloadSpan", () => { if (enableReportPageLoaded && _pageloadSpan) { _pageloadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, "reportPageLoaded"); _pageloadSpan.end(); } }); }, afterAllSetup(client) { if (_isBot) { return; } let startingUrl = getLocationHref(); if (linkPreviousTrace !== "off") { linkTraces(client, { linkPreviousTrace, consistentTraceSampling }); } if (WINDOW.location) { if (instrumentPageLoad) { const origin = browserPerformanceTimeOrigin(); startBrowserTracingPageLoadSpan(client, { name: WINDOW.location.pathname, // pageload should always start at timeOrigin (and needs to be in s, not ms) startTime: origin ? origin / 1e3 : void 0, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: "url", [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.pageload.browser" } }); } if (instrumentNavigation) { addHistoryInstrumentationHandler(({ to, from }) => { if (from === void 0 && startingUrl?.indexOf(to) !== -1) { startingUrl = void 0; return; } startingUrl = void 0; const parsed = parseStringToURLObject(to); const activeSpan = getActiveIdleSpan(client); const navigationIsRedirect = activeSpan && detectRedirects && isRedirect(activeSpan, lastInteractionTimestamp); startBrowserTracingNavigationSpan( client, { name: parsed?.pathname || WINDOW.location.pathname, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: "url", [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.navigation.browser" } }, { url: to, isRedirect: navigationIsRedirect } ); }); } } if (markBackgroundSpan) { registerBackgroundTabDetection(); } if (enableInteractions) { registerInteractionListener(client, idleTimeout, finalTimeout, childSpanTimeout, latestRoute); } if (enableInp) { registerInpInteractionListener(); } instrumentOutgoingRequests(client, { traceFetch, traceXHR, tracePropagationTargets: client.getOptions().tracePropagationTargets, shouldCreateSpanForRequest, enableHTTPTimings, onRequestSpanStart, onRequestSpanEnd }); if (trackFetchStreamPerformance) { client.addIntegration(fetchStreamPerformanceIntegration()); } } }; }); function startBrowserTracingPageLoadSpan(client, spanOptions, traceOptions) { client.emit("startPageLoadSpan", spanOptions, traceOptions); getCurrentScope().setTransactionName(spanOptions.name); 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 }); client.emit("startNavigationSpan", spanOptions, { isRedirect: isRedirect2 }); const scope = getCurrentScope(); scope.setTransactionName(spanOptions.name); if (url && !isRedirect2) { scope.setSDKProcessingMetadata({ normalizedRequest: { ...getHttpRequestData(), url } }); } return getActiveIdleSpan(client); } function getMetaContent(metaName) { const optionalWindowDocument = WINDOW.document; const metaTag = optionalWindowDocument?.querySelector(`meta[name=${metaName}]`); return metaTag?.getAttribute("content") || void 0; } function getServerTiming(name) { const navigation = WINDOW.performance?.getEntriesByType?.("navigation")[0]; const entry = navigation?.serverTiming?.find((entry2) => entry2.name === name); return entry?.description; } function registerInteractionListener(client, idleTimeout, finalTimeout, childSpanTimeout, latestRoute) { const optionalWindowDocument = WINDOW.document; let inflightInteractionSpan; const registerInteractionTransaction = () => { const op = "ui.action.click"; const activeIdleSpan = getActiveIdleSpan(client); if (activeIdleSpan) { const currentRootSpanOp = spanToJSON(activeIdleSpan).op; if (["navigation", "pageload"].includes(currentRootSpanOp)) { DEBUG_BUILD && debug.warn(`[Tracing] Did not create ${op} span because a pageload or navigation span is in progress.`); return void 0; } } if (inflightInteractionSpan) { inflightInteractionSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, "interactionInterrupted"); inflightInteractionSpan.end(); inflightInteractionSpan = void 0; } if (!latestRoute.name) { DEBUG_BUILD && debug.warn(`[Tracing] Did not create ${op} transaction because _latestRouteName is missing.`); return void 0; } inflightInteractionSpan = startIdleSpan( { name: latestRoute.name, op, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: latestRoute.source || "url" } }, { idleTimeout, finalTimeout, childSpanTimeout } ); }; if (optionalWindowDocument) { addEventListener("click", registerInteractionTransaction, { capture: true }); } } const ACTIVE_IDLE_SPAN_PROPERTY = "_sentry_idleSpan"; function getActiveIdleSpan(client) { return client[ACTIVE_IDLE_SPAN_PROPERTY]; } function setActiveIdleSpan(client, span) { addNonEnumerableProperty(client, ACTIVE_IDLE_SPAN_PROPERTY, span); } const REDIRECT_THRESHOLD = 1.5; function isRedirect(activeSpan, lastInteractionTimestamp) { const spanData = spanToJSON(activeSpan); const now = dateTimestampInSeconds(); const startTimestamp = spanData.start_timestamp; if (now - startTimestamp > REDIRECT_THRESHOLD) { return false; } if (lastInteractionTimestamp && now - lastInteractionTimestamp <= REDIRECT_THRESHOLD) { return false; } return true; } export { BROWSER_TRACING_INTEGRATION_ID, browserTracingIntegration, getMetaContent, getServerTiming, isBotUserAgent, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan }; //# sourceMappingURL=browserTracingIntegration.js.map