Skip to content

✨ Collect WebSocket connections opened before init() - #4938

Draft
bdibon wants to merge 6 commits into
mainfrom
boris.dibon/websocket-early-collection
Draft

✨ Collect WebSocket connections opened before init()#4938
bdibon wants to merge 6 commits into
mainfrom
boris.dibon/websocket-early-collection

Conversation

@bdibon

@bdibon bdibon commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Motivation

WebSocket collection subscribed to initWebSocketObservable() only once init() had run, so any
connection opened before then was invisible to RUM. A socket that a page opens at load — the common
case for chat, live feeds and collaborative editors — was never reported.

Changes

  • startBufferingData() gains a BufferedDataType.WEB_SOCKET source, alongside runtime errors,
    fetch, XHR and console. Instrumentation now starts at SDK load rather than at init().
  • The subscription is unconditional: the WebSocket opt-in is only knowable at init(), and
    consulting it before instrumenting would miss exactly the connections this change is about.
  • webSocketCollection consumes bufferedDataObservable instead of the raw
    Observable<WebSocketContext>, so pre-init() activity is replayed on unbuffer.
  • The opt-in gate (trackResources plus betaTrackWebSockets or the TRACK_WEBSOCKETS
    experimental feature) moves from startRum into startWebSocketCollection. startRum calls it
    unconditionally; when the gate is closed nothing is subscribed or allocated and the returned
    stop is a no-op.
  • startBufferingData() now takes an optional list of BufferedDataType sources to instrument.
    makeLogsPublicApi passes only RUNTIME_ERROR, FETCH, XHR and CONSOLE — logs never
    instruments WebSocket or pays its buffering cost. makeRumPublicApi passes all five sources,
    including WEB_SOCKET.
  • Tests: the fake WebSocket inlined in the observable spec becomes a shared core test utility
    (mockWebSocket), and the collection specs now drive through instrumentation rather than pushing
    synthetic contexts into an observable.

Known issues to resolve before merge

Two problems were surfaced while building this and deliberately deferred — each gets its own PR:

  1. WebSocket traffic can exhaust the buffered observable during pre-init. BufferedObservable
    is a 500-entry ring shared with runtime errors, fetch, XHR and console. message-in /
    message-out fire per message, so one chatty socket over a seconds-long pre-init() window can
    evict every other buffered entry. Nothing is coalesced here; the existing
    'Early data collection dropped data on unbuffer' debug telemetry is the tripwire. Coalescing
    (and where the aggregation should live) is addressed in a distinct PR.
  2. No e2e tests exercise the pre-init behavior. ✅ Add E2E coverage for WebSocket collection before init() #4939 introduces a TestBuilder.withPreInitScript() method to delay the init phase and execute code during the pre-init window.

Test instructions

  1. yarn dev, then in sandbox/index.html open a WebSocket before DD_RUM.init() and enable
    the opt-in on init:
    const ws = new WebSocket('wss://echo.websocket.org')
    ws.addEventListener('open', () => ws.send('hello'))
    // ...existing DD_RUM.init({...}) call, plus:
    //   trackResources: true, betaTrackWebSockets: true
  2. Load the page and let the socket close (or call ws.close() from the console).
  3. Confirm a websocket resource event is sent with the connection's message counts and sizes —
    the pre-init() connection is reported rather than dropped.
  4. Reload with betaTrackWebSockets removed and confirm no WebSocket resource event is sent.
  5. Confirm a logs-only page (DD_LOGS.init() without RUM) never patches the global WebSocket.

Checklist

  • Tested locally
  • Tested on staging
  • Added unit tests for this change.
  • Added e2e/integration tests for this change.
  • Updated documentation and/or relevant AGENTS.md file

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Aug 11, 2026

Copy link
Copy Markdown

Bundles Sizes Evolution

📦 Bundle Name Base Size Local Size 𝚫 𝚫% Status
Rum 180.44 KiB 180.58 KiB +142 B +0.08%
Rum Profiler 8.43 KiB 8.43 KiB 0 B 0.00%
Rum Recorder 21.12 KiB 21.12 KiB 0 B 0.00%
Logs 57.04 KiB 58.78 KiB +1.74 KiB +3.05%
Rum Salesforce N/A 138.59 KiB N/A N/A N/A
Rum Slim 138.47 KiB 138.59 KiB +121 B +0.09%
Worker 22.96 KiB 22.96 KiB 0 B 0.00%
Rum Shopify N/A 200.16 KiB N/A N/A N/A
Rum-shopify Profiler N/A 8.43 KiB N/A N/A N/A
Rum-shopify Recorder N/A 3.72 KiB N/A N/A N/A

@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented Aug 11, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 87.27%
Overall Coverage: 77.26% (+0.06%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: fa87695 | Docs | Datadog PR Page | Give us feedback!

bdibon and others added 6 commits August 11, 2026 18:30
Lift the fake WebSocket inlined in the WebSocket observable spec into a shared
core test helper, so upcoming WebSocket tickets drive instrumentation through
one test double instead of growing their own copies.

The utility follows the XHR mock's shape: it swaps the `WebSocket` global and
registers its own cleanup, including resetting the observable singleton. The
observable spec's test cases and assertions are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
WebSocket joins fetch, XHR, console and runtime errors as a buffered data
source, so a socket constructed before init() is recorded rather than lost.

The subscription is unconditional: the WebSocket opt-in is only known at
init(), and consulting it before instrumenting would miss every connection
opened before then — which is the point of the feature. The opt-in is left to
the consumer of the source. The full WebSocketContext union crosses the buffer
unchanged, with no coalescing in the core buffering layer.

Nothing consumes the new source yet, so there is no customer-visible change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
WebSocket collection now consumes the buffered data observable instead of
subscribing to the WebSocket observable directly, so connections opened before
init() are replayed and reported as complete resource events.

The opt-in gate (trackResources plus betaTrackWebSockets or the
TRACK_WEBSOCKETS experimental feature) moves into the collection entry point,
since it is not knowable until init(). RUM startup now calls it
unconditionally; the returned stop handle is a no-op when the gate is closed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The collection layer only observes what webSocketObservable emits, so asserting
that application handlers and payloads survive belongs with the instrumentation.
Handler passthrough was already covered there; add the missing send-payload
case and drop the collection-level duplicate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bdibon
bdibon force-pushed the boris.dibon/websocket-early-collection branch from 0386e91 to fa87695 Compare August 11, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant