@sentry/browser
Version:
Official Sentry SDK for browsers
438 lines (434 loc) • 17.1 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const browser = require('@sentry/core/browser');
const browserUtils = require('@sentry-internal/browser-utils');
const debugBuild = require('../debug-build.js');
const helpers = require('../helpers.js');
const fetchStreamPerformance = require('../integrations/fetchStreamPerformance.js');
const backgroundtab = require('./backgroundtab.js');
const linkedTraces = require('./linkedTraces.js');
const request = require('./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 = helpers.WINDOW.navigator;
if (!nav?.userAgent) {
return false;
}
return BOT_USER_AGENT_RE.test(nav.userAgent);
}
const DEFAULT_BROWSER_TRACING_OPTIONS = {
...browser.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: {},
...request.defaultRequestInstrumentationOptions
};
const browserTracingIntegration = ((options = {}) => {
if ("enableElementTiming" in options) {
browser.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 = helpers.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[browser.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = "custom";
finalStartSpanOptions.attributes = attributes;
}
if (!makeActive) {
const now = browser.dateTimestampInSeconds();
browser.startInactiveSpan({
...finalStartSpanOptions,
startTime: now
}).end(now);
return;
}
latestRoute.name = finalStartSpanOptions.name;
latestRoute.source = attributes[browser.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
const idleSpan = browser.startIdleSpan(finalStartSpanOptions, {
idleTimeout,
finalTimeout,
childSpanTimeout,
// should wait for finish signal if it's a pageload transaction
disableAutoFinish: isPageloadSpan,
beforeSpanEnd: (span) => {
_collectWebVitals?.();
const spanStreamingEnabled = browser.hasSpanStreamingEnabled(client);
browserUtils.addPerformanceEntries(span, {
recordClsOnPageloadSpan: !spanStreamingEnabled && !enableStandaloneClsSpans,
recordLcpOnPageloadSpan: !spanStreamingEnabled && !enableStandaloneLcpSpans,
ignoreResourceSpans,
ignorePerformanceApiSpans,
spanStreamingEnabled
});
setActiveIdleSpan(client, void 0);
const scope = browser.getCurrentScope();
const oldPropagationContext = scope.getPropagationContext();
scope.setPropagationContext({
...oldPropagationContext,
traceId: idleSpan.spanContext().traceId,
sampled: browser.spanIsSampled(idleSpan),
dsc: browser.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) {
debugBuild.DEBUG_BUILD && browser.debug.log("[Tracing] Skipping browserTracingIntegration setup for bot user agent.");
return;
}
browser.registerSpanErrorInstrumentation();
const spanStreamingEnabled = browser.hasSpanStreamingEnabled(client);
_collectWebVitals = browserUtils.startTrackingWebVitals({
recordClsStandaloneSpans: spanStreamingEnabled ? void 0 : enableStandaloneClsSpans || false,
recordLcpStandaloneSpans: spanStreamingEnabled ? void 0 : enableStandaloneLcpSpans || false,
client
});
if (spanStreamingEnabled) {
browserUtils.trackLcpAsSpan(client);
browserUtils.trackClsAsSpan(client);
if (enableInp) {
browserUtils.trackInpAsSpan();
}
} else if (enableInp) {
browserUtils.startTrackingINP();
}
if (enableLongAnimationFrame && browser.GLOBAL_OBJ.PerformanceObserver && PerformanceObserver.supportedEntryTypes?.includes("long-animation-frame")) {
browserUtils.startTrackingLongAnimationFrames();
} else if (enableLongTask) {
browserUtils.startTrackingLongTasks();
}
if (enableInteractions) {
browserUtils.startTrackingInteractions();
}
if (detectRedirects && optionalWindowDocument) {
const interactionHandler = () => {
lastInteractionTimestamp = browser.timestampInSeconds();
};
addEventListener("click", interactionHandler, { capture: true });
addEventListener("keydown", interactionHandler, { capture: true, passive: true });
}
function maybeEndActiveSpan() {
const activeSpan = getActiveIdleSpan(client);
if (activeSpan && !browser.spanToJSON(activeSpan).timestamp) {
debugBuild.DEBUG_BUILD && browser.debug.log(`[Tracing] Finishing current active span with op: ${browser.spanToJSON(activeSpan).op}`);
activeSpan.setAttribute(browser.SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, "cancelled");
activeSpan.end();
}
}
client.on("startNavigationSpan", (startSpanOptions, navigationOptions) => {
if (browser.getClient() !== client) {
return;
}
if (navigationOptions?.isRedirect) {
debugBuild.DEBUG_BUILD && browser.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();
browser.getIsolationScope().setPropagationContext({
traceId: browser.generateTraceId(),
sampleRand: Math.random(),
propagationSpanId: browser.hasSpansEnabled() ? void 0 : browser.generateSpanId()
});
const scope = browser.getCurrentScope();
scope.setPropagationContext({
traceId: browser.generateTraceId(),
sampleRand: Math.random(),
propagationSpanId: browser.hasSpansEnabled() ? void 0 : browser.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 (browser.getClient() !== client) {
return;
}
maybeEndActiveSpan();
const sentryTrace = traceOptions.sentryTrace || getMetaContent("sentry-trace") || getServerTiming("sentry-trace");
const baggage = traceOptions.baggage || getMetaContent("baggage") || getServerTiming("baggage");
const propagationContext = browser.propagationContextFromHeaders(sentryTrace, baggage);
const scope = browser.getCurrentScope();
scope.setPropagationContext(propagationContext);
if (!browser.hasSpansEnabled()) {
scope.getPropagationContext().propagationSpanId = browser.generateSpanId();
}
scope.setSDKProcessingMetadata({
normalizedRequest: helpers.getHttpRequestData()
});
_createRouteSpan(client, {
op: "pageload",
...startSpanOptions
});
});
client.on("endPageloadSpan", () => {
if (enableReportPageLoaded && _pageloadSpan) {
_pageloadSpan.setAttribute(browser.SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, "reportPageLoaded");
_pageloadSpan.end();
}
});
},
afterAllSetup(client) {
if (_isBot) {
return;
}
let startingUrl = browser.getLocationHref();
if (linkPreviousTrace !== "off") {
linkedTraces.linkTraces(client, { linkPreviousTrace, consistentTraceSampling });
}
if (helpers.WINDOW.location) {
if (instrumentPageLoad) {
const origin = browser.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 / 1e3 : void 0,
attributes: {
[browser.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: "url",
[browser.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.pageload.browser"
}
});
}
if (instrumentNavigation) {
browserUtils.addHistoryInstrumentationHandler(({ to, from }) => {
if (from === void 0 && startingUrl?.indexOf(to) !== -1) {
startingUrl = void 0;
return;
}
startingUrl = void 0;
const parsed = browser.parseStringToURLObject(to);
const activeSpan = getActiveIdleSpan(client);
const navigationIsRedirect = activeSpan && detectRedirects && isRedirect(activeSpan, lastInteractionTimestamp);
startBrowserTracingNavigationSpan(
client,
{
name: parsed?.pathname || helpers.WINDOW.location.pathname,
attributes: {
[browser.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: "url",
[browser.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.navigation.browser"
}
},
{ url: to, isRedirect: navigationIsRedirect }
);
});
}
}
if (markBackgroundSpan) {
backgroundtab.registerBackgroundTabDetection();
}
if (enableInteractions) {
registerInteractionListener(client, idleTimeout, finalTimeout, childSpanTimeout, latestRoute);
}
if (enableInp) {
browserUtils.registerInpInteractionListener();
}
request.instrumentOutgoingRequests(client, {
traceFetch,
traceXHR,
tracePropagationTargets: client.getOptions().tracePropagationTargets,
shouldCreateSpanForRequest,
enableHTTPTimings,
onRequestSpanStart,
onRequestSpanEnd
});
if (trackFetchStreamPerformance) {
client.addIntegration(fetchStreamPerformance.fetchStreamPerformanceIntegration());
}
}
};
});
function startBrowserTracingPageLoadSpan(client, spanOptions, traceOptions) {
client.emit("startPageLoadSpan", spanOptions, traceOptions);
browser.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 = browser.getCurrentScope();
scope.setTransactionName(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;
}
function registerInteractionListener(client, idleTimeout, finalTimeout, childSpanTimeout, latestRoute) {
const optionalWindowDocument = helpers.WINDOW.document;
let inflightInteractionSpan;
const registerInteractionTransaction = () => {
const op = "ui.action.click";
const activeIdleSpan = getActiveIdleSpan(client);
if (activeIdleSpan) {
const currentRootSpanOp = browser.spanToJSON(activeIdleSpan).op;
if (["navigation", "pageload"].includes(currentRootSpanOp)) {
debugBuild.DEBUG_BUILD && browser.debug.warn(`[Tracing] Did not create ${op} span because a pageload or navigation span is in progress.`);
return void 0;
}
}
if (inflightInteractionSpan) {
inflightInteractionSpan.setAttribute(browser.SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, "interactionInterrupted");
inflightInteractionSpan.end();
inflightInteractionSpan = void 0;
}
if (!latestRoute.name) {
debugBuild.DEBUG_BUILD && browser.debug.warn(`[Tracing] Did not create ${op} transaction because _latestRouteName is missing.`);
return void 0;
}
inflightInteractionSpan = browser.startIdleSpan(
{
name: latestRoute.name,
op,
attributes: {
[browser.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) {
browser.addNonEnumerableProperty(client, ACTIVE_IDLE_SPAN_PROPERTY, span);
}
const REDIRECT_THRESHOLD = 1.5;
function isRedirect(activeSpan, lastInteractionTimestamp) {
const spanData = browser.spanToJSON(activeSpan);
const now = browser.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.isBotUserAgent = isBotUserAgent;
exports.startBrowserTracingNavigationSpan = startBrowserTracingNavigationSpan;
exports.startBrowserTracingPageLoadSpan = startBrowserTracingPageLoadSpan;
//# sourceMappingURL=browserTracingIntegration.js.map