Skip to content

Commit 770842b

Browse files
bdiboncursoragent
andcommitted
✨ Collect WebSocket resource events
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9b239da commit 770842b

11 files changed

Lines changed: 792 additions & 79 deletions

File tree

packages/browser-core/src/browser/webSocketObservable.spec.ts

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { registerCleanupTask } from '../../test'
22
import type { Subscription } from '../tools/observable'
3+
import { setAllowUntrustedEvents } from './addEventListener'
34
import type { WebSocketContext } from './webSocketObservable'
45
import { initWebSocketObservable, resetWebSocketObservable } from './webSocketObservable'
56

@@ -80,7 +81,8 @@ describe('webSocketObservable', () => {
8081
})
8182

8283
function startTracking() {
83-
subscription = initWebSocketObservable({ allowUntrustedEvents: true }).subscribe((context) => {
84+
setAllowUntrustedEvents(true)
85+
subscription = initWebSocketObservable().subscribe((context) => {
8486
contexts.push(context)
8587
})
8688
}
@@ -322,35 +324,4 @@ describe('webSocketObservable', () => {
322324
})
323325
})
324326
})
325-
326-
describe('with conflicting allowUntrustedEvents policies across callers', () => {
327-
it('does not emit open or message-in for untrusted events when the customer disallows them', () => {
328-
initWebSocketObservable({ allowUntrustedEvents: true })
329-
subscription = initWebSocketObservable({ allowUntrustedEvents: false }).subscribe((context) => {
330-
contexts.push(context)
331-
})
332-
333-
const ws = new windowAsWebSocketHost.WebSocket('wss://example.com/socket')
334-
ws.simulateOpen()
335-
ws.simulateMessage('hello')
336-
337-
expect(getContexts('connecting').length).toBe(1)
338-
expect(getContexts('open').length).toBe(0)
339-
expect(getContexts('message-in').length).toBe(0)
340-
})
341-
342-
it('emits open and message-in for untrusted events when every caller allows them', () => {
343-
initWebSocketObservable({ allowUntrustedEvents: true })
344-
subscription = initWebSocketObservable({ allowUntrustedEvents: true }).subscribe((context) => {
345-
contexts.push(context)
346-
})
347-
348-
const ws = new windowAsWebSocketHost.WebSocket('wss://example.com/socket')
349-
ws.simulateOpen()
350-
ws.simulateMessage('hello')
351-
352-
expect(getContexts('open').length).toBe(1)
353-
expect(getContexts('message-in').length).toBe(1)
354-
})
355-
})
356327
})

packages/browser-core/src/browser/webSocketObservable.ts

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ import { instrumentConstructor, instrumentMethod } from '../tools/instrumentMeth
66
import { Observable } from '../tools/observable'
77
import { addEventListener } from './addEventListener'
88

9-
interface WebSocketObservableConfiguration {
10-
allowUntrustedEvents?: boolean | undefined
11-
}
12-
139
type GlobalWithWebSocket = GlobalObject & { WebSocket: typeof WebSocket }
1410

