11import type { InstrumentedMethodCall } from '../tools/instrumentMethod'
22import { instrumentMethod } from '../tools/instrumentMethod'
3- import { monitor } from '../tools/monitor'
3+ import { monitorError } from '../tools/monitor'
44import { Observable } from '../tools/observable'
55import type { ClocksState } from '../tools/utils/timeUtils'
66import { clocksNow } from '../tools/utils/timeUtils'
77import { normalizeUrl } from '../tools/utils/urlPolyfill'
88import type { GlobalObject } from '../tools/globalObject'
99import { globalObject } from '../tools/globalObject'
10+ import { readBytesFromStream } from '../tools/readBytesFromStream'
11+ import { tryToClone } from '../tools/utils/responseUtils'
1012
1113interface FetchContextBase {
1214 method : string
@@ -25,16 +27,35 @@ export interface FetchResolveContext extends FetchContextBase {
2527 state : 'resolve'
2628 status : number
2729 response ?: Response
30+ responseBody ?: string
2831 responseType ?: string
2932 isAborted : boolean
3033 error ?: Error
3134}
3235
3336export type FetchContext = FetchStartContext | FetchResolveContext
3437
38+ type ResponseBodyActionGetter = ( context : FetchResolveContext ) => ResponseBodyAction
39+
40+ /**
41+ * Action to take with the response body of a fetch request.
42+ * Values are ordered by priority: higher values take precedence when multiple actions are requested.
43+ */
44+ export const enum ResponseBodyAction {
45+ IGNORE = 0 ,
46+ // TODO(next-major): Remove the "WAIT" action when `trackEarlyRequests` is removed, as the
47+ // duration of fetch requests will always come from PerformanceResourceTiming
48+ WAIT = 1 ,
49+ COLLECT = 2 ,
50+ }
51+
3552let fetchObservable : Observable < FetchContext > | undefined
53+ const responseBodyActionGetters : ResponseBodyActionGetter [ ] = [ ]
3654
37- export function initFetchObservable ( ) {
55+ export function initFetchObservable ( { responseBodyAction } : { responseBodyAction ?: ResponseBodyActionGetter } = { } ) {
56+ if ( responseBodyAction ) {
57+ responseBodyActionGetters . push ( responseBodyAction )
58+ }
3859 if ( ! fetchObservable ) {
3960 fetchObservable = createFetchObservable ( )
4061 }
@@ -43,6 +64,7 @@ export function initFetchObservable() {
4364
4465export function resetFetchObservable ( ) {
4566 fetchObservable = undefined
67+ responseBodyActionGetters . length = 0
4668}
4769
4870function createFetchObservable ( ) {
@@ -90,38 +112,51 @@ function beforeSend(
90112 parameters [ 0 ] = context . input as RequestInfo | URL
91113 parameters [ 1 ] = context . init
92114
93- onPostCall ( ( responsePromise ) => afterSend ( observable , responsePromise , context ) )
115+ onPostCall ( ( responsePromise ) => {
116+ afterSend ( observable , responsePromise , context ) . catch ( monitorError )
117+ } )
94118}
95119
96- function afterSend (
120+ async function afterSend (
97121 observable : Observable < FetchContext > ,
98122 responsePromise : Promise < Response > ,
99123 startContext : FetchStartContext
100124) {
101125 const context = startContext as unknown as FetchResolveContext
126+ context . state = 'resolve'
127+
128+ let response : Response
102129
103- function reportFetch ( partialContext : Partial < FetchResolveContext > ) {
104- context . state = 'resolve'
105- Object . assign ( context , partialContext )
130+ try {
131+ response = await responsePromise
132+ } catch ( error ) {
133+ context . status = 0
134+ context . isAborted =
135+ context . init ?. signal ?. aborted || ( error instanceof DOMException && error . code === DOMException . ABORT_ERR )
136+ context . error = error as Error
106137 observable . notify ( context )
138+ return
107139 }
108140
109- responsePromise . then (
110- monitor ( ( response ) => {
111- reportFetch ( {
112- response ,
113- responseType : response . type ,
114- status : response . status ,
115- isAborted : false ,
116- } )
117- } ) ,
118- monitor ( ( error : Error ) => {
119- reportFetch ( {
120- status : 0 ,
121- isAborted :
122- context . init ?. signal ?. aborted || ( error instanceof DOMException && error . code === DOMException . ABORT_ERR ) ,
123- error ,
141+ context . response = response
142+ context . status = response . status
143+ context . responseType = response . type
144+ context . isAborted = false
145+
146+ const responseBodyCondition = responseBodyActionGetters . reduce (
147+ ( action , getter ) => Math . max ( action , getter ( context ) ) ,
148+ ResponseBodyAction . IGNORE
149+ ) as ResponseBodyAction
150+
151+ if ( responseBodyCondition !== ResponseBodyAction . IGNORE ) {
152+ const clonedResponse = tryToClone ( response )
153+ if ( clonedResponse && clonedResponse . body ) {
154+ const bytes = await readBytesFromStream ( clonedResponse . body , {
155+ collectStreamBody : responseBodyCondition === ResponseBodyAction . COLLECT ,
124156 } )
125- } )
126- )
157+ context . responseBody = bytes && new TextDecoder ( ) . decode ( bytes )
158+ }
159+ }
160+
161+ observable . notify ( context )
127162}
0 commit comments