Skip to content

Commit 7b40a95

Browse files
authored
feat(node): Split up http integration into composable parts (#17524)
This is a first step to de-compose the node/node-core `httpIntegration` into multiple composable parts: * `httpServerIntegration` - handles request isolation, sessions, trace continuation, so core Sentry functionality * `httpServerSpansIntegration` - emits `http.server` spans for incoming requests The `httpIntegration` sets these up under the hood, and for now it is not really recommended for users to use these stand-alone (though it is possible if users opt-out of the `httpIntegration`). The reason is to remain backwards compatible with users using/customizing the `httpIntegration`. We can revisit this in a later major. These new integrations have a much slimmer API surface, and also allows us to avoid having to prefix all the options etc. with what they are about (e.g. `incomingXXX` or `outgoingXXX`). It also means you can actually tree-shake certain features (span creation) out, in theory. Outgoing request handling remains the same for the time being, once we decoupled this from the otel http instrumentation we can do something similar there. The biggest challenge was how to make it possible to de-compose this without having to monkey patch the http server twice. I opted to allow to add callbacks to the `httpServerIntegration` which it will call on any request. So the `httpServerSpansIntegration` can register a callback for itself and plug into this with little overhead.
1 parent 4a9946c commit 7b40a95

File tree

21 files changed

+1110
-825
lines changed

21 files changed

+1110
-825
lines changed

packages/astro/src/index.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ export {
6666
hapiIntegration,
6767
honoIntegration,
6868
httpIntegration,
69+
httpServerIntegration,
70+
httpServerSpansIntegration,
6971
// eslint-disable-next-line deprecation/deprecation
7072
inboundFiltersIntegration,
7173
eventFiltersIntegration,

packages/astro/src/server/sdk.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { applySdkMetadata } from '@sentry/core';
2-
import type { NodeClient, NodeOptions } from '@sentry/node';
2+
import type { Event, NodeClient, NodeOptions } from '@sentry/node';
33
import { init as initNodeSdk } from '@sentry/node';
44

55
/**
@@ -13,5 +13,28 @@ export function init(options: NodeOptions): NodeClient | undefined {
1313

1414
applySdkMetadata(opts, 'astro', ['astro', 'node']);
1515

16-
return initNodeSdk(opts);
16+
const client = initNodeSdk(opts);
17+
18+
client?.addEventProcessor(
19+
Object.assign(
20+
(event: Event) => {
21+
// For http.server spans that did not go though the astro middleware,
22+
// we want to drop them
23+
// this is the case with http.server spans of prerendered pages
24+
// we do not care about those, as they are effectively static
25+
if (
26+
event.type === 'transaction' &&
27+
event.contexts?.trace?.op === 'http.server' &&
28+
event.contexts?.trace?.origin === 'auto.http.otel.http'
29+
) {
30+
return null;
31+
}
32+
33+
return event;
34+
},
35+
{ id: 'AstroHttpEventProcessor' },
36+
),
37+
);
38+
39+
return client;
1740
}

packages/aws-serverless/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export {
5050
disableAnrDetectionForCallback,
5151
consoleIntegration,
5252
httpIntegration,
53+
httpServerIntegration,
54+
httpServerSpansIntegration,
5355
nativeNodeFetchIntegration,
5456
onUncaughtExceptionIntegration,
5557
onUnhandledRejectionIntegration,

packages/bun/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ export {
7070
disableAnrDetectionForCallback,
7171
consoleIntegration,
7272
httpIntegration,
73+
httpServerIntegration,
74+
httpServerSpansIntegration,
7375
nativeNodeFetchIntegration,
7476
onUncaughtExceptionIntegration,
7577
onUnhandledRejectionIntegration,

packages/core/src/client.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type { Integration } from './types-hoist/integration';
2626
import type { Log } from './types-hoist/log';
2727
import type { ClientOptions } from './types-hoist/options';
2828
import type { ParameterizedString } from './types-hoist/parameterize';
29+
import type { RequestEventData } from './types-hoist/request';
2930
import type { SdkMetadata } from './types-hoist/sdkmetadata';
3031
import type { Session, SessionAggregates } from './types-hoist/session';
3132
import type { SeverityLevel } from './types-hoist/severity';
@@ -687,6 +688,17 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
687688
*/
688689
public on(hook: 'flushLogs', callback: () => void): () => void;
689690

691+
/**
692+
* A hook that is called when a http server request is started.
693+
* This hook is called after request isolation, but before the request is processed.
694+
*
695+
* @returns {() => void} A function that, when executed, removes the registered callback.
696+
*/
697+
public on(
698+
hook: 'httpServerRequest',
699+
callback: (request: unknown, response: unknown, normalizedRequest: RequestEventData) => void,
700+
): () => void;
701+
690702
/**
691703
* Register a hook on this client.
692704
*/
@@ -875,6 +887,17 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
875887
*/
876888
public emit(hook: 'flushLogs'): void;
877889

890+
/**
891+
* Emit a hook event for client when a http server request is started.
892+
* This hook is called after request isolation, but before the request is processed.
893+
*/
894+
public emit(
895+
hook: 'httpServerRequest',
896+
request: unknown,
897+
response: unknown,
898+
normalizedRequest: RequestEventData,
899+
): void;
900+
878901
/**
879902
* Emit a hook that was previously registered via `on()`.
880903
*/

packages/core/test/lib/utils/promisebuffer.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,12 @@ describe('PromiseBuffer', () => {
149149
expect(p5).toHaveBeenCalled();
150150

151151
expect(buffer.$.length).toEqual(5);
152-
const result = await buffer.drain(8);
152+
const result = await buffer.drain(6);
153153
expect(result).toEqual(false);
154-
// p5 is still in the buffer
155-
expect(buffer.$.length).toEqual(1);
154+
// p5 & p4 are still in the buffer
155+
// Leaving some wiggle room, possibly one or two items are still in the buffer
156+
// to avoid flakiness
157+
expect(buffer.$.length).toBeGreaterThanOrEqual(1);
156158

157159
// Now drain final item
158160
const result2 = await buffer.drain();

packages/google-cloud-serverless/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export {
5050
disableAnrDetectionForCallback,
5151
consoleIntegration,
5252
httpIntegration,
53+
httpServerIntegration,
54+
httpServerSpansIntegration,
5355
nativeNodeFetchIntegration,
5456
onUncaughtExceptionIntegration,
5557
onUnhandledRejectionIntegration,

packages/node-core/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import * as logger from './logs/exports';
22

33
export { httpIntegration } from './integrations/http';
4+
export { httpServerSpansIntegration } from './integrations/http/httpServerSpansIntegration';
5+
export { httpServerIntegration } from './integrations/http/httpServerIntegration';
6+
47
export {
58
SentryHttpInstrumentation,
69
type SentryHttpInstrumentationOptions,

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

Lines changed: 30 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,15 @@ 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';
65
import { context } from '@opentelemetry/api';
76
import { isTracingSuppressed } from '@opentelemetry/core';
87
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
98
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
9+
import type { Span } from '@sentry/core';
1010
import { debug, LRUMap, SDK_VERSION } from '@sentry/core';
1111
import { DEBUG_BUILD } from '../../debug-build';
1212
import { getRequestUrl } from '../../utils/getRequestUrl';
1313
import { INSTRUMENTATION_NAME } from './constants';
14-
import { instrumentServer } from './incoming-requests';
1514
import {
1615
addRequestBreadcrumb,
1716
addTracePropagationHeadersToOutgoingRequest,
@@ -23,31 +22,12 @@ type Https = typeof https;
2322

2423
export type SentryHttpInstrumentationOptions = InstrumentationConfig & {
2524
/**
26-
* Whether breadcrumbs should be recorded for requests.
25+
* Whether breadcrumbs should be recorded for outgoing requests.
2726
*
2827
* @default `true`
2928
*/
3029
breadcrumbs?: boolean;
3130

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-
40-
/**
41-
* Whether to extract the trace ID from the `sentry-trace` header for incoming requests.
42-
* By default this is done by the HttpInstrumentation, but if that is not added (e.g. because tracing is disabled, ...)
43-
* then this instrumentation can take over.
44-
*
45-
* @deprecated This is always true and the option will be removed in the future.
46-
*
47-
* @default `true`
48-
*/
49-
extractIncomingTraceFromHeader?: boolean;
50-
5131
/**
5232
* Whether to propagate Sentry trace headers in outgoing requests.
5333
* By default this is done by the HttpInstrumentation, but if that is not added (e.g. because tracing is disabled)
@@ -57,20 +37,6 @@ export type SentryHttpInstrumentationOptions = InstrumentationConfig & {
5737
*/
5838
propagateTraceInOutgoingRequests?: boolean;
5939

60-
/**
61-
* Whether to automatically ignore common static asset requests like favicon.ico, robots.txt, etc.
62-
* This helps reduce noise in your transactions.
63-
*
64-
* @default `true`
65-
*/
66-
ignoreStaticAssets?: boolean;
67-
68-
/**
69-
* If true, do not generate spans for incoming requests at all.
70-
* This is used by Remix to avoid generating spans for incoming requests, as it generates its own spans.
71-
*/
72-
disableIncomingRequestSpans?: boolean;
73-
7440
/**
7541
* Do not capture breadcrumbs for outgoing HTTP requests to URLs where the given callback returns `true`.
7642
* For the scope of this instrumentation, this callback only controls breadcrumb creation.
@@ -82,55 +48,51 @@ export type SentryHttpInstrumentationOptions = InstrumentationConfig & {
8248
*/
8349
ignoreOutgoingRequests?: (url: string, request: http.RequestOptions) => boolean;
8450

51+
// All options below do not do anything anymore in this instrumentation, and will be removed in the future.
52+
// They are only kept here for backwards compatibility - the respective functionality is now handled by the httpServerIntegration/httpServerSpansIntegration.
53+
8554
/**
86-
* Do not capture spans for incoming HTTP requests to URLs where the given callback returns `true`.
87-
*
88-
* @param urlPath Contains the URL path and query string (if any) of the incoming request.
89-
* @param request Contains the {@type IncomingMessage} object of the incoming request.
55+
* @deprecated This no longer does anything.
9056
*/
91-
ignoreSpansForIncomingRequests?: (urlPath: string, request: http.IncomingMessage) => boolean;
57+
spans?: boolean;
9258

9359
/**
94-
* Do not capture the request body for incoming HTTP requests to URLs where the given callback returns `true`.
95-
* This can be useful for long running requests where the body is not needed and we want to avoid capturing it.
96-
*
97-
* @param url Contains the entire URL, including query string (if any), protocol, host, etc. of the incoming request.
98-
* @param request Contains the {@type RequestOptions} object used to make the incoming request.
60+
* @depreacted This no longer does anything.
9961
*/
100-
ignoreIncomingRequestBody?: (url: string, request: http.RequestOptions) => boolean;
62+
extractIncomingTraceFromHeader?: boolean;
10163

10264
/**
103-
* A hook that can be used to mutate the span for incoming requests.
104-
* This is triggered after the span is created, but before it is recorded.
65+
* @deprecated This no longer does anything.
10566
*/
106-
incomingRequestSpanHook?: (span: Span, request: http.IncomingMessage, response: http.ServerResponse) => void;
67+
ignoreStaticAssets?: boolean;
10768

10869
/**
109-
* Controls the maximum size of incoming HTTP request bodies attached to events.
110-
*
111-
* Available options:
112-
* - 'none': No request bodies will be attached
113-
* - 'small': Request bodies up to 1,000 bytes will be attached
114-
* - 'medium': Request bodies up to 10,000 bytes will be attached (default)
115-
* - 'always': Request bodies will always be attached
116-
*
117-
* Note that even with 'always' setting, bodies exceeding 1MB will never be attached
118-
* for performance and security reasons.
119-
*
120-
* @default 'medium'
70+
* @deprecated This no longer does anything.
71+
*/
72+
disableIncomingRequestSpans?: boolean;
73+
74+
/**
75+
* @deprecated This no longer does anything.
76+
*/
77+
ignoreSpansForIncomingRequests?: (urlPath: string, request: http.IncomingMessage) => boolean;
78+
79+
/**
80+
* @deprecated This no longer does anything.
81+
*/
82+
ignoreIncomingRequestBody?: (url: string, request: http.RequestOptions) => boolean;
83+
84+
/**
85+
* @deprecated This no longer does anything.
12186
*/
12287
maxIncomingRequestBodySize?: 'none' | 'small' | 'medium' | 'always';
12388

12489
/**
125-
* Whether the integration should create [Sessions](https://docs.sentry.io/product/releases/health/#sessions) for incoming requests to track the health and crash-free rate of your releases in Sentry.
126-
* Read more about Release Health: https://docs.sentry.io/product/releases/health/
127-
*
128-
* Defaults to `true`.
90+
* @deprecated This no longer does anything.
12991
*/
13092
trackIncomingRequestsAsSessions?: boolean;
13193

13294
/**
133-
* @deprecated This is deprecated in favor of `incomingRequestSpanHook`.
95+
* @deprecated This no longer does anything.
13496
*/
13597
instrumentation?: {
13698
requestHook?: (span: Span, req: http.ClientRequest | http.IncomingMessage) => void;
@@ -143,9 +105,7 @@ export type SentryHttpInstrumentationOptions = InstrumentationConfig & {
143105
};
144106

145107
/**
146-
* Number of milliseconds until sessions tracked with `trackIncomingRequestsAsSessions` will be flushed as a session aggregate.
147-
*
148-
* Defaults to `60000` (60s).
108+
* @deprecated This no longer does anything.
149109
*/
150110
sessionFlushingDelayMS?: number;
151111
};
@@ -180,24 +140,6 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
180140
// but we only want to register them once, whichever is loaded first
181141
let hasRegisteredHandlers = false;
182142

183-
const spansEnabled = this.getConfig().spans ?? true;
184-
185-
const onHttpServerRequestStart = ((_data: unknown) => {
186-
const data = _data as { server: http.Server };
187-
instrumentServer(data.server, {
188-
// eslint-disable-next-line deprecation/deprecation
189-
instrumentation: this.getConfig().instrumentation,
190-
ignoreIncomingRequestBody: this.getConfig().ignoreIncomingRequestBody,
191-
ignoreSpansForIncomingRequests: this.getConfig().ignoreSpansForIncomingRequests,
192-
incomingRequestSpanHook: this.getConfig().incomingRequestSpanHook,
193-
maxIncomingRequestBodySize: this.getConfig().maxIncomingRequestBodySize,
194-
trackIncomingRequestsAsSessions: this.getConfig().trackIncomingRequestsAsSessions,
195-
sessionFlushingDelayMS: this.getConfig().sessionFlushingDelayMS ?? 60_000,
196-
ignoreStaticAssets: this.getConfig().ignoreStaticAssets,
197-
spans: spansEnabled && !this.getConfig().disableIncomingRequestSpans,
198-
});
199-
}) satisfies ChannelListener;
200-
201143
const onHttpClientResponseFinish = ((_data: unknown) => {
202144
const data = _data as { request: http.ClientRequest; response: http.IncomingMessage };
203145
this._onOutgoingRequestFinish(data.request, data.response);
@@ -220,7 +162,6 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
220162

221163
hasRegisteredHandlers = true;
222164

223-
subscribe('http.server.request.start', onHttpServerRequestStart);
224165
subscribe('http.client.response.finish', onHttpClientResponseFinish);
225166

226167
// When an error happens, we still want to have a breadcrumb
@@ -238,7 +179,6 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
238179
};
239180

240181
const unwrap = (): void => {
241-
unsubscribe('http.server.request.start', onHttpServerRequestStart);
242182
unsubscribe('http.client.response.finish', onHttpClientResponseFinish);
243183
unsubscribe('http.client.request.error', onHttpClientRequestError);
244184
unsubscribe('http.client.request.created', onHttpClientRequestCreated);

0 commit comments

Comments
 (0)