UNPKG

@sentry/node

Version:
311 lines (277 loc) 11.4 kB
import { _optionalChain } from '@sentry/utils'; import { isSentryRequestUrl, getCurrentHub, getDynamicSamplingContextFromClient } from '@sentry/core'; import { 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'; /** * 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. */ class Http { /** * @inheritDoc */ static __initStatic() {this.id = 'Http';} /** * @inheritDoc */ __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, setupOnceGetCurrentHub, ) { // No need to instrument if we don't want to track anything if (!this._breadcrumbs && !this._tracing) { return; } const clientOptions = _optionalChain([setupOnceGetCurrentHub, 'call', _ => _(), 'access', _2 => _2.getClient, 'call', _3 => _3(), 'optionalAccess', _4 => _4.getOptions, 'call', _5 => _5()]); // 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 = // eslint-disable-next-line deprecation/deprecation _optionalChain([this, 'access', _6 => _6._tracing, 'optionalAccess', _7 => _7.shouldCreateSpanForRequest]) || _optionalChain([clientOptions, 'optionalAccess', _8 => _8.shouldCreateSpanForRequest]); // eslint-disable-next-line deprecation/deprecation const tracePropagationTargets = _optionalChain([clientOptions, 'optionalAccess', _9 => _9.tracePropagationTargets]) || _optionalChain([this, 'access', _10 => _10._tracing, 'optionalAccess', _11 => _11.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 && 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, ) { if (!getCurrentHub().getIntegration(Http)) { return; } getCurrentHub().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); // we don't want to record requests to Sentry as either breadcrumbs or spans, so just use the original method if (isSentryRequestUrl(requestUrl, getCurrentHub())) { return originalRequestMethod.apply(httpModule, requestArgs); } const hub = getCurrentHub(); const scope = hub.getScope(); const parentSpan = scope.getSpan(); const data = getRequestSpanData(requestUrl, requestOptions); const requestSpan = shouldCreateSpan(rawRequestUrl) ? _optionalChain([parentSpan, 'optionalAccess', _12 => _12.startChild, 'call', _13 => _13({ op: 'http.client', origin: 'auto.http.node.http', description: `${data['http.method']} ${data.url}`, data, })]) : undefined; if (shouldAttachTraceData(rawRequestUrl)) { if (requestSpan) { const sentryTraceHeader = requestSpan.toTraceparent(); const dynamicSamplingContext = _optionalChain([requestSpan, 'optionalAccess', _14 => _14.transaction, 'optionalAccess', _15 => _15.getDynamicSamplingContext, 'call', _16 => _16()]); addHeadersToRequestOptions(requestOptions, requestUrl, sentryTraceHeader, dynamicSamplingContext); } else { const client = hub.getClient(); const { traceId, sampled, dsc } = scope.getPropagationContext(); const sentryTraceHeader = generateSentryTraceHeader(traceId, undefined, sampled); const dynamicSamplingContext = dsc || (client ? getDynamicSamplingContextFromClient(traceId, client, scope) : undefined); addHeadersToRequestOptions(requestOptions, requestUrl, sentryTraceHeader, dynamicSamplingContext); } } 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) { requestSpan.setHttpStatus(res.statusCode); } requestSpan.description = cleanSpanDescription(requestSpan.description, requestOptions, req); requestSpan.finish(); } }) .once('error', function () { // eslint-disable-next-line @typescript-eslint/no-this-alias const req = this; if (breadcrumbsEnabled) { addRequestBreadcrumb('error', data, req); } if (requestSpan) { requestSpan.setHttpStatus(500); requestSpan.description = cleanSpanDescription(requestSpan.description, requestOptions, req); requestSpan.finish(); } }); }; }; } function addHeadersToRequestOptions( requestOptions, requestUrl, sentryTraceHeader, dynamicSamplingContext, ) { // 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}": `); const sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); const sentryBaggageHeader = sentryBaggage && sentryBaggage.length > 0 ? normalizeBaggageHeader(requestOptions, sentryBaggage) : undefined; 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 && { baggage: 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] ; } export { Http }; //# sourceMappingURL=http.js.map