Skip to content

Commit 8d7f49c

Browse files
committed
feat(node): Update httpIntegration handling of incoming requests
1 parent f307a22 commit 8d7f49c

File tree

10 files changed

+349
-161
lines changed

10 files changed

+349
-161
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44

55
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
66

7+
### Important Changes
8+
9+
- **feat(node): Update `httpIntegration` handling of incoming requests
10+
11+
This version updates the handling of the Node SDK of incoming requests. Instead of relying on @opentelemetry/instrumentation-http for this, we now handle this internally, ensuring that we can optimize performance as much as possible and avoid interop problems.
12+
13+
This change should not affect users, unless they are relying on very in-depth implementation details. Importantly, this also drops the `_experimentalConfig` option of the integration - this will no longer do anything.
14+
Finally, you can still pass `instrumentation.{requestHook,responseHook,applyCustomAttributesOnSpan}` options, but they are deprecated and will be removed in v11. Instead, you can use the new `incomingRequestSpanHook` configuration option if you want to adjust the incoming request span.
15+
716
## 10.3.0
817

918
- feat(core): MCP Server - Capture prompt results from prompt function calls (#17284)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ describe('express tracing', () => {
2929

3030
// Also calling `updateName` AND setting a source doesn't change anything - Otel has no concept of source, this is sentry-internal.
3131
// Therefore, only the source is updated but the name is still overwritten by Otel.
32-
test("calling `span.updateName` and setting attribute source doesn't update the final name in express but it updates the source", async () => {
32+
test('calling `span.updateName` and setting attribute source updates the final name in express', async () => {
3333
const runner = createRunner(__dirname, 'server.js')
3434
.expect({
3535
transaction: {
36-
transaction: 'GET /test/:id/span-updateName-source',
36+
transaction: 'new-name',
3737
transaction_info: {
3838
source: 'custom',
3939
},

dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ Sentry.init({
1111

1212
integrations: [
1313
Sentry.httpIntegration({
14+
incomingRequestSpanHook: (span, req, res) => {
15+
span.setAttribute('incomingRequestSpanHook', 'yes');
16+
Sentry.setExtra('incomingRequestSpanHookCalled', {
17+
reqUrl: req.url,
18+
reqMethod: req.method,
19+
resUrl: res.req.url,
20+
resMethod: res.req.method,
21+
});
22+
},
1423
instrumentation: {
1524
requestHook: (span, req) => {
1625
span.setAttribute('attr1', 'yes');

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

Lines changed: 0 additions & 38 deletions
This file was deleted.

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

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -49,30 +49,38 @@ describe('httpIntegration', () => {
4949
runner.makeRequest('get', '/test');
5050
await runner.completed();
5151
});
52-
});
5352

54-
test('allows to pass experimental config through to integration', async () => {
55-
const runner = createRunner(__dirname, 'server-experimental.js')
56-
.expect({
57-
transaction: {
58-
contexts: {
59-
trace: {
60-
span_id: expect.stringMatching(/[a-f0-9]{16}/),
61-
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
62-
data: {
63-
url: expect.stringMatching(/\/test$/),
64-
'http.response.status_code': 200,
65-
'http.server_name': 'sentry-test-server-name',
53+
test('allows to configure incomingRequestSpanHook', async () => {
54+
const runner = createRunner()
55+
.expect({
56+
transaction: {
57+
contexts: {
58+
trace: {
59+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
60+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
61+
data: {
62+
url: expect.stringMatching(/\/test$/),
63+
'http.response.status_code': 200,
64+
incomingRequestSpanHook: 'yes',
65+
},
66+
op: 'http.server',
67+
status: 'ok',
6668
},
67-
op: 'http.server',
68-
status: 'ok',
6969
},
70+
extra: expect.objectContaining({
71+
incomingRequestSpanHookCalled: {
72+
reqUrl: expect.stringMatching(/\/test$/),
73+
reqMethod: 'GET',
74+
resUrl: expect.stringMatching(/\/test$/),
75+
resMethod: 'GET',
76+
},
77+
}),
7078
},
71-
},
72-
})
73-
.start();
74-
runner.makeRequest('get', '/test');
75-
await runner.completed();
79+
})
80+
.start();
81+
runner.makeRequest('get', '/test');
82+
await runner.completed();
83+
});
7684
});
7785

7886
describe("doesn't create a root span for incoming requests ignored via `ignoreIncomingRequests`", () => {

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { ChannelListener } from 'node:diagnostics_channel';
22
import { subscribe, unsubscribe } from 'node:diagnostics_channel';
33
import type * as http from 'node:http';
44
import type * as https from 'node:https';
5+
import type { Span } from '@opentelemetry/api';
56
import { context } from '@opentelemetry/api';
67
import { isTracingSuppressed } from '@opentelemetry/core';
78
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
@@ -28,12 +29,22 @@ export type SentryHttpInstrumentationOptions = InstrumentationConfig & {
2829
*/
2930
breadcrumbs?: boolean;
3031

32+
/**
33+
* Whether to create spans for requests or not.
34+
* As of now, creates spans for incoming requests, but not outgoing requests.
35+
*
36+
* @default `true`
37+
*/
38+
spans?: boolean;
39+
3140
/**
3241
* Whether to extract the trace ID from the `sentry-trace` header for incoming requests.
3342
* By default this is done by the HttpInstrumentation, but if that is not added (e.g. because tracing is disabled, ...)
3443
* then this instrumentation can take over.
3544
*
36-
* @default `false`
45+
* @deprecated This is always true and the option will be removed in the future.
46+
*
47+
* @default `true`
3748
*/
3849
extractIncomingTraceFromHeader?: boolean;
3950

@@ -57,6 +68,14 @@ export type SentryHttpInstrumentationOptions = InstrumentationConfig & {
5768
*/
5869
ignoreOutgoingRequests?: (url: string, request: http.RequestOptions) => boolean;
5970

71+
/**
72+
* Do not capture spans for incoming HTTP requests to URLs where the given callback returns `true`.
73+
*
74+
* @param urlPath Contains the URL path and query string (if any) of the incoming request.
75+
* @param request Contains the {@type IncomingMessage} object of the incoming request.
76+
*/
77+
ignoreSpansForIncomingRequests?: (urlPath: string, request: http.IncomingMessage) => boolean;
78+
6079
/**
6180
* Do not capture the request body for incoming HTTP requests to URLs where the given callback returns `true`.
6281
* This can be useful for long running requests where the body is not needed and we want to avoid capturing it.
@@ -66,6 +85,12 @@ export type SentryHttpInstrumentationOptions = InstrumentationConfig & {
6685
*/
6786
ignoreIncomingRequestBody?: (url: string, request: http.RequestOptions) => boolean;
6887

88+
/**
89+
* A hook that can be used to mutate the span for incoming requests.
90+
* This is triggered after the span is created, but before it is recorded.
91+
*/
92+
incomingRequestSpanHook?: (span: Span, request: http.IncomingMessage, response: http.ServerResponse) => void;
93+
6994
/**
7095
* Controls the maximum size of incoming HTTP request bodies attached to events.
7196
*
@@ -90,6 +115,19 @@ export type SentryHttpInstrumentationOptions = InstrumentationConfig & {
90115
*/
91116
trackIncomingRequestsAsSessions?: boolean;
92117

118+
/**
119+
* @deprecated This is deprecated in favor of `incomingRequestSpanHook`.
120+
*/
121+
instrumentation?: {
122+
requestHook?: (span: Span, req: http.ClientRequest | http.IncomingMessage) => void;
123+
responseHook?: (span: Span, response: http.IncomingMessage | http.ServerResponse) => void;
124+
applyCustomAttributesOnSpan?: (
125+
span: Span,
126+
request: http.ClientRequest | http.IncomingMessage,
127+
response: http.IncomingMessage | http.ServerResponse,
128+
) => void;
129+
};
130+
93131
/**
94132
* Number of milliseconds until sessions tracked with `trackIncomingRequestsAsSessions` will be flushed as a session aggregate.
95133
*
@@ -131,10 +169,15 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
131169
const onHttpServerRequestStart = ((_data: unknown) => {
132170
const data = _data as { server: http.Server };
133171
instrumentServer(data.server, {
172+
// eslint-disable-next-line deprecation/deprecation
173+
instrumentation: this.getConfig().instrumentation,
134174
ignoreIncomingRequestBody: this.getConfig().ignoreIncomingRequestBody,
175+
ignoreSpansForIncomingRequests: this.getConfig().ignoreSpansForIncomingRequests,
176+
incomingRequestSpanHook: this.getConfig().incomingRequestSpanHook,
135177
maxIncomingRequestBodySize: this.getConfig().maxIncomingRequestBodySize,
136178
trackIncomingRequestsAsSessions: this.getConfig().trackIncomingRequestsAsSessions,
137179
sessionFlushingDelayMS: this.getConfig().sessionFlushingDelayMS ?? 60_000,
180+
spans: this.getConfig().spans ?? true,
138181
});
139182
}) satisfies ChannelListener;
140183

0 commit comments

Comments
 (0)