Skip to content

Commit e37a6dc

Browse files
[skip ci] Merge branch into staging-45
2 parents bdedab6 + ca10ab0 commit e37a6dc

File tree

19 files changed

+792
-537
lines changed

19 files changed

+792
-537
lines changed

packages/core/src/browser/fetchObservable.spec.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { MockFetch, MockFetchManager } from '../../test'
22
import { registerCleanupTask, mockFetch } from '../../test'
33
import type { Subscription } from '../tools/observable'
44
import type { FetchResolveContext, FetchContext } from './fetchObservable'
5-
import { initFetchObservable } from './fetchObservable'
5+
import { initFetchObservable, resetFetchObservable, ResponseBodyAction } from './fetchObservable'
66

77
describe('fetch proxy', () => {
88
const FAKE_URL = 'http://fake-url/'
@@ -30,6 +30,7 @@ describe('fetch proxy', () => {
3030
registerCleanupTask(() => {
3131
requestsTrackingSubscription.unsubscribe()
3232
contextEditionSubscription?.unsubscribe()
33+
resetFetchObservable()
3334
})
3435
})
3536

@@ -265,3 +266,69 @@ describe('fetch proxy', () => {
265266
})
266267
})
267268
})
269+
270+
describe('fetch proxy with ResponseBodyAction', () => {
271+
const FAKE_URL = 'http://fake-url/'
272+
let mockFetchManager: MockFetchManager
273+
let requestsTrackingSubscription: Subscription
274+
let requests: FetchResolveContext[]
275+
let fetch: MockFetch
276+
277+
function setupFetchTracking(responseBodyAction: () => ResponseBodyAction) {
278+
mockFetchManager = mockFetch()
279+
requests = []
280+
requestsTrackingSubscription = initFetchObservable({ responseBodyAction }).subscribe((context) => {
281+
if (context.state === 'resolve') {
282+
requests.push(context)
283+
}
284+
})
285+
fetch = window.fetch as MockFetch
286+
}
287+
288+
afterEach(() => {
289+
requestsTrackingSubscription?.unsubscribe()
290+
resetFetchObservable()
291+
})
292+
293+
it('should collect response body with COLLECT action', (done) => {
294+
setupFetchTracking(() => ResponseBodyAction.COLLECT)
295+
296+
fetch(FAKE_URL).resolveWith({ status: 200, responseText: 'response body content' })
297+
298+
mockFetchManager.whenAllComplete(() => {
299+
expect(requests[0].responseBody).toBe('response body content')
300+
done()
301+
})
302+
})
303+
304+
it('should not collect response body with WAIT or IGNORE action', (done) => {
305+
setupFetchTracking(() => ResponseBodyAction.WAIT)
306+
307+
fetch(FAKE_URL).resolveWith({ status: 200, responseText: 'response body content' })
308+
309+
mockFetchManager.whenAllComplete(() => {
310+
expect(requests[0].responseBody).toBeUndefined()
311+
done()
312+
})
313+
})
314+
315+
it('should use the highest priority action when multiple getters are registered', (done) => {
316+
setupFetchTracking(() => ResponseBodyAction.WAIT)
317+
318+
initFetchObservable({
319+
responseBodyAction: () => ResponseBodyAction.COLLECT,
320+
})
321+
322+
registerCleanupTask(() => {
323+
requestsTrackingSubscription.unsubscribe()
324+
})
325+
326+
fetch = window.fetch as MockFetch
327+
fetch(FAKE_URL).resolveWith({ status: 200, responseText: 'response body content' })
328+
329+
mockFetchManager.whenAllComplete(() => {
330+
expect(requests[0].responseBody).toBe('response body content')
331+
done()
332+
})
333+
})
334+
})

packages/core/src/browser/fetchObservable.ts

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import type { InstrumentedMethodCall } from '../tools/instrumentMethod'
22
import { instrumentMethod } from '../tools/instrumentMethod'
3-
import { monitor } from '../tools/monitor'
3+
import { monitorError } from '../tools/monitor'
44
import { Observable } from '../tools/observable'
55
import type { ClocksState } from '../tools/utils/timeUtils'
66
import { clocksNow } from '../tools/utils/timeUtils'
77
import { normalizeUrl } from '../tools/utils/urlPolyfill'
88
import type { GlobalObject } from '../tools/globalObject'
99
import { globalObject } from '../tools/globalObject'
10+
import { readBytesFromStream } from '../tools/readBytesFromStream'
11+
import { tryToClone } from '../tools/utils/responseUtils'
1012

1113
interface 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

3336
export 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+
3552
let 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

4465
export function resetFetchObservable() {
4566
fetchObservable = undefined
67+
responseBodyActionGetters.length = 0
4668
}
4769

4870
function 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
}

packages/core/src/browser/xhrObservable.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ export interface XhrStartContext extends Omit<XhrOpenContext, 'state'> {
2020
isAborted: boolean
2121
xhr: XMLHttpRequest
2222
handlingStack?: string
23-
body?: unknown
23+
requestBody?: unknown
2424
}
2525

2626
export interface XhrCompleteContext extends Omit<XhrStartContext, 'state'> {
2727
state: 'complete'
2828
duration: Duration
2929
status: number
30+
responseBody?: string
3031
}
3132

3233
export type XhrContext = XhrOpenContext | XhrStartContext | XhrCompleteContext
@@ -88,7 +89,7 @@ function sendXhr(
8889
startContext.isAborted = false
8990
startContext.xhr = xhr
9091
startContext.handlingStack = handlingStack
91-
startContext.body = body
92+
startContext.requestBody = body
9293

9394
let hasBeenReported = false
9495

@@ -114,6 +115,9 @@ function sendXhr(
114115
completeContext.state = 'complete'
115116
completeContext.duration = elapsed(startContext.startClocks.timeStamp, timeStampNow())
116117
completeContext.status = xhr.status
118+
if (typeof xhr.response === 'string') {
119+
completeContext.responseBody = xhr.response
120+
}
117121
observable.notify(shallowClone(completeContext))
118122
}
119123

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export type { CookieStore, WeakRef, WeakRefConstructor } from './browser/browser
105105
export type { XhrCompleteContext, XhrStartContext } from './browser/xhrObservable'
106106
export { initXhrObservable } from './browser/xhrObservable'
107107
export type { FetchResolveContext, FetchStartContext, FetchContext } from './browser/fetchObservable'
108-
export { initFetchObservable, resetFetchObservable } from './browser/fetchObservable'
108+
export { initFetchObservable, resetFetchObservable, ResponseBodyAction } from './browser/fetchObservable'
109109
export type { PageMayExitEvent } from './browser/pageMayExitObservable'
110110
export { createPageMayExitObservable, PageExitReason, isPageExitReason } from './browser/pageMayExitObservable'
111111
export * from './browser/addEventListener'

0 commit comments

Comments
 (0)