1
1
import type { ClientRequest , IncomingMessage , RequestOptions , ServerResponse } from 'node:http' ;
2
2
import { diag } from '@opentelemetry/api' ;
3
- import { HttpInstrumentation } from '@opentelemetry/instrumentation-http' ;
4
- import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry' ;
3
+ import { HttpInstrumentation , HttpInstrumentationConfig } from '@opentelemetry/instrumentation-http' ;
5
4
6
5
import { defineIntegration } from '@sentry/core' ;
7
6
import { getClient } from '@sentry/opentelemetry' ;
8
7
import type { IntegrationFn , Span } from '@sentry/types' ;
9
8
9
+ import { generateInstrumentOnce } from '../../otel/instrument' ;
10
10
import type { NodeClient } from '../../sdk/client' ;
11
11
import type { HTTPModuleRequestIncomingMessage } from '../../transports/http-module' ;
12
12
import { addOriginToSpan } from '../../utils/addOriginToSpan' ;
@@ -55,6 +55,12 @@ interface HttpOptions {
55
55
*/
56
56
ignoreIncomingRequests ?: ( urlPath : string , request : IncomingMessage ) => boolean ;
57
57
58
+ /**
59
+ * If true, do not generate spans for incoming requests at all.
60
+ * This is used by Remix to avoid generating spans for incoming requests, as it generates its own spans.
61
+ */
62
+ disableIncomingRequestSpans ?: boolean ;
63
+
58
64
/**
59
65
* Additional instrumentation options that are passed to the underlying HttpInstrumentation.
60
66
*/
@@ -73,35 +79,53 @@ interface HttpOptions {
73
79
*/
74
80
_experimentalConfig ?: ConstructorParameters < typeof HttpInstrumentation > [ 0 ] ;
75
81
} ;
76
-
77
- /** Allows to pass a custom version of HttpInstrumentation. We use this for Next.js. */
78
- _instrumentation ?: typeof HttpInstrumentation ;
79
82
}
80
83
81
- let _httpOptions : HttpOptions = { } ;
82
- let _sentryHttpInstrumentation : SentryHttpInstrumentation | undefined ;
83
- let _httpInstrumentation : HttpInstrumentation | undefined ;
84
+ const instrumentSentryHttp = generateInstrumentOnce < { breadcrumbs ?: boolean } > (
85
+ `${ INTEGRATION_NAME } .sentry` ,
86
+ options => {
87
+ return new SentryHttpInstrumentation ( { breadcrumbs : options ?. breadcrumbs } ) ;
88
+ } ,
89
+ ) ;
90
+
91
+ const instrumentOtelHttp = generateInstrumentOnce < HttpInstrumentationConfig > ( `${ INTEGRATION_NAME } .otel` , config => {
92
+ const instrumentation = new HttpInstrumentation ( config ) ;
93
+
94
+ // We want to update the logger namespace so we can better identify what is happening here
95
+ try {
96
+ instrumentation [ '_diag' ] = diag . createComponentLogger ( {
97
+ namespace : INSTRUMENTATION_NAME ,
98
+ } ) ;
99
+ // @ts -expect-error We are writing a read-only property here...
100
+ instrumentation . instrumentationName = INSTRUMENTATION_NAME ;
101
+ } catch {
102
+ // ignore errors here...
103
+ }
104
+
105
+ return instrumentation ;
106
+ } ) ;
84
107
85
108
/**
86
109
* Instrument the HTTP module.
87
110
* This can only be instrumented once! If this called again later, we just update the options.
88
111
*/
89
112
export const instrumentHttp = Object . assign (
90
- function ( ) : void {
113
+ function ( options : HttpOptions = { } ) {
91
114
// This is the "regular" OTEL instrumentation that emits spans
92
- if ( _httpOptions . spans !== false && ! _httpInstrumentation ) {
93
- const _InstrumentationClass = _httpOptions . _instrumentation || HttpInstrumentation ;
115
+ if ( options . spans !== false ) {
116
+ const instrumentationConfig = {
117
+ ...options . instrumentation ?. _experimentalConfig ,
118
+
119
+ disableIncomingRequestInstrumentation : options . disableIncomingRequestSpans ,
94
120
95
- _httpInstrumentation = new _InstrumentationClass ( {
96
- ..._httpOptions . instrumentation ?. _experimentalConfig ,
97
121
ignoreOutgoingRequestHook : request => {
98
122
const url = getRequestUrl ( request ) ;
99
123
100
124
if ( ! url ) {
101
125
return false ;
102
126
}
103
127
104
- const _ignoreOutgoingRequests = _httpOptions . ignoreOutgoingRequests ;
128
+ const _ignoreOutgoingRequests = options . ignoreOutgoingRequests ;
105
129
if ( _ignoreOutgoingRequests && _ignoreOutgoingRequests ( url , request ) ) {
106
130
return true ;
107
131
}
@@ -120,7 +144,7 @@ export const instrumentHttp = Object.assign(
120
144
return true ;
121
145
}
122
146
123
- const _ignoreIncomingRequests = _httpOptions . ignoreIncomingRequests ;
147
+ const _ignoreIncomingRequests = options . ignoreIncomingRequests ;
124
148
if ( urlPath && _ignoreIncomingRequests && _ignoreIncomingRequests ( urlPath , request ) ) {
125
149
return true ;
126
150
}
@@ -136,7 +160,7 @@ export const instrumentHttp = Object.assign(
136
160
span . setAttribute ( 'sentry.http.prefetch' , true ) ;
137
161
}
138
162
139
- _httpOptions . instrumentation ?. requestHook ?.( span , req ) ;
163
+ options . instrumentation ?. requestHook ?.( span , req ) ;
140
164
} ,
141
165
responseHook : ( span , res ) => {
142
166
const client = getClient < NodeClient > ( ) ;
@@ -146,42 +170,21 @@ export const instrumentHttp = Object.assign(
146
170
} ) ;
147
171
}
148
172
149
- _httpOptions . instrumentation ?. responseHook ?.( span , res ) ;
173
+ options . instrumentation ?. responseHook ?.( span , res ) ;
150
174
} ,
151
175
applyCustomAttributesOnSpan : (
152
176
span : Span ,
153
177
request : ClientRequest | HTTPModuleRequestIncomingMessage ,
154
178
response : HTTPModuleRequestIncomingMessage | ServerResponse ,
155
179
) => {
156
- _httpOptions . instrumentation ?. applyCustomAttributesOnSpan ?.( span , request , response ) ;
180
+ options . instrumentation ?. applyCustomAttributesOnSpan ?.( span , request , response ) ;
157
181
} ,
158
- } ) ;
159
-
160
- // We want to update the logger namespace so we can better identify what is happening here
161
- try {
162
- _httpInstrumentation [ '_diag' ] = diag . createComponentLogger ( {
163
- namespace : INSTRUMENTATION_NAME ,
164
- } ) ;
165
- // @ts -expect-error We are writing a read-only property here...
166
- _httpInstrumentation . instrumentationName = INSTRUMENTATION_NAME ;
167
- } catch {
168
- // ignore errors here...
169
- }
170
-
171
- addOpenTelemetryInstrumentation ( _httpInstrumentation ) ;
172
- } else if ( _httpOptions . spans === false && _httpInstrumentation ) {
173
- _httpInstrumentation . disable ( ) ;
174
- }
182
+ } satisfies HttpInstrumentationConfig ;
175
183
176
- // This is our custom instrumentation that is responsible for request isolation etc.
177
- // We have to add it after the OTEL instrumentation to ensure that we wrap the already wrapped http module
178
- // Otherwise, the isolation scope does not encompass the OTEL spans
179
- if ( ! _sentryHttpInstrumentation ) {
180
- _sentryHttpInstrumentation = new SentryHttpInstrumentation ( { breadcrumbs : _httpOptions . breadcrumbs } ) ;
181
- addOpenTelemetryInstrumentation ( _sentryHttpInstrumentation ) ;
182
- } else {
183
- _sentryHttpInstrumentation . setConfig ( { breadcrumbs : _httpOptions . breadcrumbs } ) ;
184
+ instrumentOtelHttp ( instrumentationConfig ) ;
184
185
}
186
+
187
+ instrumentSentryHttp ( options ) ;
185
188
} ,
186
189
{
187
190
id : INTEGRATION_NAME ,
@@ -192,8 +195,7 @@ const _httpIntegration = ((options: HttpOptions = {}) => {
192
195
return {
193
196
name : INTEGRATION_NAME ,
194
197
setupOnce ( ) {
195
- _httpOptions = options ;
196
- instrumentHttp ( ) ;
198
+ instrumentHttp ( options ) ;
197
199
} ,
198
200
} ;
199
201
} ) satisfies IntegrationFn ;
0 commit comments