diff options
| author | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
|---|---|---|
| committer | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
| commit | bce557cc2dc767628bed6aac87301a1be7c5431b (patch) | |
| tree | b51a051228d01fe3306cd7626d4a96768aadb944 /shared/logger/node_modules/@sentry-internal/tracing/esm/browser/request.js | |
init commit
Diffstat (limited to 'shared/logger/node_modules/@sentry-internal/tracing/esm/browser/request.js')
| -rw-r--r-- | shared/logger/node_modules/@sentry-internal/tracing/esm/browser/request.js | 335 |
1 files changed, 335 insertions, 0 deletions
diff --git a/shared/logger/node_modules/@sentry-internal/tracing/esm/browser/request.js b/shared/logger/node_modules/@sentry-internal/tracing/esm/browser/request.js new file mode 100644 index 0000000..d505546 --- /dev/null +++ b/shared/logger/node_modules/@sentry-internal/tracing/esm/browser/request.js @@ -0,0 +1,335 @@ +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 |
