1
- import type { Hub } from '@sentry/core' ;
1
+ import type { Hub , Span } from '@sentry/core' ;
2
+ import { stripUrlQueryAndFragment } from '@sentry/core' ;
2
3
import type { EventProcessor , Integration } from '@sentry/types' ;
4
+ import { dynamicSamplingContextToSentryBaggageHeader , stringMatchesSomePattern } from '@sentry/utils' ;
3
5
import type DiagnosticsChannel from 'diagnostics_channel' ;
4
6
7
+ import type { NodeClient } from '../client' ;
8
+ import { isSentryRequest } from './utils/http' ;
9
+
10
+ enum ChannelName {
11
+ // https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/docs/api/DiagnosticsChannel.md#undicirequestcreate
12
+ RequestCreate = 'undici:request:create' ,
13
+ RequestEnd = 'undici:request:headers' ,
14
+ RequestError = 'undici:request:error' ,
15
+ }
16
+
17
+ interface RequestWithSentry extends DiagnosticsChannel . Request {
18
+ __sentry__ ?: Span ;
19
+ }
20
+
21
+ interface RequestCreateMessage {
22
+ request : RequestWithSentry ;
23
+ }
24
+
25
+ interface RequestEndMessage {
26
+ request : RequestWithSentry ;
27
+ response : DiagnosticsChannel . Response ;
28
+ }
29
+
30
+ interface RequestErrorMessage {
31
+ request : RequestWithSentry ;
32
+ error : Error ;
33
+ }
34
+
35
+ interface UndiciOptions {
36
+ /**
37
+ * Whether breadcrumbs should be recorded for requests
38
+ * Defaults to true
39
+ */
40
+ breadcrumbs : boolean ;
41
+ }
42
+
43
+ const DEFAULT_UNDICI_OPTIONS : UndiciOptions = {
44
+ breadcrumbs : true ,
45
+ } ;
46
+
5
47
/** */
6
48
export class Undici implements Integration {
7
49
/**
@@ -17,12 +59,21 @@ export class Undici implements Integration {
17
59
// Have to hold all built channels in memory otherwise they get garbage collected
18
60
// See: https://github.com/nodejs/node/pull/42714
19
61
// This has been fixed in Node 19+
20
- private _channels : Map < string , DiagnosticsChannel . Channel > = new Map ( ) ;
62
+ private _channels = new Set < DiagnosticsChannel . Channel > ( ) ;
63
+
64
+ private readonly _options : UndiciOptions ;
65
+
66
+ public constructor ( _options : UndiciOptions ) {
67
+ this . _options = {
68
+ ...DEFAULT_UNDICI_OPTIONS ,
69
+ ..._options ,
70
+ } ;
71
+ }
21
72
22
73
/**
23
74
* @inheritDoc
24
75
*/
25
- public setupOnce ( _addGlobalEventProcessor : ( callback : EventProcessor ) => void , _getCurrentHub : ( ) => Hub ) : void {
76
+ public setupOnce ( _addGlobalEventProcessor : ( callback : EventProcessor ) => void , getCurrentHub : ( ) => Hub ) : void {
26
77
let ds : typeof DiagnosticsChannel | undefined ;
27
78
try {
28
79
// eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -35,12 +86,146 @@ export class Undici implements Integration {
35
86
return ;
36
87
}
37
88
38
- // https://github.com/nodejs/undici/blob/main/docs/api/DiagnosticsChannel.md
39
- const undiciChannel = ds . channel ( 'undici:request' ) ;
89
+ // https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/docs/api/DiagnosticsChannel.md
90
+ const requestCreateChannel = this . _setupChannel ( ds , ChannelName . RequestCreate ) ;
91
+ requestCreateChannel . subscribe ( message => {
92
+ const { request } = message as RequestCreateMessage ;
93
+
94
+ const url = new URL ( request . path , request . origin ) ;
95
+ const stringUrl = url . toString ( ) ;
96
+
97
+ if ( isSentryRequest ( stringUrl ) ) {
98
+ return ;
99
+ }
100
+
101
+ const hub = getCurrentHub ( ) ;
102
+ const client = hub . getClient < NodeClient > ( ) ;
103
+ const scope = hub . getScope ( ) ;
104
+
105
+ const activeSpan = scope . getSpan ( ) ;
106
+
107
+ if ( activeSpan && client ) {
108
+ const options = client . getOptions ( ) ;
109
+
110
+ // eslint-disable-next-line deprecation/deprecation
111
+ const shouldCreateSpan = options . shouldCreateSpanForRequest
112
+ ? // eslint-disable-next-line deprecation/deprecation
113
+ options . shouldCreateSpanForRequest ( stringUrl )
114
+ : true ;
115
+
116
+ if ( shouldCreateSpan ) {
117
+ const span = activeSpan . startChild ( {
118
+ op : 'http.client' ,
119
+ description : `${ request . method || 'GET' } ${ stripUrlQueryAndFragment ( stringUrl ) } ` ,
120
+ data : {
121
+ 'http.query' : `?${ url . searchParams . toString ( ) } ` ,
122
+ 'http.fragment' : url . hash ,
123
+ } ,
124
+ } ) ;
125
+ request . __sentry__ = span ;
126
+
127
+ // eslint-disable-next-line deprecation/deprecation
128
+ const shouldPropagate = options . tracePropagationTargets
129
+ ? // eslint-disable-next-line deprecation/deprecation
130
+ stringMatchesSomePattern ( stringUrl , options . tracePropagationTargets )
131
+ : true ;
132
+
133
+ if ( shouldPropagate ) {
134
+ // TODO: Only do this based on tracePropagationTargets
135
+ request . addHeader ( 'sentry-trace' , span . toTraceparent ( ) ) ;
136
+ if ( span . transaction ) {
137
+ const dynamicSamplingContext = span . transaction . getDynamicSamplingContext ( ) ;
138
+ const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader ( dynamicSamplingContext ) ;
139
+ if ( sentryBaggageHeader ) {
140
+ request . addHeader ( 'baggage' , sentryBaggageHeader ) ;
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+ } ) ;
147
+
148
+ const requestEndChannel = this . _setupChannel ( ds , ChannelName . RequestEnd ) ;
149
+ requestEndChannel . subscribe ( message => {
150
+ const { request, response } = message as RequestEndMessage ;
151
+
152
+ const url = new URL ( request . path , request . origin ) ;
153
+ const stringUrl = url . toString ( ) ;
154
+
155
+ if ( isSentryRequest ( stringUrl ) ) {
156
+ return ;
157
+ }
158
+
159
+ const span = request . __sentry__ ;
160
+ if ( span ) {
161
+ span . setHttpStatus ( response . statusCode ) ;
162
+ span . finish ( ) ;
163
+ }
164
+
165
+ if ( this . _options . breadcrumbs ) {
166
+ getCurrentHub ( ) . addBreadcrumb (
167
+ {
168
+ category : 'http' ,
169
+ data : {
170
+ method : request . method ,
171
+ status_code : response . statusCode ,
172
+ url : stringUrl ,
173
+ } ,
174
+ type : 'http' ,
175
+ } ,
176
+ {
177
+ event : 'response' ,
178
+ request,
179
+ response,
180
+ } ,
181
+ ) ;
182
+ }
183
+ } ) ;
184
+
185
+ const requestErrorChannel = this . _setupChannel ( ds , ChannelName . RequestError ) ;
186
+ requestErrorChannel . subscribe ( message => {
187
+ const { request } = message as RequestErrorMessage ;
188
+
189
+ const url = new URL ( request . path , request . origin ) ;
190
+ const stringUrl = url . toString ( ) ;
191
+
192
+ if ( isSentryRequest ( stringUrl ) ) {
193
+ return ;
194
+ }
195
+
196
+ const span = request . __sentry__ ;
197
+ if ( span ) {
198
+ span . setStatus ( 'internal_error' ) ;
199
+ span . finish ( ) ;
200
+ }
201
+
202
+ if ( this . _options . breadcrumbs ) {
203
+ getCurrentHub ( ) . addBreadcrumb (
204
+ {
205
+ category : 'http' ,
206
+ data : {
207
+ method : request . method ,
208
+ url : stringUrl ,
209
+ } ,
210
+ level : 'error' ,
211
+ type : 'http' ,
212
+ } ,
213
+ {
214
+ event : 'error' ,
215
+ request,
216
+ } ,
217
+ ) ;
218
+ }
219
+ } ) ;
40
220
}
41
221
42
- private _setupChannel ( name : Parameters < typeof DiagnosticsChannel . channel > [ 0 ] ) : void {
43
- const channel = DiagnosticsChannel . channel ( name ) ;
44
- if ( node )
222
+ /** */
223
+ private _setupChannel (
224
+ ds : typeof DiagnosticsChannel ,
225
+ name : Parameters < typeof DiagnosticsChannel . channel > [ 0 ] ,
226
+ ) : DiagnosticsChannel . Channel {
227
+ const channel = ds . channel ( name ) ;
228
+ this . _channels . add ( channel ) ;
229
+ return channel ;
45
230
}
46
231
}
0 commit comments