Skip to content

✨ Collect connecting, open and closed WebSocket vitals - #5093

Open
bdibon wants to merge 14 commits into
boris.dibon/websocket-vitals-1-foundationsfrom
boris.dibon/websocket-vitals-2-basic-lifecycle
Open

bdibon wants to merge 14 commits into
boris.dibon/websocket-vitals-1-foundationsfrom
boris.dibon/websocket-vitals-2-basic-lifecycle

Conversation

@bdibon

@bdibon bdibon commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

The WebSocket prototype sent one resource event per connection, and only once the connection ended: a long-lived socket showed nothing while open, and a socket that ended without a close event reported nothing. This PR replaces it with a stream of WebSocket vitals (vital.type: "websocket"), one per connection phase, all sharing a connection id. This first slice reports the three phases every connection goes through: connecting, open and closed.

Part of the WebSocket vitals stack. Every PR targets the previous one; the first one targets the boris.dibon/websocket-vitals integration branch, which is merged into main in one go once the whole stack is approved. #5055 stays open as the reference implementation: the tip of this stack is content-identical to it, rebased on the current main.

  1. 🔧 Pin rum-events-format and add WebSocket test utilities #5092 🔧 Pin rum-events-format and add WebSocket test utilities
  2. ✨ Collect connecting, open and closed WebSocket vitals #5093 ✨ Collect connecting, open and closed WebSocket vitals 👈 this PR
  3. ✨ Report the websocket_closing vital #5094 ✨ Report the websocket_closing vital
  4. ✨ Pulse open WebSocket vitals on a shared heartbeat #5095 ✨ Pulse open WebSocket vitals on a shared heartbeat
  5. ✨ Report WebSocket silences and send queue depth, and protect WebSocket vitals in beforeSend #5096 ✨ Report WebSocket silences and send queue depth, and protect WebSocket vitals in beforeSend
  6. ✨ End WebSocket tracking when the page is discarded #5097 ✨ End WebSocket tracking when the page is discarded

Changes

Commit by commit:

  • 🎨 Move the WebSocket observable from browser-core to browser-rum-core: only RUM uses it (addresses this comment). The open context now carries the negotiated extensions.
  • ✨ Add the WebSocket vital event types: raw types for the three phases, with only the snapshot fields the schema requires (message count, total and max size, longest silence, peak send queue). RumWebSocketVitalEventDomainContext exposes the live socket to beforeSend. The profiler's vital history skips WebSocket vitals.
  • ✨ Add the tracked connection: trackedConnection.ts holds a connection's phase with the facts that come with it (a discriminated union, so no phase can be reached without its data), plus per-direction message aggregates.
  • ✨ Serialize a tracked connection into WebSocket vitals: serializeWebSocketVital.ts is pure: state → raw vital, presence rules, ms → ns. Phase dates are placed from the connecting date on the monotonic clock, so a system clock change mid-connection shifts none of them.
  • ✨ Collect WebSocket vitals on the life cycle: webSocketCollection.ts gates on trackResources and betaTrackWebSockets / track_websockets, tracks one connection per socket and notifies RAW_RUM_EVENT_COLLECTED directly. Connections still open when the session expires or the collection stops close with session_end.
  • 🔥 Replace the prototype WebSocket resource collection: wires the new collection in startRum, deletes resource/webSocketCollection*, WEBSOCKET_COMPLETED, the prototype's beforeSend field paths and the schema-validation bypasses. isWebSocket and RumWebSocketResourceEventDomainContext are deprecated rather than removed, since early adopters may reference them.
  • ✅ Cover the connecting, open and closed WebSocket vitals end to end

Suggested reading order: trackedConnection.ts → serializeWebSocketVital.ts → webSocketCollection.ts.

Test instructions

  • Unit: yarn test:unit --spec "packages/browser-rum-core/src/domain/webSocket/*.spec.ts" --spec packages/browser-rum-core/src/browser/webSocketObservable.spec.ts
  • E2E: yarn test:e2e -g "rum websockets"
  • Manually: enable betaTrackWebSockets in the sandbox, open and close a socket to an echo server, and check the three websocket_* vitals in the RUM intake requests.

Note

check-staging-merge is expected to fail: staging-40 already contains the earlier merge of #5055, which touches the same files. It clears at the next staging bump; the other checks should be green.

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

@datadog-prod-us1-3

datadog-prod-us1-3 Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Pipelines  Tests

❌ Errors

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 1 Pipeline job failed

DataDog/browser-sdk | check-staging-merge

View more details · View in GitLab

ℹ️ Info

No other issues found (see more)

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
• Patch Coverage: 91.26%
• Overall Coverage: 77.36% (+0.15%)

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 4c0f937 | Docs | View more details | Give us feedback!

@bdibon bdibon mentioned this pull request Oct 1, 2026
3 of 5 tasks
@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Bundles Sizes Evolution

