import { diag } from '@opentelemetry/api'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import { defineIntegration, getClient } from '@sentry/core'; import { generateInstrumentOnce } from '../../otel/instrument.js'; import { addOriginToSpan } from '../../utils/addOriginToSpan.js'; import { getRequestUrl } from '../../utils/getRequestUrl.js'; import { SentryHttpInstrumentation } from './SentryHttpInstrumentation.js'; import { SentryHttpInstrumentationBeforeOtel } from './SentryHttpInstrumentationBeforeOtel.js'; const INTEGRATION_NAME = 'Http'; const INSTRUMENTATION_NAME = '@opentelemetry_sentry-patched/instrumentation-http'; const instrumentSentryHttpBeforeOtel = generateInstrumentOnce(`${INTEGRATION_NAME}.sentry-before-otel`, () => { return new SentryHttpInstrumentationBeforeOtel(); }); const instrumentSentryHttp = generateInstrumentOnce( `${INTEGRATION_NAME}.sentry`, options => { return new SentryHttpInstrumentation(options); }, ); const instrumentOtelHttp = generateInstrumentOnce(INTEGRATION_NAME, config => { const instrumentation = new HttpInstrumentation(config); // We want to update the logger namespace so we can better identify what is happening here try { instrumentation['_diag'] = diag.createComponentLogger({ namespace: INSTRUMENTATION_NAME, }); // @ts-expect-error We are writing a read-only property here... instrumentation.instrumentationName = INSTRUMENTATION_NAME; } catch { // ignore errors here... } return instrumentation; }); /** Exported only for tests. */ function _shouldInstrumentSpans(options, clientOptions = {}) { // If `spans` is passed in, it takes precedence // Else, we by default emit spans, unless `skipOpenTelemetrySetup` is set to `true` return typeof options.spans === 'boolean' ? options.spans : !clientOptions.skipOpenTelemetrySetup; } /** * The http integration instruments Node's internal http and https modules. * It creates breadcrumbs and spans for outgoing HTTP requests which will be attached to the currently active span. */ const httpIntegration = defineIntegration((options = {}) => { return { name: INTEGRATION_NAME, setupOnce() { // Below, we instrument the Node.js HTTP API three times. 2 times Sentry-specific, 1 time OTEL specific. // Due to timing reasons, we sometimes need to apply Sentry instrumentation _before_ we apply the OTEL // instrumentation (e.g. to flush on serverless platforms), and sometimes we need to apply Sentry instrumentation // _after_ we apply OTEL instrumentation (e.g. for isolation scope handling and breadcrumbs). // This is Sentry-specific instrumentation that is applied _before_ any OTEL instrumentation. if (process.env.VERCEL) { // Currently this instrumentation only does something when deployed on Vercel, so to save some overhead, we short circuit adding it here only for Vercel. // If it's functionality is extended in the future, feel free to remove the if statement and this comment. instrumentSentryHttpBeforeOtel(); } const instrumentSpans = _shouldInstrumentSpans(options, getClient()?.getOptions()); // This is the "regular" OTEL instrumentation that emits spans if (instrumentSpans) { const instrumentationConfig = getConfigWithDefaults(options); instrumentOtelHttp(instrumentationConfig); } // This is Sentry-specific instrumentation that is applied _after_ any OTEL instrumentation. instrumentSentryHttp({ ...options, // If spans are not instrumented, it means the HttpInstrumentation has not been added // In that case, we want to handle incoming trace extraction ourselves extractIncomingTraceFromHeader: !instrumentSpans, }); }, }; }); /** * Determines if @param req is a ClientRequest, meaning the request was created within the express app * and it's an outgoing request. * Checking for properties instead of using `instanceOf` to avoid importing the request classes. */ function _isClientRequest(req) { return 'outputData' in req && 'outputSize' in req && !('client' in req) && !('statusCode' in req); } /** * Detects if an incoming request is a prefetch request. */ function isKnownPrefetchRequest(req) { // Currently only handles Next.js prefetch requests but may check other frameworks in the future. return req.headers['next-router-prefetch'] === '1'; } function getConfigWithDefaults(options = {}) { const instrumentationConfig = { ...options.instrumentation?._experimentalConfig, disableIncomingRequestInstrumentation: options.disableIncomingRequestSpans, ignoreOutgoingRequestHook: request => { const url = getRequestUrl(request); if (!url) { return false; } const _ignoreOutgoingRequests = options.ignoreOutgoingRequests; if (_ignoreOutgoingRequests?.(url, request)) { return true; } return false; }, ignoreIncomingRequestHook: request => { // request.url is the only property that holds any information about the url // it only consists of the URL path and query string (if any) const urlPath = request.url; const method = request.method?.toUpperCase(); // We do not capture OPTIONS/HEAD requests as transactions if (method === 'OPTIONS' || method === 'HEAD') { return true; } const _ignoreIncomingRequests = options.ignoreIncomingRequests; if (urlPath && _ignoreIncomingRequests?.(urlPath, request)) { return true; } return false; }, requireParentforOutgoingSpans: false, requireParentforIncomingSpans: false, requestHook: (span, req) => { addOriginToSpan(span, 'auto.http.otel.http'); if (!_isClientRequest(req) && isKnownPrefetchRequest(req)) { span.setAttribute('sentry.http.prefetch', true); } options.instrumentation?.requestHook?.(span, req); }, responseHook: (span, res) => { options.instrumentation?.responseHook?.(span, res); }, applyCustomAttributesOnSpan: ( span, request, response, ) => { options.instrumentation?.applyCustomAttributesOnSpan?.(span, request, response); }, } ; return instrumentationConfig; } export { _shouldInstrumentSpans, httpIntegration, instrumentOtelHttp }; //# sourceMappingURL=index.js.map