UNPKG

@sentry/node

Version:
281 lines (244 loc) 10.4 kB
import { _optionalChain } from '@sentry/utils/esm/buildPolyfills'; import { getCurrentHub } from '@sentry/core'; import { parseSemver, logger, fill, dynamicSamplingContextToSentryBaggageHeader, stringMatchesSomePattern } from '@sentry/utils'; import { LRUMap } from 'lru_map'; import { normalizeRequestArgs, extractRawUrl, extractUrl, isSentryRequest, cleanSpanDescription } from './utils/http.js'; const NODE_VERSION = parseSemver(process.versions.node); /** * 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') { (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('HTTP Integration is skipped because of instrumenter configuration.'); return; } // TODO (v8): `tracePropagationTargets` and `shouldCreateSpanForRequest` will be removed from clientOptions // and we will no longer have to do this optional merge, we can just pass `this._tracing` directly. const tracingOptions = this._tracing ? { ...clientOptions, ...this._tracing } : undefined; // eslint-disable-next-line @typescript-eslint/no-var-requires const httpModule = require('http'); const wrappedHttpHandlerMaker = _createWrappedRequestMethodFactory(this._breadcrumbs, tracingOptions, httpModule); 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( this._breadcrumbs, tracingOptions, httpsModule, ); 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( breadcrumbsEnabled, tracingOptions, httpModule, ) { // 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 = {}; const shouldCreateSpan = (url) => { if (_optionalChain([tracingOptions, 'optionalAccess', _6 => _6.shouldCreateSpanForRequest]) === undefined) { return true; } const cachedDecision = createSpanUrlMap.get(url); if (cachedDecision !== undefined) { return cachedDecision; } const decision = tracingOptions.shouldCreateSpanForRequest(url); createSpanUrlMap.set(url, decision); return decision; }; const shouldAttachTraceData = (url) => { if (_optionalChain([tracingOptions, 'optionalAccess', _7 => _7.tracePropagationTargets]) === undefined) { return true; } if (headersUrlMap[url]) { return headersUrlMap[url]; } headersUrlMap[url] = stringMatchesSomePattern(url, tracingOptions.tracePropagationTargets); return headersUrlMap[url]; }; 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 (isSentryRequest(requestUrl)) { return originalRequestMethod.apply(httpModule, requestArgs); } let requestSpan; let parentSpan; const scope = getCurrentHub().getScope(); const requestSpanData = { url: requestUrl, method: requestOptions.method || 'GET', }; if (requestOptions.hash) { // strip leading "#" requestSpanData['http.fragment'] = requestOptions.hash.substring(1); } if (requestOptions.search) { // strip leading "?" requestSpanData['http.query'] = requestOptions.search.substring(1); } if (scope && tracingOptions && shouldCreateSpan(rawRequestUrl)) { parentSpan = scope.getSpan(); if (parentSpan) { requestSpan = parentSpan.startChild({ description: `${requestSpanData.method} ${requestSpanData.url}`, op: 'http.client', data: requestSpanData, }); if (shouldAttachTraceData(rawRequestUrl)) { const sentryTraceHeader = requestSpan.toTraceparent(); (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log( `[Tracing] Adding sentry-trace header ${sentryTraceHeader} to outgoing request to "${requestUrl}": `, ); requestOptions.headers = { ...requestOptions.headers, 'sentry-trace': sentryTraceHeader, }; if (parentSpan.transaction) { const dynamicSamplingContext = parentSpan.transaction.getDynamicSamplingContext(); const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); let newBaggageHeaderField; if (!requestOptions.headers || !requestOptions.headers.baggage) { newBaggageHeaderField = sentryBaggageHeader; } else if (!sentryBaggageHeader) { newBaggageHeaderField = requestOptions.headers.baggage; } else if (Array.isArray(requestOptions.headers.baggage)) { newBaggageHeaderField = [...requestOptions.headers.baggage, sentryBaggageHeader]; } else { // 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. newBaggageHeaderField = [requestOptions.headers.baggage, sentryBaggageHeader] ; } requestOptions.headers = { ...requestOptions.headers, // Setting a hader to `undefined` will crash in node so we only set the baggage header when it's defined ...(newBaggageHeaderField && { baggage: newBaggageHeaderField }), }; } } else { (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && 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', requestSpanData, 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', requestSpanData, req); } if (requestSpan) { requestSpan.setHttpStatus(500); requestSpan.description = cleanSpanDescription(requestSpan.description, requestOptions, req); requestSpan.finish(); } }); }; }; } /** * Captures Breadcrumb based on provided request/response pair */ function addRequestBreadcrumb( event, requestSpanData, req, res, ) { if (!getCurrentHub().getIntegration(Http)) { return; } getCurrentHub().addBreadcrumb( { category: 'http', data: { method: req.method, status_code: res && res.statusCode, ...requestSpanData, }, type: 'http', }, { event, request: req, response: res, }, ); } export { Http }; //# sourceMappingURL=http.js.map