forked from off-topic/apps.apple.com
336 lines
11 KiB
JavaScript
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
|