import { _optionalChain } from '@sentry/utils'; import { defineIntegration, getClient, isSentryRequestUrl, getCurrentScope, getIsolationScope, getActiveSpan, spanToTraceHeader, getDynamicSamplingContextFromSpan, getDynamicSamplingContextFromClient, setHttpStatus, spanToJSON, hasTracingEnabled, getCurrentHub, addBreadcrumb } from '@sentry/core'; import { dropUndefinedKeys, logger, fill, LRUMap, generateSentryTraceHeader, dynamicSamplingContextToSentryBaggageHeader, stringMatchesSomePattern } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build.js'; import { NODE_VERSION } from '../nodeVersion.js'; import { normalizeRequestArgs, extractRawUrl, extractUrl, cleanSpanDescription } from './utils/http.js'; const _httpIntegration = ((options = {}) => { const { breadcrumbs, tracing, shouldCreateSpanForRequest } = options; const convertedOptions = { breadcrumbs, tracing: tracing === false ? false : dropUndefinedKeys({ // If tracing is forced to `true`, we don't want to set `enableIfHasTracingEnabled` enableIfHasTracingEnabled: tracing === true ? undefined : true, shouldCreateSpanForRequest, }), }; // eslint-disable-next-line deprecation/deprecation return new Http(convertedOptions) ; }) ; /** * The http module integration instruments Node's internal http module. It creates breadcrumbs, spans for outgoing * http requests, and attaches trace data when tracing is enabled via its `tracing` option. * * By default, this will always create breadcrumbs, and will create spans if tracing is enabled. */ const httpIntegration = defineIntegration(_httpIntegration); /** * The http module integration instruments Node's internal http module. It creates breadcrumbs, transactions for outgoing * http requests and attaches trace data when tracing is enabled via its `tracing` option. * * @deprecated Use `httpIntegration()` instead. */ class Http { /** * @inheritDoc */ static __initStatic() {this.id = 'Http';} /** * @inheritDoc */ // eslint-disable-next-line deprecation/deprecation __init() {this.name = Http.id;} /** * @inheritDoc */ constructor(options = {}) {Http.prototype.__init.call(this); this._breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs; this._tracing = !options.tracing ? undefined : options.tracing === true ? {} : options.tracing; } /** * @inheritDoc */ setupOnce( _addGlobalEventProcessor, // eslint-disable-next-line deprecation/deprecation setupOnceGetCurrentHub, ) { // eslint-disable-next-line deprecation/deprecation const clientOptions = _optionalChain([setupOnceGetCurrentHub, 'call', _ => _(), 'access', _2 => _2.getClient, 'call', _3 => _3(), 'optionalAccess', _4 => _4.getOptions, 'call', _5 => _5()]); // If `tracing` is not explicitly set, we default this based on whether or not tracing is enabled. // But for compatibility, we only do that if `enableIfHasTracingEnabled` is set. const shouldCreateSpans = _shouldCreateSpans(this._tracing, clientOptions); // No need to instrument if we don't want to track anything if (!this._breadcrumbs && !shouldCreateSpans) { return; } // Do not auto-instrument for other instrumenter if (clientOptions && clientOptions.instrumenter !== 'sentry') { DEBUG_BUILD && logger.log('HTTP Integration is skipped because of instrumenter configuration.'); return; } const shouldCreateSpanForRequest = _getShouldCreateSpanForRequest(shouldCreateSpans, this._tracing, clientOptions); // eslint-disable-next-line deprecation/deprecation const tracePropagationTargets = _optionalChain([clientOptions, 'optionalAccess', _6 => _6.tracePropagationTargets]) || _optionalChain([this, 'access', _7 => _7._tracing, 'optionalAccess', _8 => _8.tracePropagationTargets]); // eslint-disable-next-line @typescript-eslint/no-var-requires const httpModule = require('http'); const wrappedHttpHandlerMaker = _createWrappedRequestMethodFactory( httpModule, this._breadcrumbs, shouldCreateSpanForRequest, tracePropagationTargets, ); fill(httpModule, 'get', wrappedHttpHandlerMaker); fill(httpModule, 'request', wrappedHttpHandlerMaker); // NOTE: Prior to Node 9, `https` used internals of `http` module, thus we don't patch it. // If we do, we'd get double breadcrumbs and double spans for `https` calls. // It has been changed in Node 9, so for all versions equal and above, we patch `https` separately. if (NODE_VERSION.major > 8) { // eslint-disable-next-line @typescript-eslint/no-var-requires const httpsModule = require('https'); const wrappedHttpsHandlerMaker = _createWrappedRequestMethodFactory( httpsModule, this._breadcrumbs, shouldCreateSpanForRequest, tracePropagationTargets, ); fill(httpsModule, 'get', wrappedHttpsHandlerMaker); fill(httpsModule, 'request', wrappedHttpsHandlerMaker); } } }Http.__initStatic(); // for ease of reading below /** * Function which creates a function which creates wrapped versions of internal `request` and `get` calls within `http` * and `https` modules. (NB: Not a typo - this is a creator^2!) * * @param breadcrumbsEnabled Whether or not to record outgoing requests as breadcrumbs * @param tracingEnabled Whether or not to record outgoing requests as tracing spans * * @returns A function which accepts the exiting handler and returns a wrapped handler */ function _createWrappedRequestMethodFactory( httpModule, breadcrumbsEnabled, shouldCreateSpanForRequest, tracePropagationTargets, ) { // We're caching results so we don't have to recompute regexp every time we create a request. const createSpanUrlMap = new LRUMap(100); const headersUrlMap = new LRUMap(100); const shouldCreateSpan = (url) => { if (shouldCreateSpanForRequest === undefined) { return true; } const cachedDecision = createSpanUrlMap.get(url); if (cachedDecision !== undefined) { return cachedDecision; } const decision = shouldCreateSpanForRequest(url); createSpanUrlMap.set(url, decision); return decision; }; const shouldAttachTraceData = (url) => { if (tracePropagationTargets === undefined) { return true; } const cachedDecision = headersUrlMap.get(url); if (cachedDecision !== undefined) { return cachedDecision; } const decision = stringMatchesSomePattern(url, tracePropagationTargets); headersUrlMap.set(url, decision); return decision; }; /** * Captures Breadcrumb based on provided request/response pair */ function addRequestBreadcrumb( event, requestSpanData, req, res, ) { // eslint-disable-next-line deprecation/deprecation if (!getCurrentHub().getIntegration(Http)) { return; } addBreadcrumb( { category: 'http', data: { status_code: res && res.statusCode, ...requestSpanData, }, type: 'http', }, { event, request: req, response: res, }, ); } return function wrappedRequestMethodFactory(originalRequestMethod) { return function wrappedMethod( ...args) { const requestArgs = normalizeRequestArgs(httpModule, args); const requestOptions = requestArgs[0]; // eslint-disable-next-line deprecation/deprecation const rawRequestUrl = extractRawUrl(requestOptions); const requestUrl = extractUrl(requestOptions); const client = getClient(); // we don't want to record requests to Sentry as either breadcrumbs or spans, so just use the original method if (isSentryRequestUrl(requestUrl, client)) { return originalRequestMethod.apply(httpModule, requestArgs); } const scope = getCurrentScope(); const isolationScope = getIsolationScope(); const parentSpan = getActiveSpan(); const data = getRequestSpanData(requestUrl, requestOptions); const requestSpan = shouldCreateSpan(rawRequestUrl) ? // eslint-disable-next-line deprecation/deprecation _optionalChain([parentSpan, 'optionalAccess', _9 => _9.startChild, 'call', _10 => _10({ op: 'http.client', origin: 'auto.http.node.http', description: `${data['http.method']} ${data.url}`, data, })]) : undefined; if (client && shouldAttachTraceData(rawRequestUrl)) { const { traceId, spanId, sampled, dsc } = { ...isolationScope.getPropagationContext(), ...scope.getPropagationContext(), }; const sentryTraceHeader = requestSpan ? spanToTraceHeader(requestSpan) : generateSentryTraceHeader(traceId, spanId, sampled); const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader( dsc || (requestSpan ? getDynamicSamplingContextFromSpan(requestSpan) : getDynamicSamplingContextFromClient(traceId, client, scope)), ); addHeadersToRequestOptions(requestOptions, requestUrl, sentryTraceHeader, sentryBaggageHeader); } else { DEBUG_BUILD && logger.log( `[Tracing] Not adding sentry-trace header to outgoing request (${requestUrl}) due to mismatching tracePropagationTargets option.`, ); } // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return originalRequestMethod .apply(httpModule, requestArgs) .once('response', function ( res) { // eslint-disable-next-line @typescript-eslint/no-this-alias const req = this; if (breadcrumbsEnabled) { addRequestBreadcrumb('response', data, req, res); } if (requestSpan) { if (res.statusCode) { setHttpStatus(requestSpan, res.statusCode); } requestSpan.updateName( cleanSpanDescription(spanToJSON(requestSpan).description || '', requestOptions, req) || '', ); requestSpan.end(); } }) .once('error', function () { // eslint-disable-next-line @typescript-eslint/no-this-alias const req = this; if (breadcrumbsEnabled) { addRequestBreadcrumb('error', data, req); } if (requestSpan) { setHttpStatus(requestSpan, 500); requestSpan.updateName( cleanSpanDescription(spanToJSON(requestSpan).description || '', requestOptions, req) || '', ); requestSpan.end(); } }); }; }; } function addHeadersToRequestOptions( requestOptions, requestUrl, sentryTraceHeader, sentryBaggageHeader, ) { // Don't overwrite sentry-trace and baggage header if it's already set. const headers = requestOptions.headers || {}; if (headers['sentry-trace']) { return; } DEBUG_BUILD && logger.log(`[Tracing] Adding sentry-trace header ${sentryTraceHeader} to outgoing request to "${requestUrl}": `); requestOptions.headers = { ...requestOptions.headers, 'sentry-trace': sentryTraceHeader, // Setting a header to `undefined` will crash in node so we only set the baggage header when it's defined ...(sentryBaggageHeader && sentryBaggageHeader.length > 0 && { baggage: normalizeBaggageHeader(requestOptions, sentryBaggageHeader) }), }; } function getRequestSpanData(requestUrl, requestOptions) { const method = requestOptions.method || 'GET'; const data = { url: requestUrl, 'http.method': method, }; if (requestOptions.hash) { // strip leading "#" data['http.fragment'] = requestOptions.hash.substring(1); } if (requestOptions.search) { // strip leading "?" data['http.query'] = requestOptions.search.substring(1); } return data; } function normalizeBaggageHeader( requestOptions, sentryBaggageHeader, ) { if (!requestOptions.headers || !requestOptions.headers.baggage) { return sentryBaggageHeader; } else if (!sentryBaggageHeader) { return requestOptions.headers.baggage ; } else if (Array.isArray(requestOptions.headers.baggage)) { return [...requestOptions.headers.baggage, sentryBaggageHeader]; } // Type-cast explanation: // Technically this the following could be of type `(number | string)[]` but for the sake of simplicity // we say this is undefined behaviour, since it would not be baggage spec conform if the user did this. return [requestOptions.headers.baggage, sentryBaggageHeader] ; } /** Exported for tests only. */ function _shouldCreateSpans( tracingOptions, clientOptions, ) { return tracingOptions === undefined ? false : tracingOptions.enableIfHasTracingEnabled ? hasTracingEnabled(clientOptions) : true; } /** Exported for tests only. */ function _getShouldCreateSpanForRequest( shouldCreateSpans, tracingOptions, clientOptions, ) { const handler = shouldCreateSpans ? // eslint-disable-next-line deprecation/deprecation _optionalChain([tracingOptions, 'optionalAccess', _11 => _11.shouldCreateSpanForRequest]) || _optionalChain([clientOptions, 'optionalAccess', _12 => _12.shouldCreateSpanForRequest]) : () => false; return handler; } export { Http, _getShouldCreateSpanForRequest, _shouldCreateSpans, httpIntegration }; //# sourceMappingURL=http.js.map