🐛 [RUM-17016] register bridge-config responder at instrument time#170
🐛 [RUM-17016] register bridge-config responder at instrument time#170bcaudan wants to merge 4 commits into
Conversation
|
@codex review |
There was a problem hiding this comment.
Stale comment
PR Review — Score: 4.7 / 5
This is a well-targeted fix for the deferred-init hang: registering the
datadog:bridge-configresponder at instrument time, backed by a process-global holder, directly addresses the root cause while preserving safe fallback behavior. TheSymbol.forpattern matches existing instrumentation idempotency,monitor()wraps the IPC callback correctly, and the unit plus e2e coverage give good confidence. Documentation clearly explains the lifecycle and the pre-init reload caveat. I would approve this PR.Why 4.7: Correct architectural split between instrument-time registration and init-time config publication; thorough tests (holder sharing, responder behavior, deferred-init e2e); docs updated in README and ARCHITECTURE.md; fallback values are conservative.
Why not 5: Minor robustness gap around returning a live config reference from the holder, and the new e2e scenario asserts host propagation but not
defaultPrivacyLevel(unit tests cover the holder, so risk is low).
Findings
- [Minor] Live config reference —
getBridgeConfig()returns the holder's object directly; accidental mutation could affect subsequent IPC responses.- [Nit] E2e privacy assertion — deferred-init scenario verifies
allowedWebViewHostsbefore/after init but notdefaultPrivacyLevel.
Architectural flow
LoadingsequenceDiagram participant App participant Instrument participant Holder as Process-global holder participant IPC as ipcMain responder participant Preload participant Init as init() / RendererPipeline App->>Instrument: import instrument (before electron) Instrument->>IPC: register datadog:bridge-config listener Instrument->>Holder: seed fallback (mask, []) Note over App,Preload: Window opens before init() Preload->>IPC: sendSync(datadog:bridge-config) IPC->>Holder: getBridgeConfig() Holder-->>IPC: fallback config IPC-->>Preload: immediate response (no hang) App->>Init: init(config) after consent gate Init->>Holder: setBridgeConfig(real config) Note over App,Preload: New window after init Preload->>IPC: sendSync(datadog:bridge-config) IPC->>Holder: getBridgeConfig() Holder-->>IPC: real config IPC-->>Preload: configured valuesBefore:
RendererPipelineregistered thedatadog:bridge-configresponder only duringinit(). Wheninit()was deferred (e.g. user-consent gating), the preload's synchronoussendSyncfound no listener and blocked the renderer for ~30s.After: The responder is registered unconditionally at instrument time and reads from a process-global holder seeded with safe fallbacks.
init()publishes the real configuration viasetBridgeConfig, so windows opened before init load promptly with fallback values and windows opened after init receive the configured privacy level and webview hosts.Sent by Cursor Automation: electron-sdk reviews
|
Codex Review: Didn't find any major issues. Hooray! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
5a5bbec to
e089d13
Compare
|
@codex review |
There was a problem hiding this comment.
Stale comment
PR Review — Score: 4.9 / 5
This is a well-targeted fix for the deferred-init hang. Registering the
datadog:bridge-configresponder at instrument time, backed by a process-global holder, directly addresses the root cause while preserving safe fallback behavior. Follow-up commits since the prior review addressed both earlier findings (getBridgeConfig()now returns a defensive copy; the e2e scenario assertsdefaultPrivacyLevelbefore and after init). TheSymbol.forpattern matches existing instrumentation idempotency,monitor()wraps the IPC callback correctly, and the unit plus e2e coverage give strong confidence. Documentation clearly explains the lifecycle and the pre-init reload caveat. I would approve this PR.Why 4.9: Correct architectural split between instrument-time registration and init-time config publication; prior review feedback fully addressed; thorough tests (holder sharing, copy semantics, responder behavior, deferred-init e2e with bounded load timeout); docs updated in README and ARCHITECTURE.md; fallback values are conservative.
Why not 5: No blocking or meaningful residual issues — the remaining gap is purely stylistic (e.g.
setBridgeConfigstores the config reference whilegetBridgeConfigclones on read; harmless givenbuildConfigurationproduces a stable object and IPC always goes through the copy path).
Architectural flow
LoadingsequenceDiagram participant App participant Instrument participant Holder as Process-global holder participant IPC as ipcMain responder participant Preload participant Init as init() / RendererPipeline App->>Instrument: import instrument (before electron) Instrument->>IPC: register datadog:bridge-config listener Instrument->>Holder: seed fallback (mask, []) Note over App,Preload: Window opens before init() Preload->>IPC: sendSync(datadog:bridge-config) IPC->>Holder: getBridgeConfig() (returns copy) Holder-->>IPC: fallback config IPC-->>Preload: immediate response (no hang) App->>Init: init(config) after consent gate Init->>Holder: setBridgeConfig(real config) Note over App,Preload: New window after init Preload->>IPC: sendSync(datadog:bridge-config) IPC->>Holder: getBridgeConfig() (returns copy) Holder-->>IPC: real config IPC-->>Preload: configured valuesBefore:
RendererPipelineregistered thedatadog:bridge-configresponder only duringinit(). Wheninit()was deferred (e.g. user-consent gating), the preload's synchronoussendSyncfound no listener and blocked the renderer for ~30s.After: The responder is registered unconditionally at instrument time and reads from a process-global holder seeded with safe fallbacks.
init()publishes the real configuration viasetBridgeConfig, so windows opened before init load promptly with fallback values and windows opened after init receive the configured privacy level and webview hosts.Sent by Cursor Automation: electron-sdk reviews
|
Codex Review: Didn't find any major issues. Delightful! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
|
@codex review |
e089d13 to
da0385c
Compare
There was a problem hiding this comment.
Stale comment
PR Review — Score: 5.0 / 5
This is a well-targeted fix for the deferred-init hang. Registering the
datadog:bridge-configresponder at instrument time, backed by a process-global holder, directly addresses the root cause while preserving safe fallback behavior. Prior review feedback is fully addressed on the current head:getBridgeConfig()returns a defensive copy, the deferred-init e2e scenario asserts bothallowedWebViewHostsanddefaultPrivacyLevelbefore and after init, and README/ARCHITECTURE.md document the lifecycle and pre-init reload caveat. TheSymbol.forpattern matches existing instrumentation idempotency,monitor()wraps the IPC callback correctly, and unit plus e2e coverage give strong confidence. I would approve this PR.Why 5.0: Correct architectural split between instrument-time registration and init-time config publication; all prior findings resolved; thorough tests (holder sharing, copy semantics, responder behavior, bounded pre-init load timeout); documentation complete; fallback values are conservative.
Why not 5: Nothing substantive — ready to merge.
Architectural flow
LoadingsequenceDiagram participant App participant Instrument participant Holder as Process-global holder participant IPC as ipcMain responder participant Preload participant Init as init() / RendererPipeline App->>Instrument: import instrument (before electron) Instrument->>IPC: register datadog:bridge-config listener Instrument->>Holder: seed fallback (mask, []) Note over App,Preload: Window opens before init() Preload->>IPC: sendSync(datadog:bridge-config) IPC->>Holder: getBridgeConfig() (returns copy) Holder-->>IPC: fallback config IPC-->>Preload: immediate response (no hang) App->>Init: init(config) after consent gate Init->>Holder: setBridgeConfig(real config) Note over App,Preload: New window after init Preload->>IPC: sendSync(datadog:bridge-config) IPC->>Holder: getBridgeConfig() (returns copy) Holder-->>IPC: real config IPC-->>Preload: configured valuesBefore:
RendererPipelineregistered thedatadog:bridge-configresponder only duringinit(). Wheninit()was deferred (e.g. user-consent gating), the preload's synchronoussendSyncfound no listener and blocked the renderer for ~30s.After: The responder is registered unconditionally at instrument time and reads from a process-global holder seeded with safe fallbacks.
init()publishes the real configuration viasetBridgeConfig, so windows opened before init load promptly with fallback values and windows opened after init receive the configured privacy level and webview hosts.Sent by Cursor Automation: electron-sdk reviews
|
Codex Review: Didn't find any major issues. 🚀 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
|
@codex review |
da0385c to
3a3b1ca
Compare
There was a problem hiding this comment.
Stale comment
PR Review — Score: 5.0 / 5
This is a well-targeted fix for the deferred-init hang. Registering the
datadog:bridge-configresponder at instrument time, backed by a process-global holder, directly addresses the root cause while preserving safe fallback behavior. The current head (3a3b1cae) includes follow-up commits that land the deferred-init e2e scenario (with bounded pre-init load timeout and before/after assertions on bothallowedWebViewHostsanddefaultPrivacyLevel) and lifecycle documentation in README and ARCHITECTURE.md. Prior review feedback is fully addressed:getBridgeConfig()returns a defensive copy, theSymbol.forpattern matches existing instrumentation idempotency,monitor()wraps the IPC callback correctly, and unit plus e2e coverage give strong confidence. I would approve this PR.Why 5.0: Correct architectural split between instrument-time registration and init-time config publication; all prior findings resolved; thorough tests and documentation; fallback values are conservative; no residual correctness or compatibility concerns.
Why not 5: Nothing substantive — ready to merge.
Architectural flow
LoadingsequenceDiagram participant App participant Instrument participant Holder as Process-global holder participant IPC as ipcMain responder participant Preload participant Init as init() / RendererPipeline App->>Instrument: import instrument (before electron) Instrument->>IPC: register datadog:bridge-config listener Instrument->>Holder: seed fallback (mask, []) Note over App,Preload: Window opens before init() Preload->>IPC: sendSync(datadog:bridge-config) IPC->>Holder: getBridgeConfig() (returns copy) Holder-->>IPC: fallback config IPC-->>Preload: immediate response (no hang) App->>Init: init(config) after consent gate Init->>Holder: setBridgeConfig(real config) Note over App,Preload: New window after init Preload->>IPC: sendSync(datadog:bridge-config) IPC->>Holder: getBridgeConfig() (returns copy) Holder-->>IPC: real config IPC-->>Preload: configured valuesBefore:
RendererPipelineregistered thedatadog:bridge-configresponder only duringinit(). Wheninit()was deferred (e.g. user-consent gating), the preload's synchronoussendSyncfound no listener and blocked the renderer for ~30s.After: The responder is registered unconditionally at instrument time and reads from a process-global holder seeded with safe fallbacks.
init()publishes the real configuration viasetBridgeConfig, so windows opened before init load promptly with fallback values and windows opened after init receive the configured privacy level and webview hosts.Sent by Cursor Automation: electron-sdk reviews
|
Codex Review: Didn't find any major issues. 🚀 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
There was a problem hiding this comment.
PR Review — Score: 5.0 / 5
This is a well-targeted fix for the deferred-init hang. Registering the datadog:bridge-config responder at instrument time, backed by a process-global holder, directly addresses the root cause while preserving safe fallback behavior. The current head (3a3b1cae) includes the deferred-init e2e scenario (bounded pre-init load timeout; before/after assertions on both allowedWebViewHosts and defaultPrivacyLevel) and lifecycle documentation in README and ARCHITECTURE.md. Prior review feedback is fully addressed: getBridgeConfig() returns a defensive copy, the Symbol.for pattern matches existing instrumentation idempotency, monitor() wraps the IPC callback correctly, and unit plus e2e coverage give strong confidence. I would approve this PR.
Why 5.0: Correct architectural split between instrument-time registration and init-time config publication; all prior findings resolved; thorough tests and documentation; fallback values are conservative; no residual correctness or compatibility concerns.
Why not 5: Nothing substantive — ready to merge.
Architectural flow
sequenceDiagram
participant App
participant Instrument
participant Holder as Process-global holder
participant IPC as ipcMain responder
participant Preload
participant Init as init() / RendererPipeline
App->>Instrument: import instrument (before electron)
Instrument->>IPC: register datadog:bridge-config listener
Instrument->>Holder: seed fallback (mask, [])
Note over App,Preload: Window opens before init()
Preload->>IPC: sendSync(datadog:bridge-config)
IPC->>Holder: getBridgeConfig() (returns copy)
Holder-->>IPC: fallback config
IPC-->>Preload: immediate response (no hang)
App->>Init: init(config) after consent gate
Init->>Holder: setBridgeConfig(real config)
Note over App,Preload: New window after init
Preload->>IPC: sendSync(datadog:bridge-config)
IPC->>Holder: getBridgeConfig() (returns copy)
Holder-->>IPC: real config
IPC-->>Preload: configured values
Before: RendererPipeline registered the datadog:bridge-config responder only during init(). When init() was deferred (e.g. user-consent gating), the preload's synchronous sendSync found no listener and blocked the renderer for ~30s.
After: The responder is registered unconditionally at instrument time and reads from a process-global holder seeded with safe fallbacks. init() publishes the real configuration via setBridgeConfig, so windows opened before init load promptly with fallback values and windows opened after init receive the configured privacy level and webview hosts.
Sent by Cursor Automation: electron-sdk reviews
cdn34dd
left a comment
There was a problem hiding this comment.
Do we want to keep this as a temporary workaround until tracking consent is supported by the SDK, and then remove support for deferred initialization? If so, I'm ok with the PR as it is.
If however, deferred initialization is intended to remain a supported use case independently of tracking consent, we should support it properly by buffering renderer events emitted before the SDK listener is registered.
The reason is that this flow has a high chance of breaking things, an example would be Session Replay, where the initial full snapshot is created on page load and is simply lost if the listener is not registered yet, potentially leading to broken replays. Missing initial RUM events could also cause correlation issues.
Consent tracking is a deferred initialization, as we won't want to send anything before consent is granted but we should have more control to avoid losing data. |


Motivation
Importing
@datadog/electron-sdk/instrumentbeforeelectronbut deferringinit()(a common pattern whengating on user consent) made the first window/frame load hang for ~30s. The injected preload does a
synchronous
ipcRenderer.sendSync('datadog:bridge-config')at frame commit, but the responder was onlyregistered by
init(). With no responder, Electron blocked the renderer until the sendSync timeout.Fixes #150 (RUM-17016).
Changes
datadog:bridge-configresponder unconditionally at instrument time, so the preload'ssynchronous request always resolves immediately, whether or not
init()has run.(
maskprivacy, no extra webview hosts). It must be a process global because theinstrumentandinit()entry points ship in separate bundles.init()(viaRendererPipeline) now updates the holder with the real config instead of owning theresponder. A window loaded before
init()reads the fallback; one loaded after reads the real config.ARCHITECTURE.mdand the pre-init reload caveat in the README.Test instructions
require('@datadog/electron-sdk/instrument')beforeelectronand do NOT callinit().BrowserWindowand load any content.sendSync ... without listenerswarning.init(...), open a second window, and confirm it picks up the configuredallowedWebViewHosts/defaultPrivacyLevel.Checklist