Files
2025-11-04 05:03:50 +08:00

336 lines
11 KiB
JavaScript

import { _optionalChain } from '@sentry/utils/esm/buildPolyfills';
import { hasTracingEnabled, getCurrentHub } from '@sentry/core';
import { addInstrumentationHandler, browserPerformanceTimeOrigin, dynamicSamplingContextToSentryBaggageHeader, isInstanceOf, BAGGAGE_HEADER_NAME, SENTRY_XHR_DATA_KEY, stringMatchesSomePattern } from '@sentry/utils';
/* eslint-disable max-lines */
const DEFAULT_TRACE_PROPAGATION_TARGETS = ['localhost', /^\/(?!\/)/];
/** Options for Request Instrumentation */
const defaultRequestInstrumentationOptions = {
traceFetch: true,
traceXHR: true,
// TODO (v8): Remove this property
tracingOrigins: DEFAULT_TRACE_PROPAGATION_TARGETS,
tracePropagationTargets: DEFAULT_TRACE_PROPAGATION_TARGETS,
_experiments: {},
};
/** Registers span creators for xhr and fetch requests */
function instrumentOutgoingRequests(_options) {
// eslint-disable-next-line deprecation/deprecation
const { traceFetch, traceXHR, tracePropagationTargets, tracingOrigins, shouldCreateSpanForRequest, _experiments } = {
traceFetch: defaultRequestInstrumentationOptions.traceFetch,
traceXHR: defaultRequestInstrumentationOptions.traceXHR,
..._options,
};
const shouldCreateSpan =
typeof shouldCreateSpanForRequest === 'function' ? shouldCreateSpanForRequest : (_) => true;
// TODO(v8) Remove tracingOrigins here
// The only reason we're passing it in here is because this instrumentOutgoingRequests function is publicly exported
// and we don't want to break the API. We can remove it in v8.
const shouldAttachHeadersWithTargets = (url) =>
shouldAttachHeaders(url, tracePropagationTargets || tracingOrigins);
const spans = {};
if (traceFetch) {
addInstrumentationHandler('fetch', (handlerData) => {
const createdSpan = fetchCallback(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans);
if (_optionalChain([_experiments, 'optionalAccess', _2 => _2.enableHTTPTimings]) && createdSpan) {
addHTTPTimings(createdSpan);
}
});
}
if (traceXHR) {
addInstrumentationHandler('xhr', (handlerData) => {
const createdSpan = xhrCallback(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans);
if (_optionalChain([_experiments, 'optionalAccess', _3 => _3.enableHTTPTimings]) && createdSpan) {
addHTTPTimings(createdSpan);
}
});
}
}
/**
* Creates a temporary observer to listen to the next fetch/xhr resourcing timings,
* so that when timings hit their per-browser limit they don't need to be removed.
*
* @param span A span that has yet to be finished, must contain `url` on data.
*/
function addHTTPTimings(span) {
const url = span.data.url;
const observer = new PerformanceObserver(list => {
const entries = list.getEntries() ;
entries.forEach(entry => {
if ((entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') && entry.name.endsWith(url)) {
const spanData = resourceTimingEntryToSpanData(entry);
spanData.forEach(data => span.setData(...data));
observer.disconnect();
}
});
});
observer.observe({
entryTypes: ['resource'],
});
}
function resourceTimingEntryToSpanData(resourceTiming) {
const version = resourceTiming.nextHopProtocol.split('/')[1] || 'none';
const timingSpanData = [];
if (version) {
timingSpanData.push(['network.protocol.version', version]);
}
if (!browserPerformanceTimeOrigin) {
return timingSpanData;
}
return [
...timingSpanData,
['http.request.connect_start', (browserPerformanceTimeOrigin + resourceTiming.connectStart) / 1000],
['http.request.request_start', (browserPerformanceTimeOrigin + resourceTiming.requestStart) / 1000],
['http.request.response_start', (browserPerformanceTimeOrigin + resourceTiming.responseStart) / 1000],
];
}
/**
* A function that determines whether to attach tracing headers to a request.
* This was extracted from `instrumentOutgoingRequests` to make it easier to test shouldAttachHeaders.
* We only export this fuction for testing purposes.
*/
function shouldAttachHeaders(url, tracePropagationTargets) {
return stringMatchesSomePattern(url, tracePropagationTargets || DEFAULT_TRACE_PROPAGATION_TARGETS);
}
/**
* Create and track fetch request spans
*
* @returns Span if a span was created, otherwise void.
*/
function fetchCallback(
handlerData,
shouldCreateSpan,
shouldAttachHeaders,
spans,
) {
if (!hasTracingEnabled() || !(handlerData.fetchData && shouldCreateSpan(handlerData.fetchData.url))) {
return;
}
if (handlerData.endTimestamp) {
const spanId = handlerData.fetchData.__span;
if (!spanId) return;
const span = spans[spanId];
if (span) {
if (handlerData.response) {
// TODO (kmclb) remove this once types PR goes through
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
span.setHttpStatus(handlerData.response.status);
const contentLength =
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
handlerData.response && handlerData.response.headers && handlerData.response.headers.get('content-length');
const contentLengthNum = parseInt(contentLength);
if (contentLengthNum > 0) {
span.setData('http.response_content_length', contentLengthNum);
}
} else if (handlerData.error) {
span.setStatus('internal_error');
}
span.finish();
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete spans[spanId];
}
return;
}
const currentSpan = getCurrentHub().getScope().getSpan();
const activeTransaction = currentSpan && currentSpan.transaction;
if (currentSpan && activeTransaction) {
const { method, url } = handlerData.fetchData;
const span = currentSpan.startChild({
data: {
url,
type: 'fetch',
'http.method': method,
},
description: `${method} ${url}`,
op: 'http.client',
});
handlerData.fetchData.__span = span.spanId;
spans[span.spanId] = span;
const request = handlerData.args[0];
// In case the user hasn't set the second argument of a fetch call we default it to `{}`.
handlerData.args[1] = handlerData.args[1] || {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const options = handlerData.args[1];
if (shouldAttachHeaders(handlerData.fetchData.url)) {
options.headers = addTracingHeadersToFetchRequest(
request,
activeTransaction.getDynamicSamplingContext(),
span,
options,
);
}
return span;
}
}
/**
* Adds sentry-trace and baggage headers to the various forms of fetch headers
*/
function addTracingHeadersToFetchRequest(
request, // unknown is actually type Request but we can't export DOM types from this package,
dynamicSamplingContext,
span,
options
,
) {
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
const sentryTraceHeader = span.toTraceparent();
const headers =
typeof Request !== 'undefined' && isInstanceOf(request, Request) ? (request ).headers : options.headers;
if (!headers) {
return { 'sentry-trace': sentryTraceHeader, baggage: sentryBaggageHeader };
} else if (typeof Headers !== 'undefined' && isInstanceOf(headers, Headers)) {
const newHeaders = new Headers(headers );
newHeaders.append('sentry-trace', sentryTraceHeader);
if (sentryBaggageHeader) {
// If the same header is appended multiple times the browser will merge the values into a single request header.
// Its therefore safe to simply push a "baggage" entry, even though there might already be another baggage header.
newHeaders.append(BAGGAGE_HEADER_NAME, sentryBaggageHeader);
}
return newHeaders ;
} else if (Array.isArray(headers)) {
const newHeaders = [...headers, ['sentry-trace', sentryTraceHeader]];
if (sentryBaggageHeader) {
// If there are multiple entries with the same key, the browser will merge the values into a single request header.
// Its therefore safe to simply push a "baggage" entry, even though there might already be another baggage header.
newHeaders.push([BAGGAGE_HEADER_NAME, sentryBaggageHeader]);
}
return newHeaders ;
} else {
const existingBaggageHeader = 'baggage' in headers ? headers.baggage : undefined;
const newBaggageHeaders = [];
if (Array.isArray(existingBaggageHeader)) {
newBaggageHeaders.push(...existingBaggageHeader);
} else if (existingBaggageHeader) {
newBaggageHeaders.push(existingBaggageHeader);
}
if (sentryBaggageHeader) {
newBaggageHeaders.push(sentryBaggageHeader);
}
return {
...(headers ),
'sentry-trace': sentryTraceHeader,
baggage: newBaggageHeaders.length > 0 ? newBaggageHeaders.join(',') : undefined,
};
}
}
/**
* Create and track xhr request spans
*
* @returns Span if a span was created, otherwise void.
*/
function xhrCallback(
handlerData,
shouldCreateSpan,
shouldAttachHeaders,
spans,
) {
const xhr = handlerData.xhr;
const sentryXhrData = xhr && xhr[SENTRY_XHR_DATA_KEY];
if (
!hasTracingEnabled() ||
(xhr && xhr.__sentry_own_request__) ||
!(xhr && sentryXhrData && shouldCreateSpan(sentryXhrData.url))
) {
return;
}
// check first if the request has finished and is tracked by an existing span which should now end
if (handlerData.endTimestamp) {
const spanId = xhr.__sentry_xhr_span_id__;
if (!spanId) return;
const span = spans[spanId];
if (span) {
span.setHttpStatus(sentryXhrData.status_code);
span.finish();
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete spans[spanId];
}
return;
}
const currentSpan = getCurrentHub().getScope().getSpan();
const activeTransaction = currentSpan && currentSpan.transaction;
if (currentSpan && activeTransaction) {
const span = currentSpan.startChild({
data: {
...sentryXhrData.data,
type: 'xhr',
'http.method': sentryXhrData.method,
url: sentryXhrData.url,
},
description: `${sentryXhrData.method} ${sentryXhrData.url}`,
op: 'http.client',
});
xhr.__sentry_xhr_span_id__ = span.spanId;
spans[xhr.__sentry_xhr_span_id__] = span;
if (xhr.setRequestHeader && shouldAttachHeaders(sentryXhrData.url)) {
try {
xhr.setRequestHeader('sentry-trace', span.toTraceparent());
const dynamicSamplingContext = activeTransaction.getDynamicSamplingContext();
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
if (sentryBaggageHeader) {
// From MDN: "If this method is called several times with the same header, the values are merged into one single request header."
// We can therefore simply set a baggage header without checking what was there before
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader
xhr.setRequestHeader(BAGGAGE_HEADER_NAME, sentryBaggageHeader);
}
} catch (_) {
// Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.
}
}
return span;
}
}
export { DEFAULT_TRACE_PROPAGATION_TARGETS, addTracingHeadersToFetchRequest, defaultRequestInstrumentationOptions, instrumentOutgoingRequests, shouldAttachHeaders };
//# sourceMappingURL=request.js.map