@sentry/core
Version:
Base implementation for all Sentry JavaScript SDKs
264 lines (221 loc) • 8.69 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const currentScopes = require('./currentScopes.js');
const semanticAttributes = require('./semanticAttributes.js');
require('./tracing/errors.js');
const is = require('./utils-hoist/is.js');
require('./utils-hoist/debug-build.js');
require('./utils-hoist/logger.js');
require('./debug-build.js');
const hasSpansEnabled = require('./utils/hasSpansEnabled.js');
const spanUtils = require('./utils/spanUtils.js');
require('./utils-hoist/time.js');
const baggage = require('./utils-hoist/baggage.js');
const sentryNonRecordingSpan = require('./tracing/sentryNonRecordingSpan.js');
const spanstatus = require('./tracing/spanstatus.js');
const trace = require('./tracing/trace.js');
const traceData = require('./utils/traceData.js');
const url = require('./utils-hoist/url.js');
/**
* Create and track fetch request spans for usage in combination with `addFetchInstrumentationHandler`.
*
* @returns Span if a span was created, otherwise void.
*/
function instrumentFetchRequest(
handlerData,
shouldCreateSpan,
shouldAttachHeaders,
spans,
spanOrigin = 'auto.http.browser',
) {
if (!handlerData.fetchData) {
return undefined;
}
const { method, url } = handlerData.fetchData;
const shouldCreateSpanResult = hasSpansEnabled.hasSpansEnabled() && shouldCreateSpan(url);
if (handlerData.endTimestamp && shouldCreateSpanResult) {
const spanId = handlerData.fetchData.__span;
if (!spanId) return;
const span = spans[spanId];
if (span) {
endSpan(span, handlerData);
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete spans[spanId];
}
return undefined;
}
const hasParent = !!spanUtils.getActiveSpan();
const span =
shouldCreateSpanResult && hasParent
? trace.startInactiveSpan(getSpanStartOptions(url, method, spanOrigin))
: new sentryNonRecordingSpan.SentryNonRecordingSpan();
handlerData.fetchData.__span = span.spanContext().spanId;
spans[span.spanContext().spanId] = span;
if (shouldAttachHeaders(handlerData.fetchData.url)) {
const request = handlerData.args[0];
const options = handlerData.args[1] || {};
const headers = _addTracingHeadersToFetchRequest(
request,
options,
// If performance is disabled (TWP) or there's no active root span (pageload/navigation/interaction),
// we do not want to use the span as base for the trace headers,
// which means that the headers will be generated from the scope and the sampling decision is deferred
hasSpansEnabled.hasSpansEnabled() && hasParent ? span : undefined,
);
if (headers) {
// Ensure this is actually set, if no options have been passed previously
handlerData.args[1] = options;
options.headers = headers;
}
}
const client = currentScopes.getClient();
if (client) {
const fetchHint = {
input: handlerData.args,
response: handlerData.response,
startTimestamp: handlerData.startTimestamp,
endTimestamp: handlerData.endTimestamp,
} ;
client.emit('beforeOutgoingRequestSpan', span, fetchHint);
}
return span;
}
/**
* Adds sentry-trace and baggage headers to the various forms of fetch headers.
* exported only for testing purposes
*
* When we determine if we should add a baggage header, there are 3 cases:
* 1. No previous baggage header -> add baggage
* 2. Previous baggage header has no sentry baggage values -> add our baggage
* 3. Previous baggage header has sentry baggage values -> do nothing (might have been added manually by users)
*/
// eslint-disable-next-line complexity -- yup it's this complicated :(
function _addTracingHeadersToFetchRequest(
request,
fetchOptionsObj
,
span,
) {
const traceHeaders = traceData.getTraceData({ span });
const sentryTrace = traceHeaders['sentry-trace'];
const baggage = traceHeaders.baggage;
// Nothing to do, when we return undefined here, the original headers will be used
if (!sentryTrace) {
return undefined;
}
const originalHeaders = fetchOptionsObj.headers || (is.isRequest(request) ? request.headers : undefined);
if (!originalHeaders) {
return { ...traceHeaders };
} else if (isHeaders(originalHeaders)) {
const newHeaders = new Headers(originalHeaders);
// We don't want to override manually added sentry headers
if (!newHeaders.get('sentry-trace')) {
newHeaders.set('sentry-trace', sentryTrace);
}
if (baggage) {
const prevBaggageHeader = newHeaders.get('baggage');
if (!prevBaggageHeader) {
newHeaders.set('baggage', baggage);
} else if (!baggageHeaderHasSentryBaggageValues(prevBaggageHeader)) {
newHeaders.set('baggage', `${prevBaggageHeader},${baggage}`);
}
}
return newHeaders;
} else if (Array.isArray(originalHeaders)) {
const newHeaders = [...originalHeaders];
if (!originalHeaders.find(header => header[0] === 'sentry-trace')) {
newHeaders.push(['sentry-trace', sentryTrace]);
}
const prevBaggageHeaderWithSentryValues = originalHeaders.find(
header => header[0] === 'baggage' && baggageHeaderHasSentryBaggageValues(header[1]),
);
if (baggage && !prevBaggageHeaderWithSentryValues) {
// 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', baggage]);
}
return newHeaders ;
} else {
const existingSentryTraceHeader = 'sentry-trace' in originalHeaders ? originalHeaders['sentry-trace'] : undefined;
const existingBaggageHeader = 'baggage' in originalHeaders ? originalHeaders.baggage : undefined;
const newBaggageHeaders = existingBaggageHeader
? Array.isArray(existingBaggageHeader)
? [...existingBaggageHeader]
: [existingBaggageHeader]
: [];
const prevBaggageHeaderWithSentryValues =
existingBaggageHeader &&
(Array.isArray(existingBaggageHeader)
? existingBaggageHeader.find(headerItem => baggageHeaderHasSentryBaggageValues(headerItem))
: baggageHeaderHasSentryBaggageValues(existingBaggageHeader));
if (baggage && !prevBaggageHeaderWithSentryValues) {
newBaggageHeaders.push(baggage);
}
return {
...(originalHeaders ),
'sentry-trace': (existingSentryTraceHeader ) ?? sentryTrace,
baggage: newBaggageHeaders.length > 0 ? newBaggageHeaders.join(',') : undefined,
};
}
}
function endSpan(span, handlerData) {
if (handlerData.response) {
spanstatus.setHttpStatus(span, handlerData.response.status);
const contentLength = handlerData.response?.headers && handlerData.response.headers.get('content-length');
if (contentLength) {
const contentLengthNum = parseInt(contentLength);
if (contentLengthNum > 0) {
span.setAttribute('http.response_content_length', contentLengthNum);
}
}
} else if (handlerData.error) {
span.setStatus({ code: spanstatus.SPAN_STATUS_ERROR, message: 'internal_error' });
}
span.end();
}
function baggageHeaderHasSentryBaggageValues(baggageHeader) {
return baggageHeader.split(',').some(baggageEntry => baggageEntry.trim().startsWith(baggage.SENTRY_BAGGAGE_KEY_PREFIX));
}
function isHeaders(headers) {
return typeof Headers !== 'undefined' && is.isInstanceOf(headers, Headers);
}
function getSpanStartOptions(
url$1,
method,
spanOrigin,
) {
const parsedUrl = url.parseStringToURLObject(url$1);
return {
name: parsedUrl ? `${method} ${url.getSanitizedUrlStringFromUrlObject(parsedUrl)}` : method,
attributes: getFetchSpanAttributes(url$1, parsedUrl, method, spanOrigin),
};
}
function getFetchSpanAttributes(
url$1,
parsedUrl,
method,
spanOrigin,
) {
const attributes = {
url: url$1,
type: 'fetch',
'http.method': method,
[semanticAttributes.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin,
[semanticAttributes.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client',
};
if (parsedUrl) {
if (!url.isURLObjectRelative(parsedUrl)) {
attributes['http.url'] = parsedUrl.href;
attributes['server.address'] = parsedUrl.host;
}
if (parsedUrl.search) {
attributes['http.query'] = parsedUrl.search;
}
if (parsedUrl.hash) {
attributes['http.fragment'] = parsedUrl.hash;
}
}
return attributes;
}
exports._addTracingHeadersToFetchRequest = _addTracingHeadersToFetchRequest;
exports.instrumentFetchRequest = instrumentFetchRequest;
//# sourceMappingURL=fetch.js.map