@@ -23,7 +23,7 @@ type FetchResource = string | { toString(): string } | { url: string };
2323export function addFetchInstrumentationHandler ( handler : ( data : HandlerDataFetch ) => void ) : void {
2424 const type = 'fetch' ;
2525 addHandler ( type , handler ) ;
26- maybeInstrument ( type , instrumentFetch ) ;
26+ maybeInstrument ( type , ( ) => instrumentFetch ( type ) ) ;
2727}
2828
2929/**
@@ -37,18 +37,17 @@ export function addFetchInstrumentationHandler(handler: (data: HandlerDataFetch)
3737export function addFetchEndInstrumentationHandler ( handler : ( data : HandlerDataFetch ) => void ) : void {
3838 const type = 'fetch-body-resolved' ;
3939 addHandler ( type , handler ) ;
40- maybeInstrument ( type , instrumentFetchBodyReceived ) ;
40+ maybeInstrument ( type , ( ) => instrumentFetch ( type ) ) ;
4141}
4242
43- function instrumentFetch ( ) : void {
43+ function instrumentFetch ( handlerType : 'fetch' | 'fetch-body-resolved' ) : void {
4444 if ( ! supportsNativeFetch ( ) ) {
4545 return ;
4646 }
4747
4848 fill ( GLOBAL_OBJ , 'fetch' , function ( originalFetch : ( ) => void ) : ( ) => void {
4949 return function ( ...args : any [ ] ) : void {
5050 const { method, url } = parseFetchArgs ( args ) ;
51-
5251 const handlerData : HandlerDataFetch = {
5352 args,
5453 fetchData : {
@@ -58,9 +57,11 @@ function instrumentFetch(): void {
5857 startTimestamp : timestampInSeconds ( ) * 1000 ,
5958 } ;
6059
61- triggerHandlers ( 'fetch' , {
62- ...handlerData ,
63- } ) ;
60+ if ( handlerType === 'fetch' ) {
61+ triggerHandlers ( 'fetch' , {
62+ ...handlerData ,
63+ } ) ;
64+ }
6465
6566 // We capture the stack right here and not in the Promise error callback because Safari (and probably other
6667 // browsers too) will wipe the stack trace up to this point, only leaving us with this file which is useless.
@@ -73,91 +74,100 @@ function instrumentFetch(): void {
7374
7475 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
7576 return originalFetch . apply ( GLOBAL_OBJ , args ) . then (
76- ( response : Response ) => {
77- const finishedHandlerData : HandlerDataFetch = {
78- ...handlerData ,
79- endTimestamp : timestampInSeconds ( ) * 1000 ,
80- response,
81- } ;
82-
83- triggerHandlers ( 'fetch' , finishedHandlerData ) ;
77+ async ( response : Response ) => {
78+ if ( handlerType === 'fetch-body-resolved' ) {
79+ // clone response for awaiting stream
80+ let clonedResponseForResolving : Response | undefined ;
81+ try {
82+ clonedResponseForResolving = response . clone ( ) ;
83+ } catch ( e ) {
84+ // noop
85+ DEBUG_BUILD && logger . warn ( 'Failed to clone response body.' ) ;
86+ }
87+
88+ await resolveResponse ( clonedResponseForResolving , ( ) => {
89+ triggerHandlers ( 'fetch-body-resolved' , {
90+ endTimestamp : timestampInSeconds ( ) * 1000 ,
91+ response,
92+ } ) ;
93+ } ) ;
94+ } else {
95+ const finishedHandlerData : HandlerDataFetch = {
96+ ...handlerData ,
97+ endTimestamp : timestampInSeconds ( ) * 1000 ,
98+ response,
99+ } ;
100+ triggerHandlers ( 'fetch' , finishedHandlerData ) ;
101+ }
102+
84103 return response ;
85104 } ,
86105 ( error : Error ) => {
87- const erroredHandlerData : HandlerDataFetch = {
88- ...handlerData ,
89- endTimestamp : timestampInSeconds ( ) * 1000 ,
90- error,
91- } ;
92-
93- triggerHandlers ( 'fetch' , erroredHandlerData ) ;
106+ if ( handlerType === 'fetch' ) {
107+ const erroredHandlerData : HandlerDataFetch = {
108+ ...handlerData ,
109+ endTimestamp : timestampInSeconds ( ) * 1000 ,
110+ error,
111+ } ;
112+
113+ triggerHandlers ( 'fetch' , erroredHandlerData ) ;
114+
115+ if ( isError ( error ) && error . stack === undefined ) {
116+ // NOTE: If you are a Sentry user, and you are seeing this stack frame,
117+ // it means the error, that was caused by your fetch call did not
118+ // have a stack trace, so the SDK backfilled the stack trace so
119+ // you can see which fetch call failed.
120+ error . stack = virtualStackTrace ;
121+ addNonEnumerableProperty ( error , 'framesToPop' , 1 ) ;
122+ }
94123
95- if ( isError ( error ) && error . stack === undefined ) {
96124 // NOTE: If you are a Sentry user, and you are seeing this stack frame,
97- // it means the error, that was caused by your fetch call did not
98- // have a stack trace, so the SDK backfilled the stack trace so
99- // you can see which fetch call failed.
100- error . stack = virtualStackTrace ;
101- addNonEnumerableProperty ( error , 'framesToPop' , 1 ) ;
125+ // it means the sentry.javascript SDK caught an error invoking your application code.
126+ // This is expected behavior and NOT indicative of a bug with sentry.javascript.
127+ throw error ;
102128 }
103-
104- // NOTE: If you are a Sentry user, and you are seeing this stack frame,
105- // it means the sentry.javascript SDK caught an error invoking your application code.
106- // This is expected behavior and NOT indicative of a bug with sentry.javascript.
107- throw error ;
108129 } ,
109130 ) ;
110131 } ;
111132 } ) ;
112133}
113134
114- function instrumentFetchBodyReceived ( ) : void {
115- if ( ! supportsNativeFetch ( ) ) {
116- return ;
117- }
135+ function resolveResponse ( res : Response | undefined , onFinishedResolving : ( ) => void ) : void {
136+ if ( res && res . body ) {
137+ const responseReader = res . body . getReader ( ) ;
118138
119- fill ( GLOBAL_OBJ , 'fetch' , function ( originalFetch : ( ) => void ) : ( ) => void {
120- return function ( ...args : any [ ] ) : void {
121- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
122- return originalFetch . apply ( GLOBAL_OBJ , args ) . then ( async ( response : Response ) => {
123- // clone response for awaiting stream
124- let clonedResponseForResolving : Response | undefined ;
139+ // eslint-disable-next-line no-inner-declarations
140+ async function consumeChunks ( { done } : { done : boolean } ) : Promise < void > {
141+ if ( ! done ) {
125142 try {
126- clonedResponseForResolving = response . clone ( ) ;
127- } catch ( e ) {
128- // noop
129- DEBUG_BUILD && logger . warn ( 'Failed to clone response body.' ) ;
130- }
131-
132- if ( clonedResponseForResolving && clonedResponseForResolving . body ) {
133- const responseReader = clonedResponseForResolving . body . getReader ( ) ;
134-
135- // eslint-disable-next-line no-inner-declarations
136- function consumeChunks ( { done } : { done : boolean } ) : Promise < void > {
137- if ( ! done ) {
138- return responseReader . read ( ) . then ( consumeChunks ) ;
139- } else {
140- return Promise . resolve ( ) ;
141- }
142- }
143-
144- responseReader
145- . read ( )
146- . then ( consumeChunks )
147- . then ( ( ) => {
148- triggerHandlers ( 'fetch-body-resolved' , {
149- endTimestamp : timestampInSeconds ( ) * 1000 ,
150- response,
151- } ) ;
152- } )
153- . catch ( ( ) => {
154- // noop
155- } ) ;
143+ // abort reading if read op takes more than 5s
144+ const result = await Promise . race ( [
145+ responseReader . read ( ) ,
146+ new Promise < { done : boolean } > ( res => {
147+ setTimeout ( ( ) => {
148+ res ( { done : true } ) ;
149+ } , 5000 ) ;
150+ } ) ,
151+ ] ) ;
152+ await consumeChunks ( result ) ;
153+ } catch ( error ) {
154+ // handle error if needed
156155 }
157- return response ;
156+ } else {
157+ return Promise . resolve ( ) ;
158+ }
159+ }
160+
161+ responseReader
162+ . read ( )
163+ . then ( consumeChunks )
164+ . then ( ( ) => {
165+ onFinishedResolving ( ) ;
166+ } )
167+ . catch ( ( ) => {
168+ // noop
158169 } ) ;
159- } ;
160- } ) ;
170+ }
161171}
162172
163173function hasProp < T extends string > ( obj : unknown , prop : T ) : obj is Record < string , string > {
0 commit comments