@sentry/browser
Version:
Official Sentry SDK for browsers
430 lines (427 loc) • 17 kB
JavaScript
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