@sentry/node
Version:
Official Sentry SDK for Node.js
281 lines (244 loc) • 10.4 kB
JavaScript
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