package.build.npm.esm.tracing.browserTracingIntegration.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of browser Show documentation
Show all versions of browser Show documentation
Official Sentry SDK for browsers
import { startTrackingWebVitals, startTrackingINP, startTrackingLongAnimationFrames, startTrackingLongTasks, startTrackingInteractions, addHistoryInstrumentationHandler, registerInpInteractionListener, addPerformanceEntries } from '@sentry-internal/browser-utils';
import { TRACING_DEFAULTS, registerSpanErrorInstrumentation, getClient, spanToJSON, getCurrentScope, getRootSpan, spanIsSampled, getDynamicSamplingContextFromSpan, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getActiveSpan, getIsolationScope, startIdleSpan, SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON } from '@sentry/core';
import { GLOBAL_OBJ, logger, propagationContextFromHeaders, browserPerformanceTimeOrigin, generatePropagationContext, getDomElement } from '@sentry/utils';
import { DEBUG_BUILD } from '../debug-build.js';
import { WINDOW } from '../helpers.js';
import { registerBackgroundTabDetection } from './backgroundtab.js';
import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request.js';
/* eslint-disable max-lines */
const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing';
const DEFAULT_BROWSER_TRACING_OPTIONS = {
...TRACING_DEFAULTS,
instrumentNavigation: true,
instrumentPageLoad: true,
markBackgroundSpan: true,
enableLongTask: true,
enableLongAnimationFrame: true,
enableInp: true,
_experiments: {},
...defaultRequestInstrumentationOptions,
};
/**
* 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 = {}) => {
registerSpanErrorInstrumentation();
const {
enableInp,
enableLongTask,
enableLongAnimationFrame,
_experiments: { enableInteractions, enableStandaloneClsSpans },
beforeStartSpan,
idleTimeout,
finalTimeout,
childSpanTimeout,
markBackgroundSpan,
traceFetch,
traceXHR,
shouldCreateSpanForRequest,
enableHTTPTimings,
instrumentPageLoad,
instrumentNavigation,
} = {
...DEFAULT_BROWSER_TRACING_OPTIONS,
..._options,
};
const _collectWebVitals = startTrackingWebVitals({ recordClsStandaloneSpans: enableStandaloneClsSpans || false });
if (enableInp) {
startTrackingINP();
}
if (
enableLongAnimationFrame &&
GLOBAL_OBJ.PerformanceObserver &&
PerformanceObserver.supportedEntryTypes.includes('long-animation-frame')
) {
startTrackingLongAnimationFrames();
} else if (enableLongTask) {
startTrackingLongTasks();
}
if (enableInteractions) {
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[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = 'custom';
finalStartSpanOptions.attributes = attributes;
}
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: isPageloadTransaction,
beforeSpanEnd: span => {
_collectWebVitals();
addPerformanceEntries(span, { recordClsOnPageloadSpan: !enableStandaloneClsSpans });
},
});
function emitFinish() {
if (['interactive', 'complete'].includes(WINDOW.document.readyState)) {
client.emit('idleSpanEnableAutoFinish', idleSpan);
}
}
if (isPageloadTransaction && WINDOW.document) {
WINDOW.document.addEventListener('readystatechange', () => {
emitFinish();
});
emitFinish();
}
return idleSpan;
}
return {
name: BROWSER_TRACING_INTEGRATION_ID,
afterAllSetup(client) {
let activeSpan;
let startingUrl = WINDOW.location && WINDOW.location.href;
client.on('startNavigationSpan', startSpanOptions => {
if (getClient() !== client) {
return;
}
if (activeSpan && !spanToJSON(activeSpan).timestamp) {
DEBUG_BUILD && logger.log(`[Tracing] Finishing current root span with op: ${spanToJSON(activeSpan).op}`);
// If there's an open transaction on the scope, we need to finish it before creating an new one.
activeSpan.end();
}
activeSpan = _createRouteSpan(client, {
op: 'navigation',
...startSpanOptions,
});
});
client.on('startPageLoadSpan', (startSpanOptions, traceOptions = {}) => {
if (getClient() !== client) {
return;
}
if (activeSpan && !spanToJSON(activeSpan).timestamp) {
DEBUG_BUILD && logger.log(`[Tracing] Finishing current root span with op: ${spanToJSON(activeSpan).op}`);
// If there's an open transaction on the scope, we need to finish it before creating an new one.
activeSpan.end();
}
const sentryTrace = traceOptions.sentryTrace || getMetaContent('sentry-trace');
const baggage = traceOptions.baggage || getMetaContent('baggage');
const propagationContext = propagationContextFromHeaders(sentryTrace, baggage);
getCurrentScope().setPropagationContext(propagationContext);
activeSpan = _createRouteSpan(client, {
op: 'pageload',
...startSpanOptions,
});
});
// A trace should to stay the consistent over the entire time span of one route.
// Therefore, when the initial pageload or navigation root span ends, we update the
// scope's propagation context to keep span-specific attributes like the `sampled` decision and
// the dynamic sampling context valid, even after the root span has ended.
// This ensures that the trace data is consistent for the entire duration of the route.
client.on('spanEnd', span => {
const op = spanToJSON(span).op;
if (span !== getRootSpan(span) || (op !== 'navigation' && op !== 'pageload')) {
return;
}
const scope = getCurrentScope();
const oldPropagationContext = scope.getPropagationContext();
scope.setPropagationContext({
...oldPropagationContext,
sampled: oldPropagationContext.sampled !== undefined ? oldPropagationContext.sampled : spanIsSampled(span),
dsc: oldPropagationContext.dsc || getDynamicSamplingContextFromSpan(span),
});
});
if (WINDOW.location) {
if (instrumentPageLoad) {
startBrowserTracingPageLoadSpan(client, {
name: WINDOW.location.pathname,
// pageload should always start at timeOrigin (and needs to be in s, not ms)
startTime: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser',
},
});
}
if (instrumentNavigation) {
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 && startingUrl.indexOf(to) !== -1) {
startingUrl = undefined;
return;
}
if (from !== to) {
startingUrl = undefined;
startBrowserTracingNavigationSpan(client, {
name: WINDOW.location.pathname,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser',
},
});
}
});
}
}
if (markBackgroundSpan) {
registerBackgroundTabDetection();
}
if (enableInteractions) {
registerInteractionListener(idleTimeout, finalTimeout, childSpanTimeout, latestRoute);
}
if (enableInp) {
registerInpInteractionListener();
}
instrumentOutgoingRequests(client, {
traceFetch,
traceXHR,
tracePropagationTargets: client.getOptions().tracePropagationTargets,
shouldCreateSpanForRequest,
enableHTTPTimings,
});
},
};
}) ;
/**
* 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 tags.
*/
function startBrowserTracingPageLoadSpan(
client,
spanOptions,
traceOptions,
) {
client.emit('startPageLoadSpan', spanOptions, traceOptions);
getCurrentScope().setTransactionName(spanOptions.name);
const span = getActiveSpan();
const op = span && spanToJSON(span).op;
return op === 'pageload' ? span : undefined;
}
/**
* Manually start a navigation span.
* This will only do something if a browser tracing integration has been setup.
*/
function startBrowserTracingNavigationSpan(client, spanOptions) {
getIsolationScope().setPropagationContext(generatePropagationContext());
getCurrentScope().setPropagationContext(generatePropagationContext());
client.emit('startNavigationSpan', spanOptions);
getCurrentScope().setTransactionName(spanOptions.name);
const span = getActiveSpan();
const op = span && spanToJSON(span).op;
return op === 'navigation' ? span : undefined;
}
/** Returns the value of a meta tag */
function getMetaContent(metaName) {
// Can't specify generic to `getDomElement` because tracing can be used
// in a variety of environments, have to disable `no-unsafe-member-access`
// as a result.
const metaTag = getDomElement(`meta[name=${metaName}]`);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return metaTag ? metaTag.getAttribute('content') : undefined;
}
/** Start listener for interaction transactions */
function registerInteractionListener(
idleTimeout,
finalTimeout,
childSpanTimeout,
latestRoute,
) {
let inflightInteractionSpan;
const registerInteractionTransaction = () => {
const op = 'ui.action.click';
const activeSpan = getActiveSpan();
const rootSpan = activeSpan && getRootSpan(activeSpan);
if (rootSpan) {
const currentRootSpanOp = spanToJSON(rootSpan).op;
if (['navigation', 'pageload'].includes(currentRootSpanOp )) {
DEBUG_BUILD &&
logger.warn(`[Tracing] Did not create ${op} span because a pageload or navigation span is in progress.`);
return undefined;
}
}
if (inflightInteractionSpan) {
inflightInteractionSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, 'interactionInterrupted');
inflightInteractionSpan.end();
inflightInteractionSpan = undefined;
}
if (!latestRoute.name) {
DEBUG_BUILD && logger.warn(`[Tracing] Did not create ${op} transaction because _latestRouteName is missing.`);
return undefined;
}
inflightInteractionSpan = startIdleSpan(
{
name: latestRoute.name,
op,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: latestRoute.source || 'url',
},
},
{
idleTimeout,
finalTimeout,
childSpanTimeout,
},
);
};
if (WINDOW.document) {
addEventListener('click', registerInteractionTransaction, { once: false, capture: true });
}
}
export { BROWSER_TRACING_INTEGRATION_ID, browserTracingIntegration, getMetaContent, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan };
//# sourceMappingURL=browserTracingIntegration.js.map