1- import type { Hub } from '@sentry/core' ;
1+ import type { Hub , Span } from '@sentry/core' ;
2+ import { stripUrlQueryAndFragment } from '@sentry/core' ;
23import type { EventProcessor , Integration } from '@sentry/types' ;
4+ import { dynamicSamplingContextToSentryBaggageHeader , stringMatchesSomePattern } from '@sentry/utils' ;
35import type DiagnosticsChannel from 'diagnostics_channel' ;
46
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+
547/** */
648export class Undici implements Integration {
749 /**
@@ -17,12 +59,21 @@ export class Undici implements Integration {
1759 // Have to hold all built channels in memory otherwise they get garbage collected
1860 // See: https://github.com/nodejs/node/pull/42714
1961 // 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+ }
2172
2273 /**
2374 * @inheritDoc
2475 */
25- public setupOnce ( _addGlobalEventProcessor : ( callback : EventProcessor ) => void , _getCurrentHub : ( ) => Hub ) : void {
76+ public setupOnce ( _addGlobalEventProcessor : ( callback : EventProcessor ) => void , getCurrentHub : ( ) => Hub ) : void {
2677 let ds : typeof DiagnosticsChannel | undefined ;
2778 try {
2879 // eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -35,12 +86,146 @@ export class Undici implements Integration {
3586 return ;
3687 }
3788
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+ } ) ;
40220 }
41221
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 ;
45230 }
46231}
0 commit comments