✨ support telemetry from the renderer process - #205
Conversation
7ae8c30 to
4410046
Compare
There was a problem hiding this comment.
💡 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".
| * 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; |
There was a problem hiding this comment.
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_SESSIONnumber.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
4410046 to
9a68e2c
Compare
There was a problem hiding this comment.
💡 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".
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>
|
@codex review |
There was a problem hiding this comment.
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-formatsyncs; 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
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).
Sent by Cursor Automation: electron-sdk reviews
| eventData.type !== 'telemetry' || | ||
| typeof eventData.date !== 'number' || | ||
| !isIndexableObject(eventData.telemetry) || | ||
| !KNOWN_TELEMETRY_TYPES.has(eventData.telemetry.type) |
There was a problem hiding this comment.
[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.
| rumApi = rum.getApi(); | ||
| setDurationVitalApi(rumApi); | ||
|
|
||
| new RendererPipeline(eventManager, hooks, config); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
| rumApi = rum.getApi(); | ||
| setDurationVitalApi(rumApi); | ||
|
|
||
| new RendererPipeline(eventManager, hooks, config); |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 👍 / 👎.


Motivation
The bridge has always carried
internal_telemetryevents from the renderer's browser SDK, andRendererPipelinedropped them on the floor behind aTODO(RUM-15253). Any telemetry the browser SDK reported inside an Electron renderer was lost.Ticket: RUM-15253 (follow-up to #195)
Changes
RendererPipeline.handleTelemetryEventforwards the event as aServerTelemetryEventon the RUM track withsource: RENDERER, parallel tohandleRumEvent— noRawEventdetour, since the browser SDK sends it fully assembled. The payload is validated at the boundary — atelemetryevent with a numericdateand atelemetrypayload of a kindrum-events-formatdefines — and reported as a telemetry error otherwise, matching theprofile/recordcases. 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:
date,source,service,versionddtags,action.idapplication.id,session.idstartSessionManagerStub(), so its session id is a random UUID nothing else knowsview.idcontainerto hold the main-process view tooNot 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_rateis type-only in browser-core, never set, so nothing downstream could scale the survivors back up. This follows iOS'sWebViewEventReceiver; Android has no webview telemetry path.The two streams have separate rates, the same way they have separate caps. The Electron SDK's
telemetrySampleRateis not consulted for relayed events at all, including when it is0: a renderer's rate is configured in its owninit()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.swiftgivesTelemetryReceiveraSampler(samplingRate: configuration.telemetrySampleRate)for native telemetry, whileWebViewEventReceivergets 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), andTelemetryTypeis exported publicly.Existing e2e scenarios needed scoping, not just new tests —
session.scenario.tsindexed 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/byRendererTelemetryTypemake those reads mean what they say.Test instructions
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 owntelemetrySampleRate.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 startagainst a real intake: loading the window emits the browser SDK'sconfigurationevent over the bridge, which should arrive with the playground's application and session id,source: browser,service: browser-rum-sdk.Checklist
docs/ARCHITECTURE.md→ "Renderer telemetry")🤖 Generated with Claude Code