Skip to content

Commit de7db7d

Browse files
✨⚗️ [RUM-10146] implement early request collection (#3740)
Co-authored-by: bcaudan <[email protected]>
1 parent 0e07e9a commit de7db7d

File tree

6 files changed

+400
-171
lines changed

6 files changed

+400
-171
lines changed

packages/core/src/tools/experimentalFeatures.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { objectHasValue } from './utils/objectUtils'
1616
export enum ExperimentalFeature {
1717
TRACK_INTAKE_REQUESTS = 'track_intake_requests',
1818
WRITABLE_RESOURCE_GRAPHQL = 'writable_resource_graphql',
19+
EARLY_REQUEST_COLLECTION = 'early_request_collection',
1920
WATCH_COOKIE_WITHOUT_LOCK = 'watch_cookie_without_lock',
2021
USE_TREE_WALKER_FOR_ACTION_NAME = 'use_tree_walker_for_action_name',
2122
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import type { RelativeTime } from '@datadog/browser-core'
2+
import { createPerformanceEntry } from '../../../test'
3+
import { RumPerformanceEntryType } from '../../browser/performanceObservable'
4+
import { LifeCycle, LifeCycleEventType } from '../lifeCycle'
5+
import type { RequestCompleteEvent } from '../requestCollection'
6+
import { createRequestRegistry, MAX_REQUESTS } from './requestRegistry'
7+
8+
describe('RequestRegistry', () => {
9+
const URL = 'https://example.com/resource'
10+
it('returns the closest preceding request', () => {
11+
const lifeCycle = new LifeCycle()
12+
13+
const requestRegistry = createRequestRegistry(lifeCycle)
14+
const request1 = createRequestCompleteEvent({ startTime: 1 })
15+
const request2 = createRequestCompleteEvent({ startTime: 2 })
16+
const request3 = createRequestCompleteEvent({ startTime: 3 })
17+
const request4 = createRequestCompleteEvent({ startTime: 100 })
18+
lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, request1)
19+
lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, request2)
20+
lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, request3)
21+
lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, request4)
22+
23+
expect(requestRegistry.getMatchingRequest(createResourceEntry({ startTime: 99 }))).toBe(request3)
24+
})
25+
26+
it('ignores requests that have a different URL', () => {
27+
const lifeCycle = new LifeCycle()
28+
29+
const requestRegistry = createRequestRegistry(lifeCycle)
30+
31+
const request1 = createRequestCompleteEvent({ startTime: 1, url: URL })
32+
const request2 = createRequestCompleteEvent({ startTime: 2, url: 'https://another-url.com/resource' })
33+
lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, request1)
34+
lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, request2)
35+
36+
expect(requestRegistry.getMatchingRequest(createResourceEntry({ startTime: 3 }))).toBe(request1)
37+
})
38+
39+
it('does not return the same request twice', () => {
40+
const lifeCycle = new LifeCycle()
41+
42+
const requestRegistry = createRequestRegistry(lifeCycle)
43+
44+
const request1 = createRequestCompleteEvent({ startTime: 1 })
45+
const request2 = createRequestCompleteEvent({ startTime: 2 })
46+
47+
lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, request1)
48+
lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, request2)
49+
50+
expect(requestRegistry.getMatchingRequest(createResourceEntry({ startTime: 2 }))).toBe(request2)
51+
expect(requestRegistry.getMatchingRequest(createResourceEntry({ startTime: 2 }))).toBe(request1)
52+
expect(requestRegistry.getMatchingRequest(createResourceEntry({ startTime: 2 }))).toBeUndefined()
53+
})
54+
55+
it('is limited to a maximum number of requests', () => {
56+
const lifeCycle = new LifeCycle()
57+
const requestRegistry = createRequestRegistry(lifeCycle)
58+
for (let i = 0; i < MAX_REQUESTS + 1; i++) {
59+
lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, createRequestCompleteEvent({ startTime: i }))
60+
}
61+
expect(requestRegistry.getMatchingRequest(createResourceEntry({ startTime: 0 }))).toBeUndefined()
62+
})
63+
64+
function createRequestCompleteEvent({
65+
startTime,
66+
url = URL,
67+
}: {
68+
startTime: number
69+
url?: string
70+
}): RequestCompleteEvent {
71+
return {
72+
startClocks: { relative: startTime as RelativeTime },
73+
url,
74+
} as RequestCompleteEvent
75+
}
76+
77+
function createResourceEntry({ startTime }: { startTime: number }) {
78+
return createPerformanceEntry(RumPerformanceEntryType.RESOURCE, {
79+
startTime: startTime as RelativeTime,
80+
name: URL,
81+
})
82+
}
83+
})
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { addTelemetryDebug } from '@datadog/browser-core'
2+
import type { RumPerformanceResourceTiming } from '../../browser/performanceObservable'
3+
import type { LifeCycle } from '../lifeCycle'
4+
import { LifeCycleEventType } from '../lifeCycle'
5+
import type { RequestCompleteEvent } from '../requestCollection'
6+
7+
export interface RequestRegistry {
8+
getMatchingRequest(entry: RumPerformanceResourceTiming): RequestCompleteEvent | undefined
9+
stop(): void
10+
}
11+
12+
// Maximum number of requests to keep in the registry. Requests should be removed quite quickly in
13+
// general, this is just a safety limit to avoid memory leaks in case of a bug.
14+
export const MAX_REQUESTS = 1000
15+
16+
export function createRequestRegistry(lifeCycle: LifeCycle): RequestRegistry {
17+
const requests = new Set<RequestCompleteEvent>()
18+
19+
const subscription = lifeCycle.subscribe(LifeCycleEventType.REQUEST_COMPLETED, (request) => {
20+
requests.add(request)
21+
if (requests.size > MAX_REQUESTS) {
22+
addTelemetryDebug('Too many requests')
23+
requests.delete(requests.values().next().value!)
24+
}
25+
})
26+
27+
return {
28+
getMatchingRequest(entry) {
29+
// Returns the closest request object that happened before the entry
30+
let minTimeDifference = Infinity
31+
let closestRequest: RequestCompleteEvent | undefined
32+
for (const request of requests) {
33+
const timeDifference = entry.startTime - request.startClocks.relative
34+
if (0 <= timeDifference && timeDifference < minTimeDifference && request.url === entry.name) {
35+
minTimeDifference = Math.abs(timeDifference)
36+
closestRequest = request
37+
}
38+
}
39+
40+
if (closestRequest) {
41+
requests.delete(closestRequest)
42+
}
43+
44+
return closestRequest
45+
},
46+
47+
stop() {
48+
subscription.unsubscribe()
49+
},
50+
}
51+
}

0 commit comments

Comments
 (0)