1511
function isGlobalWithWebSocket(global: GlobalObject): global is GlobalWithWebSocket {
@@ -64,18 +60,7 @@ export type WebSocketContext =
6460

6561
let webSocketObservable: Observable<WebSocketContext> | undefined
6662

67-
// The singleton WebSocket observable applies the latest caller's allowUntrustedEvents policy so
68-
// that the customer's configuration overrides an early call (e.g. from bufferedData) that opts
69-
// in before the customer config is parsed.
70-
let allowUntrustedEvents: boolean | undefined
71-
72-
export function initWebSocketObservable(
73-
configuration: WebSocketObservableConfiguration = {}
74-
): Observable<WebSocketContext> {
75-
if (configuration.allowUntrustedEvents !== undefined) {
76-
allowUntrustedEvents = configuration.allowUntrustedEvents
77-
}
78-
63+
export function initWebSocketObservable(): Observable<WebSocketContext> {
7964
if (!webSocketObservable) {
8065
webSocketObservable = createWebSocketObservable()
8166
}
@@ -143,23 +128,23 @@ function attachInstanceListeners(
143128
observable: Observable<WebSocketContext>,
144129
stopListeners: Array<() => void>
145130
) {
146-
const { stop: stopOpen } = addEventListener({ allowUntrustedEvents }, instance, 'open', () => {
131+
const { stop: stopOpen } = addEventListener(instance, 'open', () => {
147132
observable.notify({
148133
state: 'open',
149134
instance,
150135
openClocks: clocksNow(),
151136
protocol: instance.protocol || '',
152137
})
153138
})
154-
const { stop: stopMessage } = addEventListener({ allowUntrustedEvents }, instance, 'message', (event) => {
139+
const { stop: stopMessage } = addEventListener(instance, 'message', (event) => {
155140
observable.notify({
156141
state: 'message-in',
157142
instance,
158143
size: computePayloadSize(event.data),
159144
at: clocksNow(),
160145
})
161146
})
162-
const { stop: stopClose } = addEventListener({ allowUntrustedEvents }, instance, 'close', (event) => {
147+
const { stop: stopClose } = addEventListener(instance, 'close', (event) => {
163148
observable.notify({
164149
state: 'closed',
165150
instance,
@@ -196,5 +181,4 @@ function computePayloadSize(data: unknown): number {
196181
*/
197182
export function resetWebSocketObservable() {
198183
webSocketObservable = undefined
199-
allowUntrustedEvents = undefined
200184
}

packages/browser-rum-core/src/boot/startRum.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@ import {
1717
startGlobalContext,
1818
startUserContext,
1919
startTabContext,
20+
isExperimentalFeatureEnabled,
21+
ExperimentalFeature,
2022
} from '@datadog/browser-core'
2123
import { createDOMMutationObservable } from '../browser/domMutationObservable'
2224
import { createWindowOpenObservable } from '../browser/windowOpenObservable'
2325
import { startInternalContext } from '../domain/contexts/internalContext'
2426
import { LifeCycle, LifeCycleEventType } from '../domain/lifeCycle'
2527
import { startViewHistory } from '../domain/contexts/viewHistory'
2628
import { startRequestCollection } from '../domain/requestCollection'
29+
import { startWebSocketCollection } from '../domain/webSocketCollection'
2730
import { startActionCollection } from '../domain/action/actionCollection'
2831
import { startErrorCollection } from '../domain/error/errorCollection'
2932
import { startResourceCollection } from '../domain/resource/resourceCollection'
@@ -230,6 +233,11 @@ export function startRumEventCollection(
230233

231234
const vitalCollection = startVitalCollection(lifeCycle, pageStateHistory)
232235

236+
if (isExperimentalFeatureEnabled(ExperimentalFeature.TRACK_WEB_SOCKETS)) {
237+
const webSocketCollection = startWebSocketCollection(lifeCycle, viewHistory, vitalCollection.addDurationVital)
238+
cleanupTasks.push(webSocketCollection.stop)
239+
}
240+
233241
const internalContext = startInternalContext(
234242
configuration.applicationId,
235243
sessionManager,

packages/browser-rum-core/src/domain/lifeCycle.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { AbstractLifeCycle } from '@datadog/browser-core'
44
import type { RumEventDomainContext } from '../domainContext.types'
55
import type { RawRumEvent, AssembledRumEvent } from '../rawRumEvent.types'
66
import type { RequestCompleteEvent, RequestStartEvent } from './requestCollection'
7+
import type { WebSocketCompleteEvent } from './webSocketCollection'
78
import type { AutoAction } from './action/actionCollection'
89
import type { ViewEvent, ViewCreatedEvent, ViewEndedEvent, BeforeViewUpdateEvent } from './view/trackViews'
910
import type { DurationVitalStart } from './vital/vitalCollection'
@@ -22,6 +23,7 @@ export const enum LifeCycleEventType {
2223
AFTER_VIEW_ENDED,
2324
REQUEST_STARTED,
2425
REQUEST_COMPLETED,
26+
WEBSOCKET_COMPLETED,
2527

2628
// The SESSION_EXPIRED lifecycle event has been introduced to represent when a session has expired
2729
// and trigger cleanup tasks related to this, prior to renewing the session. Its implementation is
@@ -67,6 +69,7 @@ declare const LifeCycleEventTypeAsConst: {
6769
AFTER_VIEW_ENDED: LifeCycleEventType.AFTER_VIEW_ENDED
6870
REQUEST_STARTED: LifeCycleEventType.REQUEST_STARTED
6971
REQUEST_COMPLETED: LifeCycleEventType.REQUEST_COMPLETED
72+
WEBSOCKET_COMPLETED: LifeCycleEventType.WEBSOCKET_COMPLETED
7073
SESSION_EXPIRED: LifeCycleEventType.SESSION_EXPIRED
7174
SESSION_RENEWED: LifeCycleEventType.SESSION_RENEWED
7275
PAGE_MAY_EXIT: LifeCycleEventType.PAGE_MAY_EXIT
@@ -89,6 +92,7 @@ export interface LifeCycleEventMap {
8992
[LifeCycleEventTypeAsConst.AFTER_VIEW_ENDED]: ViewEndedEvent
9093
[LifeCycleEventTypeAsConst.REQUEST_STARTED]: RequestStartEvent
9194
[LifeCycleEventTypeAsConst.REQUEST_COMPLETED]: RequestCompleteEvent
95+
[LifeCycleEventTypeAsConst.WEBSOCKET_COMPLETED]: WebSocketCompleteEvent
9296
[LifeCycleEventTypeAsConst.SESSION_EXPIRED]: void
9397
[LifeCycleEventTypeAsConst.SESSION_RENEWED]: void
9498
[LifeCycleEventTypeAsConst.PAGE_MAY_EXIT]: PageMayExitEvent

packages/browser-rum-core/src/domain/resource/resourceCollection.ts

Lines changed: 75 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,47 @@
1-
import type { Duration } from '@datadog/js-core/time'
2-
import { toServerDuration, relativeToClocks } from '@datadog/js-core/time'
31
import {
2+
addTelemetryDebug,
43
combine,
5-
generateUUID,
64
createTaskQueue,
5+
display,
6+
generateUUID,
7+
matchList,
78
mockable,
9+
RequestType,
10+
ResourceType,
811
runOnReadyState,
9-
matchList,
1012
safeTruncate,
11-
display,
12-
addTelemetryDebug,
13-
RequestType,
1413
setTimeout,
1514
} from '@datadog/browser-core'
16-
import type { MatchHeader, RumConfiguration } from '../configuration'
17-
import { RumPerformanceEntryType, createPerformanceObservable } from '../../browser/performanceObservable'
15+
import type { Duration } from '@datadog/js-core/time'
16+
import { elapsed, relativeToClocks, toServerDuration } from '@datadog/js-core/time'
17+
import { createPerformanceObservable, RumPerformanceEntryType } from '../../browser/performanceObservable'
18+
import { getNavigationEntry } from '../../browser/performanceUtils'
1819
import type { RumResourceEventDomainContext } from '../../domainContext.types'
1920
import type { NetworkHeaders, RawRumResourceEvent, ResourceRequest, ResourceResponse } from '../../rawRumEvent.types'
2021
import { RumEventType } from '../../rawRumEvent.types'
21-
import type { RawRumEventCollectedData, LifeCycle } from '../lifeCycle'
22+
import type { MatchHeader, RumConfiguration } from '../configuration'
23+
import { startEventTracker } from '../eventTracker'
24+
import { extractRegexMatch } from '../extractRegexMatch'
25+
import type { LifeCycle, RawRumEventCollectedData } from '../lifeCycle'
2226
import { LifeCycleEventType } from '../lifeCycle'
2327
import type { RequestCompleteEvent } from '../requestCollection'
24-
import { createSpanIdentifier } from '../tracing/identifier'
2528
import { getDocumentTraceId } from '../tracing/getDocumentTraceId'
26-
import { getNavigationEntry } from '../../browser/performanceUtils'
27-
import { startEventTracker } from '../eventTracker'
28-
import { extractRegexMatch } from '../extractRegexMatch'
29+
import { createSpanIdentifier } from '../tracing/identifier'
30+
import type { WebSocketCompleteEvent } from '../webSocketCollection'
31+
import type { GraphQlMetadata } from './graphql'
32+
import { extractGraphQlMetadata, findGraphQlConfiguration } from './graphql'
33+
import { createRequestRegistry } from './requestRegistry'
34+
import type { ResourceLikeEntry } from './resourceUtils'
2935
import {
36+
computeResourceEntryDeliveryType,
3037
computeResourceEntryDetails,
3138
computeResourceEntryDuration,
32-
computeResourceEntryType,
33-
computeResourceEntrySize,
3439
computeResourceEntryProtocol,
35-
computeResourceEntryDeliveryType,
40+
computeResourceEntrySize,
41+
computeResourceEntryType,
3642
isResourceEntryRequestType,
3743
sanitizeIfLongDataUrl,
3844
} from './resourceUtils'
39-
import type { ResourceLikeEntry } from './resourceUtils'
40-
import { createRequestRegistry } from './requestRegistry'
41-
import type { GraphQlMetadata } from './graphql'
42-
import { extractGraphQlMetadata, findGraphQlConfiguration } from './graphql'
4345
import type { ManualResourceData } from './trackManualResources'
4446
import { trackManualResources } from './trackManualResources'
4547

@@ -51,6 +53,9 @@ export function startResourceCollection(lifeCycle: LifeCycle, configuration: Rum
5153
const taskQueue = mockable(createTaskQueue)()
5254
const requestRegistry = createRequestRegistry(lifeCycle)
5355

56+
lifeCycle.subscribe(LifeCycleEventType.WEBSOCKET_COMPLETED, (event: WebSocketCompleteEvent) => {
57+
handleResource(() => assembleWebSocketResource(event))
58+
})
5459
const performanceResourceSubscription = createPerformanceObservable({
5560
type: RumPerformanceEntryType.RESOURCE,
5661
buffered: true,
@@ -108,6 +113,55 @@ export function startResourceCollection(lifeCycle: LifeCycle, configuration: Rum
108113
}
109114
}
110115

116+
function assembleWebSocketResource(
117+
event: WebSocketCompleteEvent
118+
): RawRumEventCollectedData<RawRumResourceEvent> | undefined {
119+
const duration = elapsed(event.startClocks.timeStamp, event.endClocks.timeStamp)
120+
121+
const rawRumEvent: RawRumResourceEvent = {
122+
date: event.startClocks.timeStamp,
123+
type: RumEventType.RESOURCE,
124+
resource: {
125+
id: generateUUID(),
126+
type: ResourceType.WEBSOCKET,
127+
url: event.url,
128+
duration: toServerDuration(duration),
129+
websocket: {
130+
connection_id: event.connectionId,
131+
handshake_succeeded: event.handshakeSucceeded,
132+
start_time: event.startClocks.timeStamp,
133+
end_time: event.endClocks.timeStamp,
134+
start_view_id: event.startViewId,
135+
end_view_id: event.endViewId,
136+
tracking_end_reason: event.trackingEndReason,
137+
close_code: event.closeCode,
138+
close_reason: event.closeReason,
139+
was_clean: event.wasClean,
140+
messages_in: event.messagesIn,
141+
messages_out: event.messagesOut,
142+
time_to_first_message_in: event.firstMessageInOffset,
143+
time_to_first_message_out: event.firstMessageOutOffset,
144+
last_message_in_at: event.lastMessageInAt,
145+
longest_inbound_silence: event.longestInboundSilence,
146+
inbound_idle_duration_before_close: event.inboundIdleDurationBeforeClose,
147+
buffered_amount_max: event.bufferedAmountMax,
148+
protocol: event.protocol,
149+
setup_duration: event.setupDuration,
150+
},
151+
},
152+
_dd: {
153+
discarded: false,
154+
},
155+
}
156+
157+
return {
158+
startClocks: event.startClocks,
159+
duration,
160+
rawRumEvent,
161+
domainContext: {},
162+
}
163+
}
164+
111165
function assembleResource(
112166
entry: ResourceLikeEntry,
113167
request: RequestCompleteEvent | undefined,

0 commit comments

Comments
 (0)