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 of a kind rum-events-format defines — and reported as a telemetry error otherwise, matching the profile/record cases. An unknown kind is one the backend would reject anyway, so relaying it would only spend relay budget on an event that cannot land. 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 is dropped rather than forwarded carrying the browser SDK's stub session id, which would mint phantom sessions in telemetry. 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                          # 1005 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, and relaying regardless of the host's own telemetrySampleRate.

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
Boundary validation now requires a `telemetry` payload of a kind the schema
defines. The known kinds are spelled as `Record<TelemetryType, true>`, so a kind
added to `rum-events-format` fails to compile here instead of being dropped
silently at the boundary. The identity fields stay unchecked: they are the
browser SDK's to report.

`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

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

eventData.type !== 'telemetry' ||
typeof eventData.date !== 'number' ||
!isIndexableObject(eventData.telemetry) ||
!KNOWN_TELEMETRY_TYPES.has(eventData.telemetry.type)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Nit] Schema sync window. Requiring KNOWN_TELEMETRY_TYPES.has(eventData.telemetry.type) cleanly closes the empty-payload hole Codex flagged, and the satisfies Record<TelemetryType, true> pattern is a nice compile-time guard. The documented downside is that a kind browser-core adds before rum-events-format is synced will be dropped (with a telemetry error) until this submodule bumps — acceptable, but worth a quick check whenever browser-core is updated.

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.

Good fix for the init race Codex raised: constructing RendererPipeline only after Transport.create means renderer configuration telemetry arriving immediately after browser SDK init will find a registered transport handler instead of being silently lost.

// arrives with the session id the browser SDK generates for itself in bridge mode — a stub nothing
// else knows — and `combine` cannot remove a key, so drop the event rather than attribute telemetry
// to a session Datadog never sees. Matches iOS, which drops webview telemetry for unsampled sessions.
return params.source === EventSource.RENDERER ? DISCARDED : SKIPPED;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Correct boundary behaviour for renderer telemetry: dropping (DISCARDED) when no main-process session covers the event's date avoids forwarding the browser SDK's stub session id, which would mint phantom sessions in telemetry. Keeping SKIPPED for main-process events preserves pre-session SDK error reporting.

@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 👍 / 👎.

eventData.type !== 'telemetry' ||
typeof eventData.date !== 'number' ||
!isIndexableObject(eventData.telemetry) ||
!KNOWN_TELEMETRY_TYPES.has(eventData.telemetry.type)

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 Validate fields required by each telemetry kind

For a host-controlled or incompatible renderer, { type: 'telemetry', date: 123, telemetry: { type: 'usage' } } passes this discriminator-only check and is uploaded even though the generated schema requires usage; likewise configuration requires its payload and log requires status and message. Fresh evidence after the earlier thread was marked fixed is that the updated condition validates only telemetry.type, so these malformed variants still consume relay budget and reach intake. Validate the required shape for the selected kind before forwarding it. docs/REVIEW.mdL20-L26

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.

1 participant