@@ -53,108 +53,112 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void): void {
5353 return ;
5454 }
5555
56- fill ( GLOBAL_OBJ , 'fetch' , function ( originalFetch : ( ) => void ) : ( ) => void {
57- return function ( ...args : any [ ] ) : void {
58- // We capture the error right here and not in the Promise error callback because Safari (and probably other
59- // browsers too) will wipe the stack trace up to this point, only leaving us with this file which is useless.
60-
61- // NOTE: If you are a Sentry user, and you are seeing this stack frame,
62- // it means the error, that was caused by your fetch call did not
63- // have a stack trace, so the SDK backfilled the stack trace so
64- // you can see which fetch call failed.
65- const virtualError = new Error ( ) ;
66-
67- const { method, url } = parseFetchArgs ( args ) ;
68- const handlerData : HandlerDataFetch = {
69- args,
70- fetchData : {
71- method,
72- url,
73- } ,
74- startTimestamp : timestampInSeconds ( ) * 1000 ,
75- // // Adding the error to be able to fingerprint the failed fetch event in HttpClient instrumentation
76- virtualError,
77- headers : getHeadersFromFetchArgs ( args ) ,
78- } ;
79-
80- // if there is no callback, fetch is instrumented directly
81- if ( ! onFetchResolved ) {
82- triggerHandlers ( 'fetch' , {
83- ...handlerData ,
84- } ) ;
85- }
56+ // We wrap `fetch` in a `Proxy` rather than a plain closure so that non-standard own properties some
57+ // runtimes hang off the global `fetch` (e.g. Bun's `fetch.preconnect`) keep working - property access
58+ // and `toString()` transparently forward to the native implementation.
59+ fill ( GLOBAL_OBJ , 'fetch' , function ( originalFetch : ( ...args : any [ ] ) => Promise < Response > ) : unknown {
60+ return new Proxy ( originalFetch , {
61+ apply ( target , _thisArg , args : any [ ] ) : Promise < Response > {
62+ // We capture the error right here and not in the Promise error callback because Safari (and probably other
63+ // browsers too) will wipe the stack trace up to this point, only leaving us with this file which is useless.
64+
65+ // NOTE: If you are a Sentry user, and you are seeing this stack frame,
66+ // it means the error, that was caused by your fetch call did not
67+ // have a stack trace, so the SDK backfilled the stack trace so
68+ // you can see which fetch call failed.
69+ const virtualError = new Error ( ) ;
70+
71+ const { method, url } = parseFetchArgs ( args ) ;
72+ const handlerData : HandlerDataFetch = {
73+ args,
74+ fetchData : {
75+ method,
76+ url,
77+ } ,
78+ startTimestamp : timestampInSeconds ( ) * 1000 ,
79+ // // Adding the error to be able to fingerprint the failed fetch event in HttpClient instrumentation
80+ virtualError,
81+ headers : getHeadersFromFetchArgs ( args ) ,
82+ } ;
83+
84+ // if there is no callback, fetch is instrumented directly
85+ if ( ! onFetchResolved ) {
86+ triggerHandlers ( 'fetch' , {
87+ ...handlerData ,
88+ } ) ;
89+ }
90+
91+ return Reflect . apply ( target , GLOBAL_OBJ , args ) . then (
92+ async ( response : Response ) => {
93+ if ( onFetchResolved ) {
94+ onFetchResolved ( response ) ;
95+ } else {
96+ triggerHandlers ( 'fetch' , {
97+ ...handlerData ,
98+ endTimestamp : timestampInSeconds ( ) * 1000 ,
99+ response,
100+ } ) ;
101+ }
86102
87- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
88- return originalFetch . apply ( GLOBAL_OBJ , args ) . then (
89- async ( response : Response ) => {
90- if ( onFetchResolved ) {
91- onFetchResolved ( response ) ;
92- } else {
103+ return response ;
104+ } ,
105+ ( error : Error ) => {
93106 triggerHandlers ( 'fetch' , {
94107 ...handlerData ,
95108 endTimestamp : timestampInSeconds ( ) * 1000 ,
96- response ,
109+ error ,
97110 } ) ;
98- }
99111
100- return response ;
101- } ,
102- ( error : Error ) => {
103- triggerHandlers ( 'fetch' , {
104- ... handlerData ,
105- endTimestamp : timestampInSeconds ( ) * 1000 ,
106- error,
107- } ) ;
112+ if ( isError ( error ) && error . stack === undefined ) {
113+ // NOTE: If you are a Sentry user, and you are seeing this stack frame ,
114+ // it means the error, that was caused by your fetch call did not
115+ // have a stack trace, so the SDK backfilled the stack trace so
116+ // you can see which fetch call failed.
117+ error . stack = virtualError . stack ;
118+ addNonEnumerableProperty ( error , 'framesToPop' , 1 ) ;
119+ }
108120
109- if ( isError ( error ) && error . stack === undefined ) {
110- // NOTE: If you are a Sentry user, and you are seeing this stack frame,
111- // it means the error, that was caused by your fetch call did not
112- // have a stack trace, so the SDK backfilled the stack trace so
113- // you can see which fetch call failed.
114- error . stack = virtualError . stack ;
115- addNonEnumerableProperty ( error , 'framesToPop' , 1 ) ;
116- }
117-
118- // We enhance fetch error messages with hostname information based on the configuration.
119- // Possible messages we handle here:
120- // * "Failed to fetch" (chromium)
121- // * "Load failed" (webkit)
122- // * "NetworkError when attempting to fetch resource." (firefox)
123- const client = getClient ( ) ;
124- const enhanceOption = client ?. getOptions ( ) . enhanceFetchErrorMessages ?? 'always' ;
125- const shouldEnhance = enhanceOption !== false ;
126-
127- if (
128- shouldEnhance &&
129- error instanceof TypeError &&
130- ( error . message === 'Failed to fetch' ||
131- error . message === 'Load failed' ||
132- error . message === 'NetworkError when attempting to fetch resource.' )
133- ) {
134- try {
135- const url = new URL ( handlerData . fetchData . url ) ;
136- const hostname = url . host ;
137-
138- if ( enhanceOption === 'always' ) {
139- // Modify the error message directly
140- error . message = `${ error . message } (${ hostname } )` ;
141- } else {
142- // Store hostname as non-enumerable property for Sentry-only enhancement
143- // This preserves the original error message for third-party packages
144- addNonEnumerableProperty ( error , '__sentry_fetch_url_host__' , hostname ) ;
121+ // We enhance fetch error messages with hostname information based on the configuration.
122+ // Possible messages we handle here:
123+ // * "Failed to fetch" (chromium)
124+ // * "Load failed" (webkit)
125+ // * "NetworkError when attempting to fetch resource." (firefox)
126+ const client = getClient ( ) ;
127+ const enhanceOption = client ?. getOptions ( ) . enhanceFetchErrorMessages ?? 'always' ;
128+ const shouldEnhance = enhanceOption !== false ;
129+
130+ if (
131+ shouldEnhance &&
132+ error instanceof TypeError &&
133+ ( error . message === 'Failed to fetch' ||
134+ error . message === 'Load failed' ||
135+ error . message === 'NetworkError when attempting to fetch resource.' )
136+ ) {
137+ try {
138+ const url = new URL ( handlerData . fetchData . url ) ;
139+ const hostname = url . host ;
140+
141+ if ( enhanceOption === 'always' ) {
142+ // Modify the error message directly
143+ error . message = `${ error . message } (${ hostname } )` ;
144+ } else {
145+ // Store hostname as non-enumerable property for Sentry-only enhancement
146+ // This preserves the original error message for third-party packages
147+ addNonEnumerableProperty ( error , '__sentry_fetch_url_host__' , hostname ) ;
148+ }
149+ } catch {
150+ // ignore it if errors happen here
145151 }
146- } catch {
147- // ignore it if errors happen here
148152 }
149- }
150-
151- // NOTE: If you are a Sentry user, and you are seeing this stack frame,
152- // it means the sentry.javascript SDK caught an error invoking your application code .
153- // This is expected behavior and NOT indicative of a bug with sentry.javascript.
154- throw error ;
155- } ,
156- ) ;
157- } ;
153+
154+ // NOTE: If you are a Sentry user, and you are seeing this stack frame,
155+ // it means the sentry.javascript SDK caught an error invoking your application code.
156+ // This is expected behavior and NOT indicative of a bug with sentry.javascript .
157+ throw error ;
158+ } ,
159+ ) ;
160+ } ,
161+ } ) ;
158162 } ) ;
159163}
160164
0 commit comments