Skip to content

Commit 3c8776d

Browse files
Lms24billyvg
authored andcommitted
fix(node): Ensure ignoreOutgoingRequests of httpIntegration applies to breadcrumbs (#13970)
Fix a bug which caused the `httpIntegration`'s `ignoreOutgoingRequests` option to not be applied to breadcrumb creation for outgoing requests. As a result, despite spans being correctly filtered by the option, breadcrumbs would still be created which contradicts the function'S JSDoc and [our docs](https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/http/#ignoreoutgoingrequests). This patch fixes the bug by: - correctly passing in `ignoreOutgoingRequests` to `SentryHttpIntegration` (same signature as `httpIntegration`) - reconstructing the `url` and `request` parameter like in the [OpenTelemetry instrumentation](https://github.com/open-telemetry/opentelemetry-js/blob/7293e69c1e55ca62e15d0724d22605e61bd58952/experimental/packages/opentelemetry-instrumentation-http/src/http.ts#L756-L789) - adding/adjusting a regression test so that we properly test agains `ignoreOutgoingRequests` with both params. Now not just for filtered spans but also for breadcrumbs.
1 parent 132b14c commit 3c8776d

File tree

4 files changed

+110
-76
lines changed

4 files changed

+110
-76
lines changed

dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreOutgoingRequests.js

+30-20
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ Sentry.init({
1111
integrations: [
1212
Sentry.httpIntegration({
1313
ignoreOutgoingRequests: (url, request) => {
14-
if (url.includes('example.com')) {
14+
if (url === 'http://example.com/blockUrl') {
1515
return true;
1616
}
17-
if (request.method === 'POST' && request.path === '/path') {
17+
18+
if (request.hostname === 'example.com' && request.path === '/blockRequest') {
1819
return true;
1920
}
2021
return false;
@@ -32,28 +33,37 @@ const app = express();
3233

3334
app.use(cors());
3435

35-
app.get('/test', (_req, response) => {
36-
http
37-
.request('http://example.com/', res => {
38-
res.on('data', () => {});
39-
res.on('end', () => {
40-
response.send({ response: 'done' });
41-
});
42-
})
43-
.end();
36+
app.get('/testUrl', (_req, response) => {
37+
makeHttpRequest('http://example.com/blockUrl').then(() => {
38+
makeHttpRequest('http://example.com/pass').then(() => {
39+
response.send({ response: 'done' });
40+
});
41+
});
4442
});
4543

46-
app.post('/testPath', (_req, response) => {
47-
http
48-
.request('http://example.com/path', res => {
49-
res.on('data', () => {});
50-
res.on('end', () => {
51-
response.send({ response: 'done' });
52-
});
53-
})
54-
.end();
44+
app.get('/testRequest', (_req, response) => {
45+
makeHttpRequest('http://example.com/blockRequest').then(() => {
46+
makeHttpRequest('http://example.com/pass').then(() => {
47+
response.send({ response: 'done' });
48+
});
49+
});
5550
});
5651

5752
Sentry.setupExpressErrorHandler(app);
5853

5954
startExpressServerAndSendPortToRunner(app);
55+
56+
function makeHttpRequest(url) {
57+
return new Promise((resolve, reject) => {
58+
http
59+
.get(url, res => {
60+
res.on('data', () => {});
61+
res.on('end', () => {
62+
resolve();
63+
});
64+
})
65+
.on('error', error => {
66+
reject(error);
67+
});
68+
});
69+
}

dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts

+23-43
Original file line numberDiff line numberDiff line change
@@ -128,65 +128,45 @@ describe('httpIntegration', () => {
128128
});
129129
});
130130

131-
describe("doesn't create child spans for outgoing requests ignored via `ignoreOutgoingRequests`", () => {
131+
describe("doesn't create child spans or breadcrumbs for outgoing requests ignored via `ignoreOutgoingRequests`", () => {
132132
test('via the url param', done => {
133133
const runner = createRunner(__dirname, 'server-ignoreOutgoingRequests.js')
134134
.expect({
135-
transaction: {
136-
contexts: {
137-
trace: {
138-
span_id: expect.any(String),
139-
trace_id: expect.any(String),
140-
data: {
141-
url: expect.stringMatching(/\/test$/),
142-
'http.response.status_code': 200,
143-
},
144-
op: 'http.server',
145-
status: 'ok',
146-
},
147-
},
148-
transaction: 'GET /test',
149-
spans: [
150-
expect.objectContaining({ op: 'middleware.express', description: 'query' }),
151-
expect.objectContaining({ op: 'middleware.express', description: 'expressInit' }),
152-
expect.objectContaining({ op: 'middleware.express', description: 'corsMiddleware' }),
153-
expect.objectContaining({ op: 'request_handler.express', description: '/test' }),
154-
],
135+
transaction: event => {
136+
expect(event.transaction).toBe('GET /testUrl');
137+
138+
const requestSpans = event.spans?.filter(span => span.op === 'http.client');
139+
expect(requestSpans).toHaveLength(1);
140+
expect(requestSpans![0]?.description).toBe('GET http://example.com/pass');
141+
142+
const breadcrumbs = event.breadcrumbs?.filter(b => b.category === 'http');
143+
expect(breadcrumbs).toHaveLength(1);
144+
expect(breadcrumbs![0]?.data?.url).toEqual('http://example.com/pass');
155145
},
156146
})
157147
.start(done);
158148

159-
runner.makeRequest('get', '/test');
149+
runner.makeRequest('get', '/testUrl');
160150
});
161151

162152
test('via the request param', done => {
163153
const runner = createRunner(__dirname, 'server-ignoreOutgoingRequests.js')
164154
.expect({
165-
transaction: {
166-
contexts: {
167-
trace: {
168-
span_id: expect.any(String),
169-
trace_id: expect.any(String),
170-
data: {
171-
url: expect.stringMatching(/\/testPath$/),
172-
'http.response.status_code': 200,
173-
},
174-
op: 'http.server',
175-
status: 'ok',
176-
},
177-
},
178-
transaction: 'POST /testPath',
179-
spans: [
180-
expect.objectContaining({ op: 'middleware.express', description: 'query' }),
181-
expect.objectContaining({ op: 'middleware.express', description: 'expressInit' }),
182-
expect.objectContaining({ op: 'middleware.express', description: 'corsMiddleware' }),
183-
expect.objectContaining({ op: 'request_handler.express', description: '/testPath' }),
184-
],
155+
transaction: event => {
156+
expect(event.transaction).toBe('GET /testRequest');
157+
158+
const requestSpans = event.spans?.filter(span => span.op === 'http.client');
159+
expect(requestSpans).toHaveLength(1);
160+
expect(requestSpans![0]?.description).toBe('GET http://example.com/pass');
161+
162+
const breadcrumbs = event.breadcrumbs?.filter(b => b.category === 'http');
163+
expect(breadcrumbs).toHaveLength(1);
164+
expect(breadcrumbs![0]?.data?.url).toEqual('http://example.com/pass');
185165
},
186166
})
187167
.start(done);
188168

189-
runner.makeRequest('post', '/testPath');
169+
runner.makeRequest('get', '/testRequest');
190170
});
191171
});
192172
});

packages/node/src/integrations/http/SentryHttpInstrumentation.ts

+45-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type * as http from 'node:http';
2+
import type { RequestOptions } from 'node:http';
23
import type * as https from 'node:https';
34
import { VERSION } from '@opentelemetry/core';
45
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
56
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
7+
import { getRequestInfo } from '@opentelemetry/instrumentation-http';
68
import { addBreadcrumb, getClient, getIsolationScope, withIsolationScope } from '@sentry/core';
79
import type { SanitizedRequestData } from '@sentry/types';
810
import {
@@ -12,12 +14,29 @@ import {
1214
stripUrlQueryAndFragment,
1315
} from '@sentry/utils';
1416
import type { NodeClient } from '../../sdk/client';
17+
import { getRequestUrl } from '../../utils/getRequestUrl';
1518

1619
type Http = typeof http;
1720
type Https = typeof https;
1821

1922
type SentryHttpInstrumentationOptions = InstrumentationConfig & {
23+
/**
24+
* Whether breadcrumbs should be recorded for requests.
25+
*
26+
* @default `true`
27+
*/
2028
breadcrumbs?: boolean;
29+
30+
/**
31+
* Do not capture breadcrumbs for outgoing HTTP requests to URLs where the given callback returns `true`.
32+
* For the scope of this instrumentation, this callback only controls breadcrumb creation.
33+
* The same option can be passed to the top-level httpIntegration where it controls both, breadcrumb and
34+
* span creation.
35+
*
36+
* @param url Contains the entire URL, including query string (if any), protocol, host, etc. of the outgoing request.
37+
* @param request Contains the {@type RequestOptions} object used to make the outgoing request.
38+
*/
39+
ignoreOutgoingRequests?: (url: string, request: RequestOptions) => boolean;
2140
};
2241

2342
/**
@@ -140,20 +159,42 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
140159
private _getPatchOutgoingRequestFunction(): (
141160
// eslint-disable-next-line @typescript-eslint/no-explicit-any
142161
original: (...args: any[]) => http.ClientRequest,
143-
) => (...args: unknown[]) => http.ClientRequest {
162+
) => (options: URL | http.RequestOptions | string, ...args: unknown[]) => http.ClientRequest {
144163
// eslint-disable-next-line @typescript-eslint/no-this-alias
145164
const instrumentation = this;
146165

147166
return (original: (...args: unknown[]) => http.ClientRequest): ((...args: unknown[]) => http.ClientRequest) => {
148167
return function outgoingRequest(this: unknown, ...args: unknown[]): http.ClientRequest {
149168
instrumentation._diag.debug('http instrumentation for outgoing requests');
150169

170+
// Making a copy to avoid mutating the original args array
171+
// We need to access and reconstruct the request options object passed to `ignoreOutgoingRequests`
172+
// so that it matches what Otel instrumentation passes to `ignoreOutgoingRequestHook`.
173+
// @see https://github.com/open-telemetry/opentelemetry-js/blob/7293e69c1e55ca62e15d0724d22605e61bd58952/experimental/packages/opentelemetry-instrumentation-http/src/http.ts#L756-L789
174+
const argsCopy = [...args];
175+
176+
const options = argsCopy.shift() as URL | http.RequestOptions | string;
177+
178+
const extraOptions =
179+
typeof argsCopy[0] === 'object' && (typeof options === 'string' || options instanceof URL)
180+
? (argsCopy.shift() as http.RequestOptions)
181+
: undefined;
182+
183+
const { optionsParsed } = getRequestInfo(options, extraOptions);
184+
151185
const request = original.apply(this, args) as ReturnType<typeof http.request>;
152186

153187
request.prependListener('response', (response: http.IncomingMessage) => {
154-
const breadcrumbs = instrumentation.getConfig().breadcrumbs;
155-
const _breadcrumbs = typeof breadcrumbs === 'undefined' ? true : breadcrumbs;
156-
if (_breadcrumbs) {
188+
const _breadcrumbs = instrumentation.getConfig().breadcrumbs;
189+
const breadCrumbsEnabled = typeof _breadcrumbs === 'undefined' ? true : _breadcrumbs;
190+
191+
const _ignoreOutgoingRequests = instrumentation.getConfig().ignoreOutgoingRequests;
192+
const shouldCreateBreadcrumb =
193+
typeof _ignoreOutgoingRequests === 'function'
194+
? !_ignoreOutgoingRequests(getRequestUrl(request), optionsParsed)
195+
: true;
196+
197+
if (breadCrumbsEnabled && shouldCreateBreadcrumb) {
157198
addRequestBreadcrumb(request, response);
158199
}
159200
});

packages/node/src/integrations/http/index.ts

+12-9
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const INSTRUMENTATION_NAME = '@opentelemetry_sentry-patched/instrumentation-http
2020

2121
interface HttpOptions {
2222
/**
23-
* Whether breadcrumbs should be recorded for requests.
23+
* Whether breadcrumbs should be recorded for outgoing requests.
2424
* Defaults to true
2525
*/
2626
breadcrumbs?: boolean;
@@ -45,8 +45,8 @@ interface HttpOptions {
4545
ignoreOutgoingRequests?: (url: string, request: RequestOptions) => boolean;
4646

4747
/**
48-
* Do not capture spans or breadcrumbs for incoming HTTP requests to URLs where the given callback returns `true`.
49-
* This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled.
48+
* Do not capture spans for incoming HTTP requests to URLs where the given callback returns `true`.
49+
* Spans will be non recording if tracing is disabled.
5050
*
5151
* The `urlPath` param consists of the URL path and query string (if any) of the incoming request.
5252
* For example: `'/users/details?id=123'`
@@ -82,12 +82,15 @@ interface HttpOptions {
8282
};
8383
}
8484

85-
export const instrumentSentryHttp = generateInstrumentOnce<{ breadcrumbs?: boolean }>(
86-
`${INTEGRATION_NAME}.sentry`,
87-
options => {
88-
return new SentryHttpInstrumentation({ breadcrumbs: options?.breadcrumbs });
89-
},
90-
);
85+
export const instrumentSentryHttp = generateInstrumentOnce<{
86+
breadcrumbs?: HttpOptions['breadcrumbs'];
87+
ignoreOutgoingRequests?: HttpOptions['ignoreOutgoingRequests'];
88+
}>(`${INTEGRATION_NAME}.sentry`, options => {
89+
return new SentryHttpInstrumentation({
90+
breadcrumbs: options?.breadcrumbs,
91+
ignoreOutgoingRequests: options?.ignoreOutgoingRequests,
92+
});
93+
});
9194

9295
export const instrumentOtelHttp = generateInstrumentOnce<HttpInstrumentationConfig>(INTEGRATION_NAME, config => {
9396
const instrumentation = new HttpInstrumentation(config);

0 commit comments

Comments
 (0)