Skip to content

Commit 86c6931

Browse files
committed
WIP http & node-fetch fixes
1 parent 72468b1 commit 86c6931

File tree

12 files changed

+544
-21
lines changed

12 files changed

+544
-21
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import '@sentry/tracing';
2+
3+
import * as http from 'http';
4+
import * as Sentry from '@sentry/node';
5+
6+
Sentry.init({
7+
dsn: 'https://[email protected]/1337',
8+
release: '1.0',
9+
tracesSampleRate: 1.0,
10+
integrations: [Sentry.httpIntegration({})],
11+
debug: true,
12+
});
13+
14+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
15+
Sentry.startSpan({ name: 'test_transaction' }, async () => {
16+
http.get('http://match-this-url.com/api/v0');
17+
http.get('http://match-this-url.com/api/v1');
18+
19+
// Give it a tick to resolve...
20+
await new Promise(resolve => setTimeout(resolve, 100));
21+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import nock from 'nock';
2+
3+
import { TestEnv, assertSentryTransaction } from '../../../../utils';
4+
5+
test('should capture spans for outgoing http requests', async () => {
6+
const match1 = nock('http://match-this-url.com').get('/api/v0').reply(200);
7+
const match2 = nock('http://match-this-url.com').get('/api/v1').reply(200);
8+
9+
const env = await TestEnv.init(__dirname);
10+
const envelope = await env.getEnvelopeRequest({ envelopeType: 'transaction' });
11+
12+
expect(match1.isDone()).toBe(true);
13+
expect(match2.isDone()).toBe(true);
14+
15+
expect(envelope).toHaveLength(3);
16+
17+
assertSentryTransaction(envelope[2], {
18+
transaction: 'test_transaction',
19+
spans: [
20+
{
21+
description: 'GET http://match-this-url.com/api/v0',
22+
op: 'http.client',
23+
origin: 'auto.http.node.http',
24+
status: 'ok',
25+
tags: {
26+
'http.status_code': '200',
27+
},
28+
},
29+
{
30+
description: 'GET http://match-this-url.com/api/v1',
31+
op: 'http.client',
32+
origin: 'auto.http.node.http',
33+
status: 'ok',
34+
tags: {
35+
'http.status_code': '200',
36+
},
37+
},
38+
],
39+
});
40+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import '@sentry/tracing';
2+
3+
import * as http from 'http';
4+
import * as Sentry from '@sentry/node';
5+
6+
Sentry.init({
7+
dsn: 'https://[email protected]/1337',
8+
release: '1.0',
9+
tracesSampleRate: 1.0,
10+
integrations: [Sentry.httpIntegration({ tracing: false })],
11+
});
12+
13+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
14+
Sentry.startSpan({ name: 'test_transaction' }, async () => {
15+
http.get('http://match-this-url.com/api/v0');
16+
http.get('http://match-this-url.com/api/v1');
17+
18+
// Give it a tick to resolve...
19+
await new Promise(resolve => setTimeout(resolve, 100));
20+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import nock from 'nock';
2+
3+
import { TestEnv, assertSentryTransaction } from '../../../../utils';
4+
5+
test('should not capture spans for outgoing http requests if tracing is disabled', async () => {
6+
const match1 = nock('http://match-this-url.com').get('/api/v0').reply(200);
7+
const match2 = nock('http://match-this-url.com').get('/api/v1').reply(200);
8+
9+
const env = await TestEnv.init(__dirname);
10+
const envelope = await env.getEnvelopeRequest({ envelopeType: 'transaction' });
11+
12+
expect(match1.isDone()).toBe(true);
13+
expect(match2.isDone()).toBe(true);
14+
15+
expect(envelope).toHaveLength(3);
16+
17+
assertSentryTransaction(envelope[2], {
18+
transaction: 'test_transaction',
19+
spans: [],
20+
});
21+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2+
import '@sentry/tracing';
3+
4+
import * as http from 'http';
5+
import * as Sentry from '@sentry/node';
6+
7+
Sentry.init({
8+
dsn: 'https://[email protected]/1337',
9+
release: '1.0',
10+
tracesSampleRate: 1.0,
11+
tracePropagationTargets: [/\/v0/, 'v1'],
12+
integrations: [Sentry.httpIntegration({})],
13+
});
14+
15+
Sentry.startSpan({ name: 'test_transaction' }, () => {
16+
http.get('http://match-this-url.com/api/v0');
17+
http.get('http://match-this-url.com/api/v1');
18+
http.get('http://dont-match-this-url.com/api/v2');
19+
http.get('http://dont-match-this-url.com/api/v3');
20+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import nock from 'nock';
2+
3+
import { TestEnv, runScenario } from '../../../../utils';
4+
5+
test('httpIntegration should instrument correct requests when tracePropagationTargets option is provided & tracing is enabled', async () => {
6+
const match1 = nock('http://match-this-url.com')
7+
.get('/api/v0')
8+
.matchHeader('baggage', val => typeof val === 'string')
9+
.matchHeader('sentry-trace', val => typeof val === 'string')
10+
.reply(200);
11+
12+
const match2 = nock('http://match-this-url.com')
13+
.get('/api/v1')
14+
.matchHeader('baggage', val => typeof val === 'string')
15+
.matchHeader('sentry-trace', val => typeof val === 'string')
16+
.reply(200);
17+
18+
const match3 = nock('http://dont-match-this-url.com')
19+
.get('/api/v2')
20+
.matchHeader('baggage', val => val === undefined)
21+
.matchHeader('sentry-trace', val => val === undefined)
22+
.reply(200);
23+
24+
const match4 = nock('http://dont-match-this-url.com')
25+
.get('/api/v3')
26+
.matchHeader('baggage', val => val === undefined)
27+
.matchHeader('sentry-trace', val => val === undefined)
28+
.reply(200);
29+
30+
const env = await TestEnv.init(__dirname);
31+
await runScenario(env.url);
32+
33+
env.server.close();
34+
nock.cleanAll();
35+
36+
await new Promise(resolve => env.server.close(resolve));
37+
38+
expect(match1.isDone()).toBe(true);
39+
expect(match2.isDone()).toBe(true);
40+
expect(match3.isDone()).toBe(true);
41+
expect(match4.isDone()).toBe(true);
42+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2+
import '@sentry/tracing';
3+
4+
import * as http from 'http';
5+
import * as Sentry from '@sentry/node';
6+
7+
Sentry.init({
8+
dsn: 'https://[email protected]/1337',
9+
release: '1.0',
10+
tracePropagationTargets: [/\/v0/, 'v1'],
11+
integrations: [Sentry.httpIntegration({})],
12+
});
13+
14+
Sentry.startSpan({ name: 'test_transaction' }, () => {
15+
http.get('http://match-this-url.com/api/v0');
16+
http.get('http://match-this-url.com/api/v1');
17+
http.get('http://dont-match-this-url.com/api/v2');
18+
http.get('http://dont-match-this-url.com/api/v3');
19+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import nock from 'nock';
2+
3+
import { TestEnv, runScenario } from '../../../../utils';
4+
5+
test('httpIntegration should not instrument when tracing is enabled', async () => {
6+
const match1 = nock('http://match-this-url.com')
7+
.get('/api/v0')
8+
.matchHeader('baggage', val => val === undefined)
9+
.matchHeader('sentry-trace', val => val === undefined)
10+
.reply(200);
11+
12+
const match2 = nock('http://match-this-url.com')
13+
.get('/api/v1')
14+
.matchHeader('baggage', val => val === undefined)
15+
.matchHeader('sentry-trace', val => val === undefined)
16+
.reply(200);
17+
18+
const match3 = nock('http://dont-match-this-url.com')
19+
.get('/api/v2')
20+
.matchHeader('baggage', val => val === undefined)
21+
.matchHeader('sentry-trace', val => val === undefined)
22+
.reply(200);
23+
24+
const match4 = nock('http://dont-match-this-url.com')
25+
.get('/api/v3')
26+
.matchHeader('baggage', val => val === undefined)
27+
.matchHeader('sentry-trace', val => val === undefined)
28+
.reply(200);
29+
30+
const env = await TestEnv.init(__dirname);
31+
await runScenario(env.url);
32+
33+
env.server.close();
34+
nock.cleanAll();
35+
36+
await new Promise(resolve => env.server.close(resolve));
37+
38+
expect(match1.isDone()).toBe(true);
39+
expect(match2.isDone()).toBe(true);
40+
expect(match3.isDone()).toBe(true);
41+
expect(match4.isDone()).toBe(true);
42+
});

packages/node/src/integrations/http.ts

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
/* eslint-disable max-lines */
12
import type * as http from 'http';
23
import type * as https from 'https';
34
import type { Hub } from '@sentry/core';
4-
import { getIsolationScope, defineIntegration } from '@sentry/core';
5+
import { defineIntegration, getIsolationScope, hasTracingEnabled } from '@sentry/core';
56
import {
67
addBreadcrumb,
78
getActiveSpan,
@@ -16,7 +17,7 @@ import {
1617
spanToTraceHeader,
1718
} from '@sentry/core';
1819
import type {
19-
DynamicSamplingContext,
20+
ClientOptions,
2021
EventProcessor,
2122
Integration,
2223
IntegrationFn,
@@ -26,6 +27,7 @@ import type {
2627
} from '@sentry/types';
2728
import {
2829
LRUMap,
30+
dropUndefinedKeys,
2931
dynamicSamplingContextToSentryBaggageHeader,
3032
fill,
3133
generateSentryTraceHeader,
@@ -36,6 +38,7 @@ import {
3638
import type { NodeClient } from '../client';
3739
import { DEBUG_BUILD } from '../debug-build';
3840
import { NODE_VERSION } from '../nodeVersion';
41+
import type { NodeClientOptions } from '../types';
3942
import type { RequestMethod, RequestMethodArgs, RequestOptions } from './utils/http';
4043
import { cleanSpanDescription, extractRawUrl, extractUrl, normalizeRequestArgs } from './utils/http';
4144

@@ -64,6 +67,12 @@ interface TracingOptions {
6467
* By default, spans will be created for all outgoing requests.
6568
*/
6669
shouldCreateSpanForRequest?: (url: string) => boolean;
70+
71+
/**
72+
* This option is just for compatibility with v7.
73+
* In v8, this will be the default behavior.
74+
*/
75+
enableIfHasTracingEnabled?: boolean;
6776
}
6877

6978
interface HttpOptions {
@@ -80,11 +89,52 @@ interface HttpOptions {
8089
tracing?: TracingOptions | boolean;
8190
}
8291

83-
const _httpIntegration = ((options?: HttpOptions) => {
92+
/* These are the newer options for `httpIntegration`. */
93+
interface HttpIntegrationOptions {
94+
/**
95+
* Whether breadcrumbs should be recorded for requests
96+
* Defaults to true.
97+
*/
98+
breadcrumbs?: boolean;
99+
100+
/**
101+
* Whether tracing spans should be created for requests
102+
* If not set, this will be enabled/disabled based on if tracing is enabled.
103+
*/
104+
tracing?: boolean;
105+
106+
/**
107+
* Function determining whether or not to create spans to track outgoing requests to the given URL.
108+
* By default, spans will be created for all outgoing requests.
109+
*/
110+
shouldCreateSpanForRequest?: (url: string) => boolean;
111+
}
112+
113+
const _httpIntegration = ((options: HttpIntegrationOptions = {}) => {
114+
const { breadcrumbs, tracing, shouldCreateSpanForRequest } = options;
115+
116+
const convertedOptions: HttpOptions = {
117+
breadcrumbs,
118+
tracing:
119+
tracing === false
120+
? false
121+
: dropUndefinedKeys({
122+
// If tracing is forced to `true`, we don't want to set `enableIfHasTracingEnabled`
123+
enableIfHasTracingEnabled: tracing === true ? undefined : true,
124+
shouldCreateSpanForRequest,
125+
}),
126+
};
127+
84128
// eslint-disable-next-line deprecation/deprecation
85-
return new Http(options) as unknown as IntegrationFnResult;
129+
return new Http(convertedOptions) as unknown as IntegrationFnResult;
86130
}) satisfies IntegrationFn;
87131

132+
/**
133+
* The http module integration instruments Node's internal http module. It creates breadcrumbs, spans for outgoing
134+
* http requests, and attaches trace data when tracing is enabled via its `tracing` option.
135+
*
136+
* By default, this will always create breadcrumbs, and will create spans if tracing is enabled.
137+
*/
88138
export const httpIntegration = defineIntegration(_httpIntegration);
89139

90140
/**
@@ -123,23 +173,26 @@ export class Http implements Integration {
123173
_addGlobalEventProcessor: (callback: EventProcessor) => void,
124174
setupOnceGetCurrentHub: () => Hub,
125175
): void {
176+
// eslint-disable-next-line deprecation/deprecation
177+
const clientOptions = setupOnceGetCurrentHub().getClient<NodeClient>()?.getOptions();
178+
179+
// If `tracing` is not explicitly set, we default this based on whether or not tracing is enabled.
180+
// But for compatibility, we only do that if `enableIfHasTracingEnabled` is set.
181+
const shouldCreateSpans = _shouldCreateSpans(this._tracing, clientOptions);
182+
126183
// No need to instrument if we don't want to track anything
127-
if (!this._breadcrumbs && !this._tracing) {
184+
if (!this._breadcrumbs && !shouldCreateSpans) {
128185
return;
129186
}
130187

131-
// eslint-disable-next-line deprecation/deprecation
132-
const clientOptions = setupOnceGetCurrentHub().getClient<NodeClient>()?.getOptions();
133-
134188
// Do not auto-instrument for other instrumenter
135189
if (clientOptions && clientOptions.instrumenter !== 'sentry') {
136190
DEBUG_BUILD && logger.log('HTTP Integration is skipped because of instrumenter configuration.');
137191
return;
138192
}
139193

140-
const shouldCreateSpanForRequest =
141-
// eslint-disable-next-line deprecation/deprecation
142-
this._tracing?.shouldCreateSpanForRequest || clientOptions?.shouldCreateSpanForRequest;
194+
const shouldCreateSpanForRequest = _getShouldCreateSpanForRequest(shouldCreateSpans, this._tracing, clientOptions);
195+
143196
// eslint-disable-next-line deprecation/deprecation
144197
const tracePropagationTargets = clientOptions?.tracePropagationTargets || this._tracing?.tracePropagationTargets;
145198

@@ -407,3 +460,29 @@ function normalizeBaggageHeader(
407460
// we say this is undefined behaviour, since it would not be baggage spec conform if the user did this.
408461
return [requestOptions.headers.baggage, sentryBaggageHeader] as string[];
409462
}
463+
464+
/** Exported for tests only. */
465+
export function _shouldCreateSpans(
466+
tracingOptions: TracingOptions | undefined,
467+
clientOptions: Partial<ClientOptions> | undefined,
468+
): boolean {
469+
return tracingOptions === undefined
470+
? false
471+
: tracingOptions.enableIfHasTracingEnabled
472+
? hasTracingEnabled(clientOptions)
473+
: true;
474+
}
475+
476+
/** Exported for tests only. */
477+
export function _getShouldCreateSpanForRequest(
478+
shouldCreateSpans: boolean,
479+
tracingOptions: TracingOptions | undefined,
480+
clientOptions: Partial<NodeClientOptions> | undefined,
481+
): undefined | ((url: string) => boolean) {
482+
const handler = shouldCreateSpans
483+
? // eslint-disable-next-line deprecation/deprecation
484+
tracingOptions?.shouldCreateSpanForRequest || clientOptions?.shouldCreateSpanForRequest
485+
: () => false;
486+
487+
return handler;
488+
}

0 commit comments

Comments
 (0)