diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 151fa384b9f9..96f6c3f9dbd7 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -1,5 +1,5 @@ import { getCurrentHub, Hub } from '@sentry/core'; -import { EventProcessor, Integration, Span, TracePropagationTargets } from '@sentry/types'; +import { EventProcessor, Integration, Span } from '@sentry/types'; import { dynamicSamplingContextToSentryBaggageHeader, fill, @@ -10,6 +10,7 @@ import { import * as http from 'http'; import * as https from 'https'; +import { NodeClient } from '../client'; import { NodeClientOptions } from '../types'; import { cleanSpanDescription, @@ -67,13 +68,8 @@ export class Http implements Integration { return; } - const clientOptions = setupOnceGetCurrentHub().getClient()?.getOptions() as NodeClientOptions | undefined; - - const wrappedHandlerMaker = _createWrappedRequestMethodFactory( - this._breadcrumbs, - this._tracing, - clientOptions?.tracePropagationTargets, - ); + const clientOptions = setupOnceGetCurrentHub().getClient()?.getOptions(); + const wrappedHandlerMaker = _createWrappedRequestMethodFactory(this._breadcrumbs, this._tracing, clientOptions); // eslint-disable-next-line @typescript-eslint/no-var-requires const httpModule = require('http'); @@ -109,24 +105,40 @@ type WrappedRequestMethodFactory = (original: OriginalRequestMethod) => WrappedR function _createWrappedRequestMethodFactory( breadcrumbsEnabled: boolean, tracingEnabled: boolean, - tracePropagationTargets: TracePropagationTargets | undefined, + options: NodeClientOptions | undefined, ): WrappedRequestMethodFactory { - // We're caching results so we dont have to recompute regexp everytime we create a request. - const urlMap: Record = {}; + // We're caching results so we don't have to recompute regexp every time we create a request. + const createSpanUrlMap: Record = {}; + const headersUrlMap: Record = {}; + + const shouldCreateSpan = (url: string): boolean => { + if (options?.shouldCreateSpanForRequest === undefined) { + return true; + } + + if (createSpanUrlMap[url]) { + return createSpanUrlMap[url]; + } + + createSpanUrlMap[url] = options.shouldCreateSpanForRequest(url); + + return createSpanUrlMap[url]; + }; + const shouldAttachTraceData = (url: string): boolean => { - if (tracePropagationTargets === undefined) { + if (options?.tracePropagationTargets === undefined) { return true; } - if (urlMap[url]) { - return urlMap[url]; + if (headersUrlMap[url]) { + return headersUrlMap[url]; } - urlMap[url] = tracePropagationTargets.some(tracePropagationTarget => + headersUrlMap[url] = options.tracePropagationTargets.some(tracePropagationTarget => isMatchingPattern(url, tracePropagationTarget), ); - return urlMap[url]; + return headersUrlMap[url]; }; return function wrappedRequestMethodFactory(originalRequestMethod: OriginalRequestMethod): WrappedRequestMethod { @@ -148,7 +160,7 @@ function _createWrappedRequestMethodFactory( const scope = getCurrentHub().getScope(); - if (scope && tracingEnabled) { + if (scope && tracingEnabled && shouldCreateSpan(requestUrl)) { parentSpan = scope.getSpan(); if (parentSpan) { diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index 4d890d8e10b8..59c4c9af7ec7 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -19,6 +19,12 @@ export interface BaseNodeOptions { */ tracePropagationTargets?: TracePropagationTargets; + /** + * Function determining whether or not to create spans to track outgoing requests to the given URL. + * By default, spans will be created for all outgoing requests. + */ + shouldCreateSpanForRequest?(url: string): boolean; + /** Callback that is executed when a fatal global error occurs. */ onFatalError?(error: Error): void; } diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index 94bc4edf6c30..4f5205f0b596 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -221,8 +221,36 @@ describe('tracing', () => { addExtensionMethods(); const transaction = hub.startTransaction({ name: 'dogpark' }); hub.getScope()?.setSpan(transaction); + return transaction; } + it("doesn't create span if shouldCreateSpanForRequest returns false", () => { + const url = 'http://dogs.are.great/api/v1/index/'; + nock(url).get(/.*/).reply(200); + + const httpIntegration = new HttpIntegration({ tracing: true }); + + const hub = createHub({ shouldCreateSpanForRequest: () => false }); + + httpIntegration.setupOnce( + () => undefined, + () => hub, + ); + + const transaction = createTransactionAndPutOnScope(hub); + const spans = (transaction as unknown as Span).spanRecorder?.spans as Span[]; + + const request = http.get(url); + + // There should be no http spans + const httpSpans = spans.filter(span => span.op?.startsWith('http')); + expect(httpSpans.length).toBe(0); + + // And headers are not attached without span creation + expect(request.getHeader('sentry-trace')).toBeUndefined(); + expect(request.getHeader('baggage')).toBeUndefined(); + }); + it.each([ ['http://dogs.are.great/api/v1/index/', [/.*/]], ['http://dogs.are.great/api/v1/index/', [/\/api/]],