📦 Bundle Name Base Size Local Size 𝚫 𝚫% Status
Rum Salesforce N/A 185.79 KiB N/A N/A N/A
Rum 185.61 KiB 185.77 KiB +161 B +0.08% ✅
Rum-salesforce Profiler N/A 8.35 KiB N/A N/A N/A
Rum Profiler 8.38 KiB 8.35 KiB -29 B -0.34% ✅
Rum-salesforce Recorder N/A 31.94 KiB N/A N/A N/A
Rum Recorder 31.94 KiB 31.94 KiB 0 B 0.00% ✅
Logs 59.00 KiB 59.00 KiB 0 B 0.00% ✅
Rum Slim 143.59 KiB 143.77 KiB +180 B +0.12% ✅
Worker 22.96 KiB 22.96 KiB 0 B 0.00% ✅
Rum Shopify N/A 216.18 KiB N/A N/A N/A
Rum-shopify Profiler N/A 8.35 KiB N/A N/A N/A
Rum-shopify Recorder N/A 4.77 KiB N/A N/A N/A

bdibon and others added 7 commits October 1, 2026 13:02
Only RUM collects WebSockets, so the observable does not need to ship with Logs. The open context now also carries the extensions the server selected.

Co-authored-by: Cursor <cursoragent@cursor.com>
Raw types for the websocket_connecting, websocket_open and websocket_closed vitals, and the domain context exposing the socket instance to beforeSend. The profiler vital history skips WebSocket vitals, which carry no duration.

Co-authored-by: Cursor <cursoragent@cursor.com>
It holds the phase a connection reached (connecting, open, closed) with the facts that come with it, and aggregates messages per direction: count, total and max size, longest gap, and the deepest send queue.

Co-authored-by: Cursor <cursoragent@cursor.com>
One vital per phase, sharing the connection id. Phase dates are placed from the connecting date on the monotonic clock, so a system clock change mid-connection shifts none of them.

Co-authored-by: Cursor <cursoragent@cursor.com>
Each instrumented connection reports websocket_connecting, websocket_open and websocket_closed vitals, with the socket instance as domain context. Connections still tracked when the session expires or the collection stops are closed with the session_end reason. The collection gates itself on trackResources and betaTrackWebSockets / TRACK_WEBSOCKETS; it is not wired yet.

Co-authored-by: Cursor <cursoragent@cursor.com>
…vitals

startRum now starts the vital collection, which gates itself. The prototype resource event, its WEBSOCKET_COMPLETED life cycle event, its beforeSend field paths, its schema validation bypasses and its e2e scenario are removed. The resource domain context types it exposed are deprecated rather than removed, as customers who opted in early may reference them.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-07T12:48:47.811459Z 4c0f937 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 997c69f340

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/browser-rum-core/src/domain/webSocket/trackedConnection.ts Outdated
Comment thread packages/browser-rum-core/src/domain/webSocket/serializeWebSocketVital.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

const { stop: stopInstrumentingSend } = instrumentMethod(
globalObject.WebSocket.prototype,
'send',

P2 Badge Report the client-initiated closing phase

issue: When application code calls WebSocket.close(), the observable instruments only send() and waits for the eventual close event, so no websocket_closing vital is produced. The generated event contract defines that phase with closing_date and close_initiator: 'client'; without instrumenting close(), client-initiated shutdowns are indistinguishable from server-initiated shutdowns in the resulting connection data.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/browser-rum-core/src/domain/webSocket/serializeWebSocketVital.ts Outdated
Comment thread packages/browser-rum-core/src/domain/webSocket/trackedConnection.ts
Comment thread packages/browser-rum-core/src/domain/webSocket/serializeWebSocketVital.ts Outdated
bdibon and others added 4 commits October 6, 2026 12:14
@bdibon
bdibon requested a review from bcaudan October 7, 2026 07:45

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0eb8b7f69e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +418 to +422
export const WebSocketVitalName = {
CONNECTING: 'websocket_connecting',
OPEN: 'websocket_open',
CLOSED: 'websocket_closed',
} as const

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Emit the closing phase when the client calls close

issue: When application code calls WebSocket.close(), this phase list has no closing state and the observable instruments only construction and send, so client- and server-initiated shutdowns both produce only the same closed vital. The generated RumVitalWebsocketClosingEvent contract is the sole place to report close_initiator: 'client'; without instrumenting close() and emitting this phase once, downstream connection data cannot distinguish who initiated the shutdown.

Useful? React with 👍 / 👎.

/** Ends tracking without a close event, so with no close outcome to report. */
recordTrackingEnd: (
endClocks: ClocksState,
trackingEndReason: Exclude<WebSocketTrackingEndReason, typeof WebSocketTrackingEndReason.CLOSE_EVENT>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥜 nitpick: ‏UnobservedTrackingEndReason could be exported and reused here

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4c0f937322

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// the peak is counted after the payload is enqueued, from the pre-send queue depth:
// `send()` grows the queue by exactly the payload size, whereas reading the socket again
// could catch a queue the browser has already partly flushed and understate the peak
snapshot.bufferedAmountMax = Math.max(snapshot.bufferedAmountMax, bufferedAmountPreSend + size)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude discarded sends from the queue maximum

issue: When application code calls send() after close() has moved the socket into CLOSING but before the close event arrives, the native method returns normally while silently discarding the payload, so the observable still reaches this path. Adding size to bufferedAmountPreSend then inflates buffered_amount_max with bytes that were never enqueued; check that the socket was OPEN before treating the call as an outbound enqueue.

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
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.

3 participants