Skip to content

Commit 27309cc

Browse files
committed
♻️ Route fetch, XHR, and console through bufferedDataObservable
- Add FETCH, XHR, and CONSOLE types to BufferedData discriminated union - Subscribe to fetch/xhr/console observables in startBufferingData() - Update Logs and RUM consumers to receive network and console events via bufferedDataObservable instead of subscribing directly - Remove early instrumentation (subscribe(noop)) from preStart phases - Remove trackConsoleError (replaced by console handling in errorCollection) - Convert fetchObservable/xhrObservable/consoleObservable from BufferedObservable to plain Observable (buffering now centralized) - Normalize BufferedData shape to use { type, data } consistently
1 parent 9c596fc commit 27309cc

26 files changed

Lines changed: 559 additions & 542 deletions

packages/core/src/browser/fetchObservable.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import type { InstrumentedMethodCall } from '../tools/instrumentMethod'
22
import { instrumentMethod } from '../tools/instrumentMethod'
33
import { monitorError } from '../tools/monitor'
4-
import type { Observable } from '../tools/observable'
5-
import { BufferedObservable } from '../tools/observable'
4+
import { Observable } from '../tools/observable'
65
import type { ClocksState } from '../tools/utils/timeUtils'
76
import { clocksNow } from '../tools/utils/timeUtils'
87
import { normalizeUrl } from '../tools/utils/urlPolyfill'
@@ -48,9 +47,7 @@ export const enum ResponseBodyAction {
4847
COLLECT = 1,
4948
}
5049

51-
const FETCH_BUFFER_LIMIT = 500
52-
53-
let fetchObservable: BufferedObservable<FetchContext> | undefined
50+
let fetchObservable: Observable<FetchContext> | undefined
5451
const responseBodyActionGetters: ResponseBodyActionGetter[] = []
5552

