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