Skip to content

Commit 496df82

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 496df82

24 files changed

Lines changed: 411 additions & 427 deletions

packages/core/src/browser/fetchObservable.ts

Lines changed: 3 additions & 6 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

packages/core/src/browser/xhrObservable.ts

Lines changed: 7 additions & 12 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)

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

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import { replaceMockable, registerCleanupTask } from '../../test'
1+
import type { MockFetch } from '../../test'
2+
import { 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 { resetConsoleObservable } from './console/consoleObservable'
49
import { BufferedDataType, startBufferingData } from './bufferedData'
510
import { ErrorHandling, ErrorSource, type RawError } from './error/error.types'
611
import { trackRuntimeError } from './error/trackRuntimeError'
@@ -28,9 +33,85 @@ describe('startBufferingData', () => {
2833
observable.subscribe((data) => {
2934
expect(data).toEqual({
3035
type: BufferedDataType.RUNTIME_ERROR,
31-
error: rawError,
36+
data: rawError,
3237
})
3338
done()
3439
})
3540
})
41+
42+
it('collects fetch requests', (done) => {
43+
const mockFetchManager = mockFetch()
44+
const { observable, stop } = startBufferingData()
45+
const fetch = window.fetch as MockFetch
46+
47+
registerCleanupTask(() => {
48+
stop()
49+
resetFetchObservable()
50+
})
51+
52+
const collected: BufferedDataType[] = []
53+
observable.subscribe((data) => {
54+
if (data.type === BufferedDataType.FETCH) {
55+
collected.push(data.type)
56+
}
57+
})
58+
59+
fetch('http://fake-url/').resolveWith({ status: 200, responseText: 'ok' })
60+
61+
mockFetchManager.whenAllComplete(() => {
62+
expect(collected.length).toBeGreaterThan(0)
63+
done()
64+
})
65+
})
66+
67+
it('collects xhr requests', (done) => {
68+
mockXhr()
69+
const { observable, stop } = startBufferingData()
70+
71+
registerCleanupTask(() => {
72+
stop()
73+
resetXhrObservable()
74+
})
75+
76+
observable.subscribe((data) => {
77+
if (data.type === BufferedDataType.XHR && data.data.state === 'complete') {
78+
expect(data.data.url).toContain('fake-url')
79+
expect(data.data.status).toBe(200)
80+
done()
81+
}
82+
})
83+
84+
withXhr({
85+
setup(xhr) {
86+
xhr.open('GET', 'http://fake-url/')
87+
xhr.send()
88+
xhr.complete(200, 'ok')
89+
},
90+
onComplete: noop,
91+
})
92+
})
93+
94+
it('collects console logs', (done) => {
95+
const { observable, stop } = startBufferingData()
96+
97+
registerCleanupTask(() => {
98+
stop()
99+
resetConsoleObservable()
100+
})
101+
102+
observable.subscribe((data) => {
103+
if (data.type === BufferedDataType.CONSOLE && data.data.api === ConsoleApiName.error) {
104+
expect(data.data.message).toContain('buffered data test error')
105+
done()
106+
}
107+
})
108+
109+
/* eslint-disable no-console */
110+
console.error('buffered data test error')
111+
/* eslint-enable no-console */
112+
})
36113
})
114+
115+
function noop() {
116+
// intentionally empty
117+
}
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'

packages/logs/src/boot/preStartLogs.spec.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
callbackAddsInstrumentation,
32
collectAsyncCalls,
43
type Clock,
54
mockClock,
@@ -204,21 +203,6 @@ describe('preStartLogs', () => {
204203
;({ strategy, doStartLogsSpy } = createPreStartStrategyWithDefaults({ trackingConsentState }))
205204
})
206205

207-
describe('basic methods instrumentation', () => {
208-
it('should instrument fetch even if tracking consent is not granted', () => {
209-
expect(
210-
callbackAddsInstrumentation(() => {
211-
strategy.init({
212-
...DEFAULT_INIT_CONFIGURATION,
213-
trackingConsent: TrackingConsent.NOT_GRANTED,
214-
})
215-
})
216-
.toMethod(window, 'fetch')
217-
.whenCalled()
218-
).toBeTrue()
219-
})
220-
})
221-
222206
it('does not start logs if tracking consent is not granted at init', () => {
223207
strategy.init({
224208
...DEFAULT_INIT_CONFIGURATION,

packages/logs/src/boot/preStartLogs.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import {
55
display,
66
displayAlreadyInitializedError,
77
initFeatureFlags,
8-
initFetchObservable,
9-
initConsoleObservable,
108
noop,
119
timeStampNow,
1210
buildAccountContextManager,
@@ -100,13 +98,6 @@ export function createPreStartStrategy(
10098

10199
cachedConfiguration = configuration
102100

103-
// Instrument fetch and console early so events fired synchronously after
104-
// init() are captured and buffered for replay when startLogs() subscribes.
105-
initFetchObservable().subscribe(noop)
106-
if (configuration.forwardConsoleLogs.length) {
107-
initConsoleObservable(configuration.forwardConsoleLogs).subscribe(noop)
108-
}
109-
110101
trackingConsentState.tryToInit(configuration.trackingConsent)
111102

112103
trackingConsentState.onGrantedOnce(() => {

packages/logs/src/boot/startLogs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ export function startLogs(
5151
const globalContext = startGlobalContext(hooks, configuration, LOGS_STORAGE_KEY, false)
5252
startRUMInternalContext(hooks)
5353

54-
startNetworkErrorCollection(configuration, lifeCycle)
54+
startNetworkErrorCollection(configuration, lifeCycle, bufferedDataObservable)
5555
startRuntimeErrorCollection(configuration, lifeCycle, bufferedDataObservable)
56+
startConsoleCollection(configuration, lifeCycle, bufferedDataObservable)
5657
bufferedDataObservable.unbuffer()
57-
startConsoleCollection(configuration, lifeCycle)
5858
startReportCollection(configuration, lifeCycle)
5959
const { handleLog } = startLoggerCollection(lifeCycle)
6060

0 commit comments

Comments
 (0)