@sentry/browser
Version:
Official Sentry SDK for browsers
416 lines (352 loc) • 14.6 kB
JavaScript
import { startTrackingWebVitals, startTrackingINP, startTrackingLongAnimationFrames, startTrackingLongTasks, startTrackingInteractions, addHistoryInstrumentationHandler, registerInpInteractionListener, addPerformanceEntries } from '@sentry-internal/browser-utils';
import { TRACING_DEFAULTS, registerSpanErrorInstrumentation, GLOBAL_OBJ, getLocationHref, getClient, getIsolationScope, generateTraceId, getCurrentScope, propagationContextFromHeaders, getRootSpan, browserPerformanceTimeOrigin, spanToJSON, logger, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, startIdleSpan, getDynamicSamplingContextFromSpan, spanIsSampled, addNonEnumerableProperty, consoleSandbox, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON } from '@sentry/core';
import { DEBUG_BUILD } from '../debug-build.js';
import { WINDOW } from '../helpers.js';
import { registerBackgroundTabDetection } from './backgroundtab.js';
import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request.js';
import { addPreviousTraceSpanLink, getPreviousTraceFromSessionStorage, storePreviousTraceInSessionStorage } from './previousTrace.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,
linkPreviousTrace: 'in-memory',
_experiments: {},
...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) {
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 = WINDOW.document ;
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 = startTrackingWebVitals({ recordClsStandaloneSpans: enableStandaloneClsSpans || false });
if (enableInp) {
startTrackingINP();
}
if (
enableLongAnimationFrame &&
GLOBAL_OBJ.PerformanceObserver &&
PerformanceObserver.supportedEntryTypes &&
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 });
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 = getCurrentScope();
const oldPropagationContext = scope.getPropagationContext();
scope.setPropagationContext({
...oldPropagationContext,
traceId: idleSpan.spanContext().traceId,
sampled: spanIsSampled(idleSpan),
dsc: 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 = getLocationHref();
function maybeEndActiveSpan() {
const activeSpan = getActiveIdleSpan(client);
if (activeSpan && !spanToJSON(activeSpan).timestamp) {
DEBUG_BUILD && logger.log(`[Tracing] Finishing current active span with op: ${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 (getClient() !== client) {
return;
}
maybeEndActiveSpan();
getIsolationScope().setPropagationContext({ traceId: generateTraceId(), sampleRand: Math.random() });
getCurrentScope().setPropagationContext({ traceId: generateTraceId(), sampleRand: Math.random() });
_createRouteSpan(client, {
op: 'navigation',
...startSpanOptions,
});
});
client.on('startPageLoadSpan', (startSpanOptions, traceOptions = {}) => {
if (getClient() !== client) {
return;
}
maybeEndActiveSpan();
const sentryTrace = traceOptions.sentryTrace || getMetaContent('sentry-trace');
const baggage = traceOptions.baggage || getMetaContent('baggage');
const propagationContext = propagationContextFromHeaders(sentryTrace, baggage);
getCurrentScope().setPropagationContext(propagationContext);
_createRouteSpan(client, {
op: 'pageload',
...startSpanOptions,
});
});
if (linkPreviousTrace !== 'off') {
let inMemoryPreviousTraceInfo = undefined;
client.on('spanStart', span => {
if (getRootSpan(span) !== span) {
return;
}
if (linkPreviousTrace === 'session-storage') {
const updatedPreviousTraceInfo = addPreviousTraceSpanLink(getPreviousTraceFromSessionStorage(), span);
storePreviousTraceInSessionStorage(updatedPreviousTraceInfo);
} else {
inMemoryPreviousTraceInfo = addPreviousTraceSpanLink(inMemoryPreviousTraceInfo, span);
}
});
}
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 / 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?.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(client, idleTimeout, finalTimeout, childSpanTimeout, latestRoute);
}
if (enableInp) {
registerInpInteractionListener();
}
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);
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);
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 = 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 = 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 &&
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 (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) {
addNonEnumerableProperty(client, ACTIVE_IDLE_SPAN_PROPERTY, span);
}
export { BROWSER_TRACING_INTEGRATION_ID, browserTracingIntegration, getMetaContent, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan };
//# sourceMappingURL=browserTracingIntegration.js.map