Skip to content

Commit d37def1

Browse files
committed
feat(node): Allow to pass instrumentation config to httpIntegration
1 parent 8c0631a commit d37def1

File tree

7 files changed

+201
-29
lines changed

7 files changed

+201
-29
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
22

3-
describe('express tracing experimental', () => {
3+
describe('express tracing', () => {
44
afterAll(() => {
55
cleanupChildProcesses();
66
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
2+
const Sentry = require('@sentry/node');
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
// disable attaching headers to /test/* endpoints
8+
tracePropagationTargets: [/^(?!.*test).*$/],
9+
tracesSampleRate: 1.0,
10+
transport: loggingTransport,
11+
12+
integrations: [
13+
Sentry.httpIntegration({
14+
instrumentation: {
15+
_experimentalConfig: {
16+
serverName: 'sentry-test-server-name',
17+
},
18+
},
19+
}),
20+
],
21+
});
22+
23+
// express must be required after Sentry is initialized
24+
const express = require('express');
25+
const cors = require('cors');
26+
const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');
27+
28+
const app = express();
29+
30+
app.use(cors());
31+
32+
app.get('/test', (_req, res) => {
33+
res.send({ response: 'response 1' });
34+
});
35+
36+
Sentry.setupExpressErrorHandler(app);
37+
38+
startExpressServerAndSendPortToRunner(app);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
2+
const Sentry = require('@sentry/node');
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
// disable attaching headers to /test/* endpoints
8+
tracePropagationTargets: [/^(?!.*test).*$/],
9+
tracesSampleRate: 1.0,
10+
transport: loggingTransport,
11+
12+
integrations: [
13+
Sentry.httpIntegration({
14+
instrumentation: {
15+
requestHook: (span, req) => {
16+
span.setAttribute('attr1', 'yes');
17+
Sentry.setExtra('requestHookCalled', {
18+
url: req.url,
19+
method: req.method,
20+
});
21+
},
22+
responseHook: (span, res) => {
23+
span.setAttribute('attr2', 'yes');
24+
Sentry.setExtra('responseHookCalled', {
25+
url: res.req.url,
26+
method: res.req.method,
27+
});
28+
},
29+
applyCustomAttributesOnSpan: (span, req, res) => {
30+
span.setAttribute('attr3', 'yes');
31+
Sentry.setExtra('applyCustomAttributesOnSpanCalled', {
32+
reqUrl: req.url,
33+
reqMethod: req.method,
34+
resUrl: res.req.url,
35+
resMethod: res.req.method,
36+
});
37+
},
38+
},
39+
}),
40+
],
41+
});
42+
43+
// express must be required after Sentry is initialized
44+
const express = require('express');
45+
const cors = require('cors');
46+
const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');
47+
48+
const app = express();
49+
50+
app.use(cors());
51+
52+
app.get('/test', (_req, res) => {
53+
res.send({ response: 'response 1' });
54+
});
55+
56+
Sentry.setupExpressErrorHandler(app);
57+
58+
startExpressServerAndSendPortToRunner(app);
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
2+
3+
describe('httpIntegration', () => {
4+
afterAll(() => {
5+
cleanupChildProcesses();
6+
});
7+
8+
test('allows to pass instrumentation options to integration', done => {
9+
createRunner(__dirname, 'server.js')
10+
.ignore('session', 'sessions')
11+
.expect({
12+
transaction: {
13+
contexts: {
14+
trace: {
15+
span_id: expect.any(String),
16+
trace_id: expect.any(String),
17+
data: {
18+
url: expect.stringMatching(/\/test$/),
19+
'http.response.status_code': 200,
20+
attr1: 'yes',
21+
attr2: 'yes',
22+
attr3: 'yes',
23+
},
24+
op: 'http.server',
25+
status: 'ok',
26+
},
27+
},
28+
extra: {
29+
requestHookCalled: {
30+
url: expect.stringMatching(/\/test$/),
31+
method: 'GET',
32+
},
33+
responseHookCalled: {
34+
url: expect.stringMatching(/\/test$/),
35+
method: 'GET',
36+
},
37+
applyCustomAttributesOnSpanCalled: {
38+
reqUrl: expect.stringMatching(/\/test$/),
39+
reqMethod: 'GET',
40+
resUrl: expect.stringMatching(/\/test$/),
41+
resMethod: 'GET',
42+
},
43+
},
44+
},
45+
})
46+
.start(done)
47+
.makeRequest('get', '/test');
48+
});
49+
50+
test('allows to pass experimental config through to integration', done => {
51+
createRunner(__dirname, 'server-experimental.js')
52+
.ignore('session', 'sessions')
53+
.expect({
54+
transaction: {
55+
contexts: {
56+
trace: {
57+
span_id: expect.any(String),
58+
trace_id: expect.any(String),
59+
data: {
60+
url: expect.stringMatching(/\/test$/),
61+
'http.response.status_code': 200,
62+
'http.server_name': 'sentry-test-server-name',
63+
},
64+
op: 'http.server',
65+
status: 'ok',
66+
},
67+
},
68+
},
69+
})
70+
.start(done)
71+
.makeRequest('get', '/test');
72+
});
73+
});

packages/nextjs/src/server/httpIntegration.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,7 @@ class CustomNextjsHttpIntegration extends HttpInstrumentation {
2222
}
2323
}
2424

25-
interface HttpOptions {
26-
/**
27-
* Whether breadcrumbs should be recorded for requests.
28-
* Defaults to true
29-
*/
30-
breadcrumbs?: boolean;
31-
32-
/**
33-
* Do not capture spans or breadcrumbs for outgoing HTTP requests to URLs where the given callback returns `true`.
34-
* This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled.
35-
*/
36-
ignoreOutgoingRequests?: (url: string) => boolean;
37-
}
25+
type HttpOptions = Parameters<typeof originalHttpIntegration>[0];
3826

3927
/**
4028
* The http integration instruments Node's internal http and https modules.

packages/node/src/integrations/http.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,25 @@ interface HttpOptions {
4343
*/
4444
ignoreIncomingRequests?: (url: string) => boolean;
4545

46+
/**
47+
* Additional instrumentation options that are passed to the underlying HttpInstrumentation.
48+
*/
49+
instrumentation?: {
50+
requestHook?: (span: Span, req: ClientRequest | HTTPModuleRequestIncomingMessage) => void;
51+
responseHook?: (span: Span, response: HTTPModuleRequestIncomingMessage | ServerResponse) => void;
52+
applyCustomAttributesOnSpan?: (
53+
span: Span,
54+
request: ClientRequest | HTTPModuleRequestIncomingMessage,
55+
response: HTTPModuleRequestIncomingMessage | ServerResponse,
56+
) => void;
57+
58+
/**
59+
* You can pass any configuration through to the underlying instrumention.
60+
* Note that there are no semver guarantees for this!
61+
*/
62+
_experimentalConfig?: ConstructorParameters<typeof HttpInstrumentation>[0];
63+
};
64+
4665
/** Allows to pass a custom version of HttpInstrumentation. We use this for Next.js. */
4766
_instrumentation?: typeof HttpInstrumentation;
4867
}
@@ -63,6 +82,7 @@ export const instrumentHttp = Object.assign(
6382
const _InstrumentationClass = _httpOptions._instrumentation || HttpInstrumentation;
6483

6584
_httpInstrumentation = new _InstrumentationClass({
85+
..._httpOptions.instrumentation?._experimentalConfig,
6686
ignoreOutgoingRequestHook: request => {
6787
const url = getRequestUrl(request);
6888

@@ -107,6 +127,7 @@ export const instrumentHttp = Object.assign(
107127
// both, incoming requests and "client" requests made within the app trigger the requestHook
108128
// we only want to isolate and further annotate incoming requests (IncomingMessage)
109129
if (_isClientRequest(req)) {
130+
_httpOptions.instrumentation?.requestHook?.(span, req);
110131
return;
111132
}
112133

@@ -134,24 +155,30 @@ export const instrumentHttp = Object.assign(
134155
const bestEffortTransactionName = `${httpMethod} ${httpTarget}`;
135156

136157
isolationScope.setTransactionName(bestEffortTransactionName);
158+
159+
_httpOptions.instrumentation?.requestHook?.(span, req);
137160
},
138-
responseHook: () => {
161+
responseHook: (span, res) => {
139162
const client = getClient<NodeClient>();
140163
if (client && client.getOptions().autoSessionTracking) {
141164
setImmediate(() => {
142165
client['_captureRequestSession']();
143166
});
144167
}
168+
169+
_httpOptions.instrumentation?.responseHook?.(span, res);
145170
},
146171
applyCustomAttributesOnSpan: (
147-
_span: Span,
172+
span: Span,
148173
request: ClientRequest | HTTPModuleRequestIncomingMessage,
149174
response: HTTPModuleRequestIncomingMessage | ServerResponse,
150175
) => {
151176
const _breadcrumbs = typeof _httpOptions.breadcrumbs === 'undefined' ? true : _httpOptions.breadcrumbs;
152177
if (_breadcrumbs) {
153178
_addRequestBreadcrumb(request, response);
154179
}
180+
181+
_httpOptions.instrumentation?.applyCustomAttributesOnSpan?.(span, request, response);
155182
},
156183
});
157184

packages/remix/src/utils/integrations/http.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,7 @@ class RemixHttpIntegration extends HttpInstrumentation {
1616
}
1717
}
1818

19-
interface HttpOptions {
20-
/**
21-
* Whether breadcrumbs should be recorded for requests.
22-
* Defaults to true
23-
*/
24-
breadcrumbs?: boolean;
25-
26-
/**
27-
* Do not capture spans or breadcrumbs for outgoing HTTP requests to URLs where the given callback returns `true`.
28-
* This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled.
29-
*/
30-
ignoreOutgoingRequests?: (url: string) => boolean;
31-
}
19+
type HttpOptions = Parameters<typeof originalHttpIntegration>[0];
3220

3321
/**
3422
* The http integration instruments Node's internal http and https modules.

0 commit comments

Comments
 (0)