5653
export function initFetchObservable({ responseBodyAction }: { responseBodyAction?: ResponseBodyActionGetter } = {}) {
@@ -69,7 +66,7 @@ export function resetFetchObservable() {
6966
}
7067

7168
function createFetchObservable() {
72-
return new BufferedObservable<FetchContext>(FETCH_BUFFER_LIMIT, (observable) => {
69+
return new Observable<FetchContext>((observable) => {
7370
// eslint-disable-next-line local-rules/disallow-zone-js-patched-values
7471
if (!globalObject.fetch) {
7572
return
@@ -126,7 +123,6 @@ async function afterSend(
126123
startContext: FetchStartContext
127124
) {
128125
const context = startContext as unknown as FetchResolveContext
129-
context.state = 'resolve'
130126

131127
let response: Response
132128

@@ -137,7 +133,7 @@ async function afterSend(
137133
context.isAborted =
138134
context.init?.signal?.aborted || (error instanceof DOMException && error.code === DOMException.ABORT_ERR)
139135
context.error = error as Error
140-
observable.notify(context)
136+
observable.notify({ ...context, state: 'resolve' })
141137
return
142138
}
143139

@@ -164,5 +160,5 @@ async function afterSend(
164160
}
165161
}
166162

167-
observable.notify(context)
163+
observable.notify({ ...context, state: 'resolve' })
168164
}

packages/core/src/browser/xhrObservable.ts

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import type { InstrumentedMethodCall } from '../tools/instrumentMethod'
22
import { instrumentMethod } from '../tools/instrumentMethod'
3-
import type { Observable } from '../tools/observable'
4-
import { BufferedObservable } from '../tools/observable'
3+
import { Observable } from '../tools/observable'
54
import type { Duration, ClocksState } from '../tools/utils/timeUtils'
65
import { elapsed, clocksNow, timeStampNow } from '../tools/utils/timeUtils'
76
import { normalizeUrl } from '../tools/utils/urlPolyfill'
87
import { shallowClone } from '../tools/utils/objectUtils'
9-
import type { Configuration } from '../domain/configuration'
108
import { globalObject } from '../tools/globalObject'
11-
import { noop } from '../tools/utils/functionUtils'
129
import { addEventListener } from './addEventListener'
1310

1411
export interface XhrOpenContext {
@@ -35,24 +32,22 @@ export interface XhrCompleteContext extends Omit<XhrStartContext, 'state'> {
3532

3633
export type XhrContext = XhrOpenContext | XhrStartContext | XhrCompleteContext
3734

38-
const XHR_BUFFER_LIMIT = 500
39-
40-
let xhrObservable: BufferedObservable<XhrContext> | undefined
35+
let xhrObservable: Observable<XhrContext> | undefined
4136
const xhrContexts = new WeakMap<XMLHttpRequest, XhrContext>()
4237

43-
export function initXhrObservable(configuration: Configuration) {
38+
export function initXhrObservable(configuration: { allowUntrustedEvents?: boolean | undefined }) {
4439
if (!xhrObservable) {
4540
xhrObservable = createXhrObservable(configuration)
4641
}
4742
return xhrObservable
4843
}
4944

50-
function createXhrObservable(configuration: Configuration) {
45+
function createXhrObservable(configuration: { allowUntrustedEvents?: boolean | undefined }) {
5146
if (!('XMLHttpRequest' in globalObject)) {
52-
return new BufferedObservable<XhrContext>(XHR_BUFFER_LIMIT, () => noop)
47+
return new Observable<XhrContext>()
5348
}
5449

55-
return new BufferedObservable<XhrContext>(XHR_BUFFER_LIMIT, (observable) => {
50+
return new Observable<XhrContext>((observable) => {
5651
const { stop: stopInstrumentingStart } = instrumentMethod(XMLHttpRequest.prototype, 'open', openXhr)
5752

5853
const { stop: stopInstrumentingSend } = instrumentMethod(
@@ -84,7 +79,7 @@ function openXhr({ target: xhr, parameters: [method, url] }: InstrumentedMethodC
8479

8580
function sendXhr(
8681
{ target: xhr, parameters: [body], handlingStack }: InstrumentedMethodCall<XMLHttpRequest, 'send'>,
87-
configuration: Configuration,
82+
configuration: { allowUntrustedEvents?: boolean | undefined },
8883
observable: Observable<XhrContext>
8984
) {
9085
const context = xhrContexts.get(xhr)
@@ -121,13 +116,12 @@ function sendXhr(
121116
hasBeenReported = true
122117

123118
const completeContext = context as XhrCompleteContext
124-
completeContext.state = 'complete'
125119
completeContext.duration = elapsed(startContext.startClocks.timeStamp, timeStampNow())
126120
completeContext.status = xhr.status
127121
if (typeof xhr.response === 'string') {
128122
completeContext.responseBody = xhr.response
129123
}
130-
observable.notify(shallowClone(completeContext))
124+
observable.notify({ ...shallowClone(completeContext), state: 'complete' })
131125
}
132126

133127
const { stop: unsubscribeLoadEndListener } = addEventListener(configuration, xhr, 'loadend', onEnd)

packages/core/src/domain/bufferedData.spec.ts

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
import { replaceMockable, registerCleanupTask } from '../../test'
1+
import type { MockFetch } from '../../test'
2+
import { collectAsyncCalls, mockFetch, mockXhr, registerCleanupTask, replaceMockable, withXhr } from '../../test'
23
import { Observable } from '../tools/observable'
4+
import { resetFetchObservable } from '../browser/fetchObservable'
5+
import { resetXhrObservable } from '../browser/xhrObservable'
36
import { clocksNow } from '../tools/utils/timeUtils'
7+
import { ConsoleApiName } from '../tools/display'
8+
import { noop } from '../tools/utils/functionUtils'
9+
import { resetConsoleObservable } from './console/consoleObservable'
10+
import type { BufferedData } from './bufferedData'
411
import { BufferedDataType, startBufferingData } from './bufferedData'
512
import { ErrorHandling, ErrorSource, type RawError } from './error/error.types'
613
import { trackRuntimeError } from './error/trackRuntimeError'
@@ -28,9 +35,123 @@ describe('startBufferingData', () => {
2835
observable.subscribe((data) => {
2936
expect(data).toEqual({
3037
type: BufferedDataType.RUNTIME_ERROR,
31-
error: rawError,
38+
data: rawError,
3239
})
3340
done()
3441
})
3542
})
43+
44+
it('collects fetch requests', async () => {
45+
mockFetch()
46+
const { observable, stop } = startBufferingData()
47+
const fetch = window.fetch as MockFetch
48+
const collected: BufferedData[] = []
49+
const bufferedDataCollectedSpy = jasmine.createSpy()
50+
51+
registerCleanupTask(() => {
52+
stop()
53+
resetFetchObservable()
54+
})
55+
56+
observable.subscribe((data) => {
57+
if (data.type === BufferedDataType.FETCH) {
58+
collected.push(data)
59+
bufferedDataCollectedSpy()
60+
}
61+
})
62+
63+
fetch('http://fake-url/').resolveWith({ status: 200, responseText: 'ok' })
64+
65+
await collectAsyncCalls(bufferedDataCollectedSpy, 2)
66+
67+
expect(collected).toEqual([
68+
{
69+
type: BufferedDataType.FETCH,
70+
data: jasmine.objectContaining({
71+
state: 'start',
72+
url: 'http://fake-url/',
73+
method: 'GET',
74+
}),
75+
},
76+
{
77+
type: BufferedDataType.FETCH,
78+
data: jasmine.objectContaining({
79+
state: 'resolve',
80+
url: 'http://fake-url/',
81+
method: 'GET',
82+
status: 200,
83+
}),
84+
},
85+
])
86+
})
87+
88+
it('collects xhr requests', async () => {
89+
mockXhr()
90+
const { observable, stop } = startBufferingData()
91+
const collected: BufferedData[] = []
92+
const bufferedDataCollectedSpy = jasmine.createSpy()
93+
94+
registerCleanupTask(() => {
95+
stop()
96+
resetXhrObservable()
97+
})
98+
99+
withXhr({
100+
setup(xhr) {
101+
xhr.open('GET', 'http://fake-url/')
102+
xhr.send()
103+
xhr.complete(200, 'ok')
104+
},
105+
onComplete: noop,
106+
})
107+
108+
observable.subscribe((data) => {
109+
if (data.type === BufferedDataType.XHR) {
110+
collected.push(data)
111+
bufferedDataCollectedSpy()
112+
}
113+
})
114+
115+
await collectAsyncCalls(bufferedDataCollectedSpy, 2)
116+
117+
expect(collected).toEqual([
118+
{
119+
type: BufferedDataType.XHR,
120+
data: jasmine.objectContaining({
121+
state: 'start',
122+
url: 'http://fake-url/',
123+
method: 'GET',
124+
}),
125+
},
126+
{
127+
type: BufferedDataType.XHR,
128+
data: jasmine.objectContaining({
129+
state: 'complete',
130+
url: 'http://fake-url/',
131+
method: 'GET',
132+
status: 200,
133+
}),
134+
},
135+
])
136+
})
137+
138+
it('collects console logs', (done) => {
139+
const { observable, stop } = startBufferingData()
140+
141+
registerCleanupTask(() => {
142+
stop()
143+
resetConsoleObservable()
144+
})
145+
146+
observable.subscribe((data) => {
147+
if (data.type === BufferedDataType.CONSOLE && data.data.api === ConsoleApiName.error) {
148+
expect(data.data.message).toContain('buffered data test error')
149+
done()
150+
}
151+
})
152+
153+
/* eslint-disable no-console */
154+
console.error('buffered data test error')
155+
/* eslint-enable no-console */
156+
})
36157
})
Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,53 @@
1+
import type { Observable, Subscription } from '../tools/observable'
12
import { BufferedObservable } from '../tools/observable'
23
import { mockable } from '../tools/mockable'
4+
import type { FetchContext } from '../browser/fetchObservable'
5+
import { initFetchObservable } from '../browser/fetchObservable'
6+
import type { XhrContext } from '../browser/xhrObservable'
7+
import { initXhrObservable } from '../browser/xhrObservable'
8+
import { ConsoleApiName } from '../tools/display'
39
import type { RawError } from './error/error.types'
410
import { trackRuntimeError } from './error/trackRuntimeError'
11+
import type { ConsoleLog } from './console/consoleObservable'
12+
import { initConsoleObservable } from './console/consoleObservable'
513

614
const BUFFER_LIMIT = 500
715

816
export const enum BufferedDataType {
917
RUNTIME_ERROR,
18+
FETCH,
19+
XHR,
20+
CONSOLE,
1021
}
1122

12-
export interface BufferedData {
13-
type: BufferedDataType.RUNTIME_ERROR
14-
error: RawError
15-
}
23+
export type BufferedData =
24+
| { type: BufferedDataType.RUNTIME_ERROR; data: RawError }
25+
| { type: BufferedDataType.FETCH; data: FetchContext }
26+
| { type: BufferedDataType.XHR; data: XhrContext }
27+
| { type: BufferedDataType.CONSOLE; data: ConsoleLog }
1628

1729
export function startBufferingData() {
1830
const observable = new BufferedObservable<BufferedData>(BUFFER_LIMIT)
31+
const subscriptions: Subscription[] = []
32+
33+
function subscribe<T extends BufferedDataType>(
34+
type: T,
35+
source: Observable<Extract<BufferedData, { type: T }>['data']>
36+
) {
37+
subscriptions.push(
38+
source.subscribe((data) => {
39+
observable.notify({ type, data } as BufferedData)
40+
})
41+
)
42+
}
1943

20-
const runtimeErrorSubscription = mockable(trackRuntimeError)().subscribe((error) => {
21-
observable.notify({
22-
type: BufferedDataType.RUNTIME_ERROR,
23-
error,
24-
})
25-
})
44+
subscribe(BufferedDataType.RUNTIME_ERROR, mockable(trackRuntimeError)())
45+
subscribe(BufferedDataType.FETCH, initFetchObservable())
46+
subscribe(BufferedDataType.XHR, initXhrObservable({ allowUntrustedEvents: true }))
47+
subscribe(BufferedDataType.CONSOLE, initConsoleObservable(Object.values(ConsoleApiName)))
2648

2749
return {
2850
observable,
29-
stop: () => {
30-
runtimeErrorSubscription.unsubscribe()
31-
},
51+
stop: () => subscriptions.forEach((subscription) => subscription.unsubscribe()),
3252
}
3353
}

packages/core/src/domain/console/consoleObservable.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { isError, computeRawError } from '../error/error'
2-
import type { Observable } from '../../tools/observable'
3-
import { BufferedObservable, mergeObservables } from '../../tools/observable'
2+
import { Observable, mergeObservables } from '../../tools/observable'
43
import { ConsoleApiName, globalConsole } from '../../tools/display'
54
import { callMonitored } from '../../tools/monitor'
65
import { sanitize } from '../../tools/serialisation/sanitize'
@@ -50,10 +49,8 @@ export function resetConsoleObservable() {
5049
consoleObservablesByApi = {}
5150
}
5251

53-
const CONSOLE_BUFFER_LIMIT = 500
54-
5552
function createConsoleObservable(api: ConsoleApiName) {
56-
return new BufferedObservable<ConsoleLog>(CONSOLE_BUFFER_LIMIT, (observable) => {
53+
return new Observable<ConsoleLog>((observable) => {
5754
const originalConsoleApi = globalConsole[api]
5855

5956
globalConsole[api] = (...params: unknown[]) => {

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export {
102102
resetInitCookies,
103103
} from './browser/cookie'
104104
export type { CookieStore, WeakRef, WeakRefConstructor } from './browser/browser.types'
105-
export type { XhrCompleteContext, XhrStartContext } from './browser/xhrObservable'
105+
export type { XhrCompleteContext, XhrStartContext, XhrContext } from './browser/xhrObservable'
106106
export { initXhrObservable } from './browser/xhrObservable'
107107
export type { FetchResolveContext, FetchStartContext, FetchContext } from './browser/fetchObservable'
108108
export { initFetchObservable, ResponseBodyAction } from './browser/fetchObservable'

0 commit comments

Comments
 (0)