Skip to content

✨ support telemetry from the renderer process - #205

Open
kikoveiga wants to merge 2 commits into
mainfrom
kikoveiga/renderer-telemetry
Open

✨ support telemetry from the renderer process#205
kikoveiga wants to merge 2 commits into
mainfrom
kikoveiga/renderer-telemetry

Conversation

@kikoveiga

@kikoveiga kikoveiga commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Motivation

The bridge has always carried internal_telemetry events from the renderer's browser SDK, and RendererPipeline dropped them on the floor behind a TODO(RUM-15253). Any telemetry the browser SDK reported inside an Electron renderer was lost.

Ticket: RUM-15253 (follow-up to #195)

Changes

RendererPipeline.handleTelemetryEvent forwards the event as a ServerTelemetryEvent on the RUM track with source: RENDERER, parallel to handleRumEvent — no RawEvent detour, since the browser SDK sends it fully assembled. The payload is validated at the boundary — a telemetry event with a numeric date and a telemetry payload carrying a type discriminator — and reported as a telemetry error otherwise, matching the profile/record cases. The kind itself is deliberately not checked against the schema: the browser SDK can add one before our rum-events-format submodule is synced, and telemetry is indexed whatever its kind, so a check would only drop valid telemetry for the length of that window — renderer RUM events are relayed whatever their type too. The identity fields (service, source, version, _dd) are deliberately left unchecked: they are the browser SDK's to report, and pinning their shape here would drop valid telemetry the day browser-core reshapes its envelope.

Because the event arrives assembled, the telemetry hooks become source-aware and the hook result is merged over it, contributing only what the main process owns:

Attribute Owner Why
date, source, service, version renderer The event reports on the browser SDK, so it has to keep identifying it
ddtags, action.id renderer Same: the renderer's own env/service/version tags, and the action its SDK attributed the event to
application.id, session.id main In bridge mode the browser SDK runs startSessionManagerStub(), so its session id is a random UUID nothing else knows
view.id renderer The view its RUM events carry; telemetry has no container to hold the main-process view too

Not re-sampled or deduplicated. The browser SDK already applied the rate the renderer's init() configured, and compounding two rates would be silently unrecoverable — effective_sample_rate is type-only in browser-core, never set, so nothing downstream could scale the survivors back up. This follows iOS's WebViewEventReceiver; Android has no webview telemetry path.

The two streams have separate rates, the same way they have separate caps. The Electron SDK's telemetrySampleRate is not consulted for relayed events at all, including when it is 0: a renderer's rate is configured in its own init() and governs what crosses the bridge, while the main process's rate governs what the SDK reports about itself. Reaching across would let the host's rate silently override a decision the renderer already made. iOS draws the same line — RUMFeature.swift gives TelemetryReceiver a Sampler(samplingRate: configuration.telemetrySampleRate) for native telemetry, while WebViewEventReceiver gets no sampler at all.

Volume is still bounded, by a per-session cap of its own rather than the main process's SessionBudget, so neither stream can starve the other. The browser SDK's ceiling is 15 events per kind per page load and lives in the SDK instance, so it resets on every window — it bounds a page, not a desktop session that outlives dozens of them. A cap is not a rate, so it cannot compound with the renderer's sampling.

An event with no main-process session covering its date has its session.id nulled rather than being dropped: the browser SDK's stub session id must not survive — it would mint phantom sessions in telemetry — but the event still reports on that SDK, and null is what the backend reads as absent (the device ProfilingCollection already uses for _dd.profiling). Main-process telemetry keeps its current behaviour and is still sent without a session, since an SDK error before the first session still reports a bug.

Riding along: main-process telemetry now carries ddtags (it was the only stream that could not be filtered by env), and TelemetryType is exported publicly.

Existing e2e scenarios needed scoping, not just new tests — session.scenario.ts indexed log telemetry positionally and the cap test counts to exactly 100/102, both with the main window's browser SDK running at its default 20% telemetry, so a relayed event could have landed in either. byMainProcessTelemetryType / byRendererTelemetryType make those reads mean what they say.

Test instructions

yarn test:unit                          # 1006 pass
yarn test:e2e:init && yarn test:e2e     # 73 pass

New unit coverage in src/assembly/RendererPipeline.spec.ts (internal telemetry events) replaces the test that pinned the old drop-everything behaviour. It covers the properties most likely to be "fixed" by a future reviewer: five identical events all going through, the per-session cap and its reset on session renewal, relaying regardless of the host's own telemetrySampleRate, and a kind the schema does not define being relayed rather than dropped.

Two new e2e scenarios in e2e/scenarios/telemetry.scenario.ts (renderer telemetry) drive a real bridge window through the real assembly path: one asserts the re-attribution, one that the renderer's view survives. Both the bridge window and the playground pin their telemetry rates to 100, since the browser SDK samples before sending and nothing would arrive deterministically otherwise.

Manually, cd playground && yarn start against a real intake: loading the window emits the browser SDK's configuration event over the bridge, which should arrive with the playground's application and session id, source: browser, service: browser-rum-sdk.

Checklist

  • Tested locally (playground) — the playground is wired for it (renderer telemetry rates at 100), but I verified against the fake intake through e2e rather than by hand
  • Added unit tests for this change.
  • Added e2e/integration tests for this change.
  • Updated related documentation. (docs/ARCHITECTURE.md → "Renderer telemetry")
  • Agentic code review findings addressed or explicitly dismissed.

🤖 Generated with Claude Code

@kikoveiga
kikoveiga force-pushed the kikoveiga/renderer-telemetry branch 5 times, most recently from 7ae8c30 to 4410046 Compare August 20, 2026 18:00
@kikoveiga
kikoveiga marked this pull request as ready for review August 20, 2026 18:00
@kikoveiga
kikoveiga requested a review from a team as a code owner August 20, 2026 18:00

@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: 4410046d73

ℹ️ 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 src/assembly/RendererPipeline.ts
Comment thread src/assembly/RendererPipeline.ts Outdated
* budget so neither stream can starve the other: a chatty renderer must not crowd out the Electron
* SDK's own error reporting, and vice versa.
*/
const MAX_RELAYED_TELEMETRY_EVENTS_PER_SESSION = 100;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was to align with MAX_TELEMETRY_EVENTS_PER_SESSION, but happy to discuss this number. From what I checked, Android SDK doesn't have telemetry from WebViews, and iOS doesn't define a limit (e.g. relies on browser-sdk`s limit). Still, thought it was good to add this because:

  • Browser's limit is 15 events per kind per page load, which can grow to a large number easily.
  • We have a safe limit well-defined on electron side.
  • Events here are not accounted for growing the MAX_TELEMETRY_EVENTS_PER_SESSION number.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kikoveiga
kikoveiga force-pushed the kikoveiga/renderer-telemetry branch from 4410046 to 9a68e2c Compare August 20, 2026 21:13

@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: 9a68e2c73e

ℹ️ 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 src/domain/telemetry/rawTelemetryData.types.ts
Comment thread src/assembly/RendererPipeline.ts
@sbarrio
sbarrio requested a review from cdn34dd August 21, 2026 07:07
@kikoveiga

Copy link
Copy Markdown
Contributor Author

@codex review

@cursor cursor 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.

PR Review — Score: 4.8 / 5

This PR closes the long-standing gap where renderer internal_telemetry events were dropped on the floor, and it does so with careful boundary design: IPC validation, source-aware hook enrichment, separate sampling/cap budgets for main vs renderer streams, and session re-attribution that avoids phantom sessions from the browser SDK stub. Prior Codex feedback on payload validation and transport registration ordering is addressed on the current head, unit and e2e coverage is strong, and docs/ARCHITECTURE.md documents the ownership model clearly. I would approve.

Why 4.8: Thoughtful cross-platform alignment (iOS WebView parity), robust validation at the IPC boundary including compile-time-checked KNOWN_TELEMETRY_TYPES, the transport-before-bridge fix, scoped e2e predicates that prevent flaky assertions once renderer telemetry flows, and thorough tests for cap/renewal/host-rate independence.

Why not 5: A documented schema-sync window drops unknown telemetry.type values until rum-events-format is bumped; relay-cap enforcement is unit-tested only (no e2e); and playground manual verification remains unchecked in the PR checklist.


Findings

  • [Nit] Schema sync window — A new browser-core telemetry kind is silently dropped at the bridge until rum-events-format syncs; the tradeoff is documented but worth tracking on submodule bumps.

Architectural flow

sequenceDiagram
    participant Browser as Browser RUM SDK
    participant IPC as DatadogEventBridge
    participant RP as RendererPipeline
    participant Hooks as Format Hooks
    participant EM as EventManager
    participant T as Transport

    Browser->>IPC: internal_telemetry assembled event
    IPC->>RP: BRIDGE_CHANNEL message
    RP->>RP: validate type date telemetry.kind
    RP->>Hooks: triggerTelemetry source RENDERER
    Note over Hooks: commonContext adds application.id<br/>SessionContext adds session.id or DISCARDED<br/>ViewContext SKIPPED keeps renderer view.id
    RP->>RP: enforce relay cap
    RP->>EM: ServerTelemetryEvent on RUM track
    EM->>T: batch and upload
Loading

Before: RendererPipeline ignored internal_telemetry bridge events (TODO RUM-15253), so any telemetry the browser SDK reported inside a renderer was lost.

After: Pre-assembled renderer telemetry is validated at the IPC boundary, enriched only with main-process-owned fields (application/session), capped independently of the main-process telemetry budget, and forwarded on the RUM track without re-sampling — preserving the renderer's own sampling decision and identity fields (source, service, version, view.id, ddtags).

Open in Web View Automation 

Sent by Cursor Automation: electron-sdk reviews

Comment thread src/assembly/RendererPipeline.ts Outdated
Comment thread src/index.ts
Comment thread src/domain/session/SessionContext.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

Here are some automated review suggestions for this pull request.

Reviewed commit: 0c45540181

ℹ️ 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 src/index.ts
rumApi = rum.getApi();
setDurationVitalApi(rumApi);

new RendererPipeline(eventManager, hooks, config);

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 Register the bridge before asynchronous setup begins

When init() is called while an already-open renderer is producing events, placing RendererPipeline here leaves BRIDGE_CHANNEL without a listener throughout Transport.create() and RumCollection.start(). In particular, the RUM transport handler is registered partway through Transport.create(), so during subsequent awaited track setup the previous ordering could deliver renderer events but the new ordering makes Electron discard their IPC messages. Register the listener earlier and queue events until transport initialization completes rather than shifting the loss window. docs/REVIEW.mdL10-L18

Useful? React with 👍 / 👎.

Comment thread src/assembly/RendererPipeline.ts Outdated
Comment thread src/domain/session/SessionContext.ts Outdated
Comment on lines +57 to +59
// Relayed renderer telemetry arrives with the browser SDK's own ddtags, so without these the
// Electron SDK's telemetry would be the only stream that cannot be filtered by env.
ddtags: buildDdtags(configuration),

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.

❗ warning: it will be the values configured by the customer though, we should not collect them in the sdk telemetry.‏

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I believe the browser SDK also adds these ddtags on its own telemetry. link

So should we keep this?

Comment thread src/assembly/RendererPipeline.ts Outdated
@kikoveiga
kikoveiga force-pushed the kikoveiga/renderer-telemetry branch from 0c45540 to b790937 Compare August 25, 2026 11:07
Boundary validation now requires a `telemetry` payload carrying a `type`
discriminator, which rejects the empty shells a bridge/SDK mismatch can produce
(`{}`, `[]`). The kind itself is deliberately not checked against the schema: the
browser SDK can add one before our `rum-events-format` submodule is synced, and
telemetry is indexed whatever its kind, so a check would drop valid telemetry for
the length of that window and buy nothing — renderer RUM events are relayed
whatever their type too. The identity fields stay unchecked as well: they are the
browser SDK's to report.

A relayed event whose date no main-process session covers now has its
`session.id` nulled rather than being dropped. The browser SDK's stub id must not
survive, but the event still reports on that SDK, and `null` is what the backend
reads as absent — the device `ProfilingCollection` already uses for
`_dd.profiling`. Main-process telemetry is likewise sent without a session.

`RendererPipeline` moves below `Transport.create` and `RumCollection.start`. It
accepts renderer IPC the moment it exists, `Transport` registers a track's
handler only once `createBatchManager` resolves, and `EventManager.notify` drops
events no handler claims — so a renderer event arriving in that window was lost
with no trace. One-shot events were the exposure: the browser SDK's
configuration telemetry is emitted once per page load and never retried.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kikoveiga
kikoveiga force-pushed the kikoveiga/renderer-telemetry branch from b790937 to 5c533dc Compare August 25, 2026 12:17

@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: 5c533dc46e

ℹ️ 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 src/index.ts
rumApi = rum.getApi();
setDurationVitalApi(rumApi);

new RendererPipeline(eventManager, hooks, config);

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 Publish bridge configuration before awaiting startup

When session replay is enabled and a window loads while Transport.create() or RumCollection.start() is pending, constructing RendererPipeline only afterward means setBridgeConfig() has not replaced the fallback yet. That fallback deliberately omits the records capability (src/common/bridgeConfig.ts), and src/preload/bridge.ts captures the response in a constant, so this renderer never advertises record support and loses session replay for the lifetime of the page. Publish the real bridge configuration before these asynchronous setup calls, independently of when event forwarding becomes ready.

AGENTS.md reference: AGENTS.md:L3-L6

Useful? React with 👍 / 👎.

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.

2 participants