Skip to content

Commit a2dcb28

Browse files
authored
feat(node): Allow to pass instrumentation config to httpIntegration (#12761)
Today, it is not (easily) possible to use custom config for the OTEL HttpInstrumentation. We depend on our own integration for various things, so overwriting this will lead to lots of problems. With this PR, you can now pass some config through directly, in an "allowlisted" way, + there is an escape hatch to add arbitrary other config: ```js Sentry.init({ integrations: [ Sentry.httpIntegration({ instrumentation: { // these three are "vetted" requestHook: (span, req) => span.setAttribute('custom', req.method), responseHook: (span, res) => span.setAttribute('custom', res.method), applyCustomAttributesOnSpan: (span, req, res) => span.setAttribute('custom', req.method), // escape hatch: Can add arbitrary other config that is passed through _experimentalConfig: { serverName: 'xxx' } }) ] }); ``` Closes #12672
1 parent 55a5cef commit a2dcb28

File tree

7 files changed

+208
-29
lines changed

7 files changed

+208
-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://public@dsn.ingest.sentry.io/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://public@dsn.ingest.sentry.io/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: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
// response shape seems different on Node 14, so we skip this there
10+
const nodeMajorVersion = Number(process.versions.node.split('.')[0]);
11+
if (nodeMajorVersion <= 14) {
12+
done();
13+
return;
14+
}
15+
16+
createRunner(__dirname, 'server.js')
17+
.ignore('session', 'sessions')
18+
.expect({
19+
transaction: {
20+
contexts: {
21+
trace: {
22+
span_id: expect.any(String),
23+
trace_id: expect.any(String),
24+
data: {
25+
url: expect.stringMatching(/\/test$/),
26+
'http.response.status_code': 200,
27+
attr1: 'yes',
28+
attr2: 'yes',
29+
attr3: 'yes',
30+
},
31+
op: 'http.server',
32+
status: 'ok',
33+
},
34+
},
35+
extra: {
36+
requestHookCalled: {
37+
url: expect.stringMatching(/\/test$/),
38+
method: 'GET',
39+
},
40+
responseHookCalled: {
41+
url: expect.stringMatching(/\/test$/),
42+
method: 'GET',
43+
},
44+
applyCustomAttributesOnSpanCalled: {
45+
reqUrl: expect.stringMatching(/\/test$/),
46+
reqMethod: 'GET',
47+
resUrl: expect.stringMatching(/\/test$/),
48+
resMethod: 'GET',
49+
},
50+
},
51+
},
52+
})
53+
.start(done)
54+
.makeRequest('get', '/test');
55+
});
56+
57+
test('allows to pass experimental config through to integration', done => {
58+
createRunner(__dirname, 'server-experimental.js')
59+
.ignore('session', 'sessions')
60+
.expect({
61+
transaction: {
62+
contexts: {
63+
trace: {
64+
span_id: expect.any(String),
65+
trace_id: expect.any(String),
66+
data: {
67+
url: expect.stringMatching(/\/test$/),
68+
'http.response.status_code': 200,
69+
'http.server_name': 'sentry-test-server-name',
70+
},
71+
op: 'http.server',
72+
status: 'ok',
73+
},
74+
},
75+
},
76+
})
77+
.start(done)
78+
.makeRequest('get', '/test');
79+
});
80+
});

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)