[RUM-15104] Integrate dd-trace#95
Conversation
…jection - Bundle dd-trace to capture main-process HTTP requests (fetch, net) as RUM resource events with trace/span IDs for APM correlation. - Uses dd-trace's electron exporter to publish spans via diagnostics channel instead of requiring a local Datadog Agent. - Add @datadog/electron-sdk/init entry point that must be imported before electron — so it can wrap BrowserWindow for automatic preload injection in both bundled and non-bundled environments.
528c305 to
147cba6
Compare
dd-trace must be available at runtime in packaged Electron apps for preload injection and tracing to work. Moving it to dependencies ensures it's installed transitively when the SDK is consumed. Also updates dd-trace.tgz with latest changes (preload path resolution fix, flagging_provider optional dependency fix, BigInt coercion fix).
Vite hoists all top-level require() calls to the start of the bundle,
breaking dd-trace's module hooking which requires require('electron')
to run after dd-trace's hooks are registered.
The datadogVitePlugin:
- Externalizes dd-trace and @datadog/electron-sdk to prevent hoisting
- Prepends dd-trace initialization before any hoisted requires
- Copies dd-trace + transitive deps into build output for packaged apps
- Emits preload script at dd-trace's fallback path
In packaged Electron apps, dd-trace's preload file resolution fails because node_modules is absent. dd-trace falls back to path.join(__dirname, 'electron', 'preload.js'). The DatadogWebpackPlugin: - Copies dd-trace's preload script to that fallback path in webpack output - Auto-excludes dd-trace from @vercel/webpack-asset-relocator-loader which crashes on dd-trace's dynamic require patterns
Add rollup build entries, TypeScript declarations, package.json exports, typesVersions, and files entries for both vite-plugin and webpack-plugin. Update instrument.ts JSDoc to reference both bundler plugins.
- forge-vite, electron-vite, electron-builder-vite: add datadogVitePlugin() - forge-webpack: add DatadogWebpackPlugin, remove manual asset-relocator exclude
- README: add Bundler Plugins section with Vite and Webpack usage examples - ARCHITECTURE: rewrite preload injection section to explain both plugins - Integration README: mention both plugins in toolchain overview
Preload injection is now fully owned by dd-trace's BrowserWindow wrapping. Remove the SDK's own preload entry point, bridge implementation, and tests. Clean up rollup build config, package.json exports, and stale comments.
147cba6 to
d347711
Compare
- Replace broad localhost/127.0.0.1 filtering with precise hostname
matching against the configured intake (browser-intake-{site}) and
proxy URL. User HTTP requests to localhost are no longer dropped.
- Override span service name with the SDK config value so all spans
use the same service as RUM events, instead of dd-trace's default
(which picks up the package.json name).
- Extract ResourceConverterConfig interface to reduce constructor args.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 393ef1e466
ℹ️ 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 .mjs builds use bare require('dd-trace') which doesn't exist in ESM
modules. When loaded via import (e.g. Electron Forge with webpack, or
esbuild ESM output), the calls silently fail in try/catch blocks
preventing dd-trace initialization and disabling the ResourceConverter.
Use createRequire(import.meta.url) for ESM context in both instrument.ts
and Tracing.ts, matching the pattern already used in the vite plugin.
esbuild hoists ESM import statements before module body code, so import '@datadog/electron-sdk/instrument' cannot guarantee execution before import 'electron'. The plugin prepends a banner that initializes dd-trace via synchronous require(), and externalizes dd-trace and @datadog/electron-sdk.
The published dd-trace has optional peer dependencies (e.g. @openfeature/server-sdk) that webpack fails to resolve when bundling. Externalize dd-trace and @datadog/electron-sdk so webpack leaves them as runtime require() calls, and copy their node_modules into the webpack output so they are available in packaged apps where the project's node_modules is absent.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8fa4262c1b
ℹ️ 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".
Detect the Rollup output format in renderChunk and use an ESM-safe banner when format is 'es'. In ESM, dd-trace's IITM hooks cannot wrap BrowserWindow because static imports are loaded before module code evaluates, so the ESM banner registers the preload script directly via session.defaultSession.registerPreloadScript() on app ready. This is needed for electron-vite which, unlike Forge's vite plugin, allows configuring ESM output for the main process.
Copy of the electron-vite integration app configured with ESM output (format: 'es') to verify the vite plugin's ESM banner and preload registration work in both dev and packaged modes.
- forge-esbuild-cjs: esbuild → CJS, exercises the SDK plugin's CJS banner + dd-trace IITM require('electron') wrap
- forge-esbuild-esm: esbuild → ESM main, CJS preload, ESM renderer; exercises the plugin's ESM branch (createRequire + session.registerPreloadScript)
- both packaged via Electron Forge (no Forge bundler plugin) and wired into INTEGRATION_APPS for auto-discovery
- Fix intake hostname normalization for subdomain sites (e.g. us3.datadoghq.com → browser-intake-us3-datadoghq.com) by extracting computeIntakeHostname and reusing it in ResourceConverter - Use either proxy or intake hostname for SDK request filtering, not both - Set startTime on RUM resource events from span.start so Assembly resolves view/session context against the request start time - Replace manual try/catch + addError with monitor() wrapper - Replace ResourceConverterConfig with Configuration - Use branded TimeStamp and ServerDuration types in RawRumResource - Remove displayInfo debug logs from ResourceConverter - Add console.warn when dd-trace is not found in bundler plugins and instrument entry point - Add console.warn when copyPackageTree fails in vite and webpack plugins - Import computeIntakeHostname from transport barrel instead of internal module
- Rename class and files to reflect broader responsibility (filtering, enriching, emitting resources, and forwarding span envelopes) - Extract onMessage handler into private methods: processTrace, isSdkRequest, enrichSpan, emitResource, emitTraceEnvelope - Update references in index.ts, Tracing.ts, and ARCHITECTURE.md
Replaces the `sw_vers` subprocess call with Electron's `process.getSystemVersion()`, which is synchronous and in-process. Makes `getUserAgent` synchronous and avoids polluting customer traces with an SDK-internal `child_process` span.
+ span processor namings + span hooks
be5bd00 to
f4b6afb
Compare
| copyPackageTree('dd-trace'); | ||
| copyPackageTree('@datadog/electron-sdk'); |
There was a problem hiding this comment.
💭 thought: copyPackageTree('dd-trace', ...) makes sense, dd-trace needs to be available at runtime in packaged apps where node_modules isn't included.
copyPackageTree('@datadog/electron-sdk', ...) feels a bit odd though: it looks like the SDK ends up copying itself into its own output. IIUC, this is a consequence of externalizing @datadog/electron-sdk so the banner require("@datadog/electron-sdk/instrument") can resolve at runtime.
One option worth considering: inlining the instrument.ts logic directly into the CJS banner:
const CJS_BANNER = 'try{require("dd-trace").default.init({experimental:{exporter:"electron"}})}catch{}';This would remove @datadog/electron-sdk from both the external list and copyPackageTree, since the banner no longer needs to resolve it at runtime, only dd-trace would need special treatment.
The main tradeoff is that the banner and instrument.ts would duplicate the init logic, so if the init options ever expand both would need updating. Is instrument.ts expected to stay this minimal long-term? If so, inlining might be worth it to avoid the self-copy.
Wdyt?
There was a problem hiding this comment.
I like the idea, but the setup we have is somewhat complex. What you're suggesting was something that Initially tried and worked well in isolation, the main thing that forced me to add externalization was due to the Tracing class and now also tracingPatches which make use createRequire at runtime and there were issues with webpack regarding asset-relocator-loader if I'm not mistaken. With vite I'm not 100% sure, but I think there it worked, so maybe we could add it to vite, but still I'd not make any more potentially breaking changes at this point.
If we feel like we need to simplify this, we should do so as a self contained PR that we have time to properly test.
There was a problem hiding this comment.
Sounds good to me, should we create a card to track it?
- Remove more debug logs
7296c25 to
8b4d100
Compare
Motivation
Capture main-process HTTP requests as RUM resource events with trace/span IDs, enabling RUM-APM correlation without requiring a local Datadog Agent. Automatically inject the renderer bridge preload via dd-trace's BrowserWindow wrapping, replacing the SDK's own
registerPreload()approach.Changes
Core tracing integration
fetch,http, and Electron'snetmodule. Spans are published via dd-trace's electron exporter to a diagnostics channel, converted to RUM resources bySpanProcessor, and sent through the existing SDK transport.SpanProcessorthat subscribes to dd-trace's diagnostics channel, filters out SDK-internal intake requests, enriches spans with electron context (application ID, session ID, view ID via format hooks), emits RUM resource events for HTTP spans, and forwards all spans to the/api/v2/spansintake.Tracingclass that configures dd-trace plugins (electron,http) and patchesipcMain.handle/handleOnceandglobalThis.fetchto fix AsyncLocalStorage context propagation through Electron's native IPC and network stack, ensuring child spans (net.request/fetch) within IPC handlers correctly parent to the handle span.RawRumResourcetype,RawTraceData/RawSpanDatatypes,SPANStransport track, and span format hooks in the assembly layer.Entry point and preload injection
@datadog/electron-sdk/instrumententry point -- must be imported before electron so dd-trace can hookrequire('electron')and wrapBrowserWindowfor automatic preload injection.registerPreload(), the SDK's preload scripts (src/preload/,src/entries/preload.ts), and associated tests -- preload injection is now fully handled by dd-trace'sBrowserWindowwrapping.tracingconfiguration option -- tracing is enabled by default when dd-trace is available.Bundler plugins
At runtime, dd-trace hooks
require('electron')which breaks when bundlers alter module loading order or when the preload file from dd-trace isn't available in packaged apps. Three bundler plugins are provided:@datadog/electron-sdk/vite-plugin(datadogVitePlugin) -- externalizes dd-trace and the SDK to prevent Vite's require hoisting from breaking hook registration, prepends a CJS or ESM initialization banner before hoisted requires, copies dd-trace and the SDK with their transitive dependencies into the build output'snode_modules/for packaged Forge apps, and emits dd-trace's preload script at the fallback path. ESM output usessession.registerPreloadScript()directly since static imports bypass dd-trace's IITM hooks.@datadog/electron-sdk/webpack-plugin(DatadogWebpackPlugin) -- externalizes dd-trace and the SDK, prepends dd-trace initialization viaBannerPlugin, automatically excludes dd-trace and the SDK from@vercel/webpack-asset-relocator-loader(which would otherwise crash on dd-trace's dynamiccreateRequirepatterns), copies dd-trace's preload script into the webpack output at the fallback path, and copies dd-trace and the SDK with their transitive dependencies into the output'snode_modules/for packaged Forge apps.@datadog/electron-sdk/esbuild-plugin(datadogEsbuildPlugin) -- externalizes dd-trace and the SDK, and prepends a CJS or ESM initialization banner. No dependency copying is needed because plain Forge (without a bundler plugin) includesnode_modules/in the asar.All three plugins are self-contained: no manual
import '@datadog/electron-sdk/instrument'is needed.Build and packaging
@rollup/plugin-json(required by dd-trace)devDependenciestodependenciesso it's installed transitively in consumer appsTesting
fetch,http.request,net.request, and intake traffic filtering.electron-vite-esmandforge-esbuild-cjs/forge-esbuild-esmintegration test apps, covering ESM output and the esbuild plugin.import '@datadog/electron-sdk/instrument'and manual preload imports.datadogVitePlugin()to forge-vite, electron-vite, electron-vite-esm, and electron-builder-vite configs; addDatadogWebpackPluginto forge-webpack config; adddatadogEsbuildPlugin()to forge-esbuild-cjs and forge-esbuild-esm configs.Checklist