Skip to content

✅ Add E2E coverage for WebSocket collection before init() - #4939

Draft
bdibon wants to merge 7 commits into
boris.dibon/websocket-early-collectionfrom
boris.dibon/websocket-early-collection-e2e
Draft

✅ Add E2E coverage for WebSocket collection before init()#4939
bdibon wants to merge 7 commits into
boris.dibon/websocket-early-collectionfrom
boris.dibon/websocket-early-collection-e2e

Conversation

@bdibon

@bdibon bdibon commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Motivation

WebSocket early data collection has unit coverage on both sides of the buffer boundary, but nothing exercised the real pre-start → post-start strategy swap in a browser. The headline behaviour of the feature — a socket opened before DD_RUM.init() still reports — was therefore unverified end to end.

The blocker was in the test framework, not the test: the E2E page setup builds the SDK bundle load and the init() call together into the page <head>, with no injection point between them. There was no way to run application script after the SDK loads but before init(), which is exactly the window this feature serves.

Follow-up to the WebSocket early-collection work, which put this out of scope on the explicit condition that it be done separately.

Changes

1. A pre-init() script injection point in the E2E framework (first commit)

createTest().withPreInitScript(js) runs application code in the window where the SDK is loaded and instrumenting the page, but not yet configured. The script is emitted as a memoised window.DD_PRE_INIT and called from the init call site of the bundle, npm and async setups.

The script may return a promise, in which case init() waits for it to settle. Without this, init() would run in the same tick as the script and only the synchronous part (the WebSocket constructor) would land in the buffer — the handshake, the message exchange and the close would all take the live path, and the test would silently prove much less than it appears to.

Two consequences worth a reviewer's attention:

  • The bundle setup now emits every SDK <script src> tag before any init script. Previously DD_LOGS.init() ran before the RUM bundle was fetched, which would put the pre-init script in a window where RUM is not yet instrumenting. This is the only change that affects tests not using the new option, hence the full-suite run below.
  • Setups that don't own their init call site (appSetup, microfrontendSetup, salesforceSetup) throw if the option is set, rather than silently ignoring it and letting a future test pass for the wrong reason.

The injection point is generic, not WebSocket-specific: fetch and XHR have the same untested pre-init window, so this unblocks equivalent coverage for them.

2. Two scenarios (second commit)

  • A socket opened and a message exchanged before init(), closed by the test afterwards.
  • The sharper case: the socket also closes before init(), so the entire connection lifecycle exists only in the buffer and the connection no longer exists when the SDK starts.

Both assert the pre-init() message counts and that timings are measured from the real constructor call rather than from init(), by comparing resource.websocket.start_time / end_time against a marker recorded at the top of init().

WebSocketPage.testBody() is deliberately untouched — the new pre-init script runs in the <head> before the body exists, so it publishes a handle on window instead of reusing the DOM fixture.

Generated with AI assistance; every claim below was verified by running the commands.

Test instructions

yarn build:apps --app vanilla     # needed for the npm setup
CI=true yarn test:e2e -g "before init\(\)"

To confirm the tests are not vacuous, check out the WebSocket collection code from before it consumed the buffered data source and re-run — both new tests fail with no websocket resource event reaching the intake, while the other eight before init tests in the suite still pass:

git checkout 29abcf7d0 -- packages/browser-rum-core/src/boot/startRum.ts \
                          packages/browser-rum-core/src/domain/resource/webSocketCollection.ts
CI=true yarn test:e2e -g "before init\(\)"     # 2 failed, 8 passed
git checkout HEAD -- packages/browser-rum-core/src/boot/startRum.ts \
                     packages/browser-rum-core/src/domain/resource/webSocketCollection.ts

Verified locally:

Check Result
yarn typecheck / yarn lint / yarn format clean
yarn test:unit 4880 passed
CI=true yarn test:e2e (full suite, all 3 setups) 989 passed, 0 failed, 9 skipped
CI=true yarn test:e2e -g "before init\(\)" --repeat-each=6 36/36
CI=true yarn test:e2e -g "websocket" --repeat-each=3 --workers=10 90/90

Note on flakiness: in the one full-suite local run, the two new tests were flaky-but-passing — as were 17 unrelated tests in the same overloaded 53-minute run, so those timeouts can't be confidently attributed to this change. A real race was found and closed regardless: page.goto() resolves on the load event, which can now precede the deliberately-delayed init(), so the scenarios wait explicitly on window.RUM_INIT_TIME and window.preInitWebSocket.

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

🤖 Generated with Claude Code

bdibon and others added 7 commits August 10, 2026 21:58
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>
The page setup built the SDK bundle load and the init() call together, with no way
to run application script in between. That window — SDK loaded and instrumenting,
but not configured yet — is exactly what the buffered data sources serve, and it
was untestable end to end.

`createTest().withPreInitScript(js)` now emits that script as `window.DD_PRE_INIT`,
called from the init site of the bundle, npm and async setups. The script may
return a promise, in which case init() waits for it to settle, so an asynchronous
exchange can complete entirely before the SDK starts.

The bundle setup now emits every SDK script tag before any init call, so the
pre-init script runs with all of them instrumenting the page. Setups that don't
own their init call site reject the option rather than silently ignoring it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
WebSocket early data collection had unit coverage on both sides of the buffer
boundary, but nothing exercised the pre-start → post-start strategy swap in a
browser. The headline behaviour — a socket opened before DD_RUM.init() still
reports — was unverified end to end.

Two scenarios open a socket in the pre-init window: one exchanges a message and
stays open until the test closes it, the other also closes before init() so the
entire connection lifecycle exists only in the buffer. Both assert the message
counts and that the timings are measured from the real constructor call rather
than from init().

Verified red: both fail against the collection code from before the buffered
source was consumed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cit-pr-commenter-54b7da

Copy link
Copy Markdown

Bundles Sizes Evolution

📦 Bundle Name Base Size Local Size 𝚫 𝚫% Status
Rum 180.68 KiB 180.68 KiB 0 B 0.00%
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 58.74 KiB 58.74 KiB 0 B 0.00%
Rum Salesforce N/A 138.56 KiB N/A N/A N/A
Rum Slim 138.55 KiB 138.55 KiB 0 B 0.00%
Worker 22.96 KiB 22.96 KiB 0 B 0.00%
Rum Shopify N/A 200.25 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-official

datadog-official 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: 100.00%
Overall Coverage: 77.27% (+0.00%)

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

@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