Skip to content

docs(rum): add Electron SDK integration docs (zh + en) - #222

Draft
Fiona2016 wants to merge 7 commits into
mainfrom
feat/rum-electron-sdk-docs
Draft

docs(rum): add Electron SDK integration docs (zh + en)#222
Fiona2016 wants to merge 7 commits into
mainfrom
feat/rum-electron-sdk-docs

Conversation

@Fiona2016

@Fiona2016 Fiona2016 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

What

Adds the Electron platform to the RUM SDK docs. zh/rum/sdk/ and en/rum/sdk/ previously covered web / android / ios / flutter / harmony / wechat-miniprogram — there was no electron.

Four pages per language under rum/sdk/electron/, mirroring the harmony/flutter page set, registered in docs.json for both languages:

Page Content
sdk-integration Two-process install, the instrument entry point, bundler plugins, renderer setup, bridge behavior
advanced-config Full init options, batching, self-hosted/proxy, manual reporting, operation monitoring, source map upload
compatible Support scope, bundler matrix, v1 limits
data-collection Per-process event types, fields, session rules, upload behavior

Content is derived from the SDK source on origin/publish rather than upstream Datadog docs — the fork changed intake URL rules, switched to an NDJSON body, and dropped the spans track. Package versions are left unpinned, per the convention of the other npm-based platform docs. Console doc links in utils/docs.ts map to rum/sdk/electron/sdk-integration and rum/sdk/electron/data-collection; both paths exist here.

Why this platform needed more than a copy-paste

Electron is the first RUM platform with a genuinely two-process integration:

  • The main process uses @flashcatcloud/electron-sdk, whose @flashcatcloud/electron-sdk/instrument entry point must be imported before require('electron') — otherwise dd-trace cannot hook BrowserWindow and preload injection silently fails.
  • Renderers use @flashcatcloud/browser-rum and hand events to the main process over IPC through the DatadogEventBridge object the preload exposes. This needs no configuration — the preload always allows the window's own host.
  • Integrating only one side is the expected failure mode. Both sdk-integration and data-collection are written to prevent it.

The two source values

The subtlest thing here. Assembly.assembleRendererRumEvent() overrides only session.id and application.id on bridged renderer events and adds container — the renderer's own source is preserved:

Origin source container.source view.url
Main process electron absent electron://main-process
Renderer window browser electron the page URL

Filtering on source:electron alone returns main-process events only. The docs consistently use source:electron OR container.source:electron. container.source also doubles as the diagnostic for a broken bridge — if renderer events lack it, the preload was never injected.

Error and crash coverage

Three distinct paths, documented separately because they behave differently:

Path Event shape
Node errors (uncaughtException, unhandledRejection, addError) Stack in the backend's @ frame format, source-map resolvable
Native crashes (crashReporter minidumps) is_crash: true, address-based stack, threads, binary images. Parsed on next startup
Process terminations (render-process-gone / child-process-gone) is_crash: false, no stack, meta with process/reason/exit code/url

is_crash: false on terminations is deliberate — the host app is alive, and the backend escalates every is_crash to a critical alert. Dedup between the last two is documented: dump-producing reasons (crashed, oom) are left to crash collection, clean-exit is silent, everything else including killed is reported.

Source maps: the SDK now normalizes stack paths

The matching rule is unchanged — the uploaded prefix and the path in the stack must correspond, and the backend matches on the URL path portion only. What changed is who does the normalizing.

normalizeStackPaths (default true) rewrites every frame below the application root to app:///<path relative to the app root>, for main-process and renderer stacks alike. Frames carry the runtime install path — chosen by the user on macOS, embedding the user name on Windows, remounted on every launch for a Linux AppImage — so previously the only way to get a stable path was to rewrite it yourself.

The section is restructured accordingly:

  • the two-step recipe becomes one step: upload with the prefix the normalization produces (app:///dist/x.js -> /dist)
  • a table of which path shapes are and are not rewritten: node:internal/..., http(s), app.asar.unpacked, already-relative paths, and view.url are all left alone
  • main-process and renderer bundles usually sit in different directories, so they need separate uploads with different prefixes — spelled out, since a single upload is the obvious wrong guess
  • a warning that the prefix is the path (/dist), not app:///dist: the CLI requires either a URL with a host or a leading /, and app:///dist has an empty host

normalizeStackPath (callback, optional) is documented for build layouts a single application root cannot express — output under <app root>/public/dist uploaded as /dist, a linked internal package. It runs before the built-in step, applies to both processes, and a callback that throws falls back rather than taking reporting down.

beforeSend demoted, not deleted

The manual two-step beforeSend rewrite moves to "Advanced: rewriting paths yourself (optional)", with the three cases that still justify it: mapping that depends on other event fields, renderer-only changes, and existing implementations not ready to migrate.

The demotion is safe by construction and the docs say so: the built-in normalization is a strict no-op on paths that are already relative, so an existing beforeSend keeps working and its output is never rewritten twice.

The section carries one warning worth the space — a hand-written regex typically covers one platform and fails silently on the others. The example given matches only the Windows packaged shape and no-ops on macOS, AppImage, and dev builds, with no error anywhere; the stacks just never resolve. Readers who need custom mapping are pointed at normalizeStackPath, which receives an already-unified absolute path.

Paint metrics of pre-warmed windows

correctPrewarmedViewTimings (default true) is new and needed a section of its own, because the symptom is confusing rather than obviously broken.

Pre-creating new BrowserWindow({ show: false }) and navigating it ahead of time is standard Electron practice, and paintWhenInitiallyHidden defaults to true, so the page paints while hidden and never fires visibilitychange. A first render deferred until show() therefore reports paint metrics inflated by the whole pre-warm interval — one measured case had a perfectly normal FCP of 404ms next to an LCP of 8084ms, because LCP keeps updating until a first interaction that cannot happen while hidden.

The docs give the activationStart formula (the same one the Paint Timing spec applies to prerendered pages) and a table of exactly when the correction fires, including the two non-obvious outcomes:

  • a window that was never shown has its FCP/LCP discarded, not corrected — there is no activation instant to rebase onto
  • WebContentsView / <webview> are left untouched, since "not observed" must not be read as "never visible"

Both are also listed under limits, so someone hunting for missing paint metrics finds the reason.

Self-hosted: two cases

site is optional, defaults to browser.flashcat.cloud, and is only checked for non-emptiness.

  • HTTPS intake — set site to your own domain, no proxy
  • Plain-HTTP intakesite cannot help; the upload URL is built from https://<site>/api/v2/rum with the scheme hardcoded, so this requires proxy

Dropping the allowlist looks like it unblocks arbitrary endpoints, and it does not. Stated in all six affected places.

v1 limits documented explicitly

  • No native crash symbolication — collected and stored, shown as raw addresses
  • No Session Replay (defaultPrivacyLevel forwarded but inert)
  • No APM / distributed tracing — only HTTP spans become RUM resources
  • No stack on process-termination events
  • Main process produces no Web Vitals; the console hides the performance section for its synthetic view
  • Upload scheme fixed to HTTPS

Two limits were removed this round because the features above close them: source map matching for file:// pages, and main-process path normalization. They are replaced by the ones that genuinely remain — code outside the application root is not normalized, and visibility tracking covers BrowserWindow only.

Also documents that dd-trace's own instrumentation telemetry is disabled by default (127.0.0.1:8126, with a direct-to-Datadog fallback when DD_API_KEY is set), and that it is env-var controlled since telemetry: false is ignored on dd-trace 5.x.

Review note

The history contains two correction passes against evolving SDK code, so reviewing the squashed diff is more useful than commit by commit. Commit 2 fixed two errors caught by the staging smoke test (the bridge is not opt-in; renderer events keep source: browser). Commit 4 re-synced against publish after PR#1..#5 — most notably main-process stacks became symbolicatable, reversing an earlier claim. Commit 5 covers PR#6 and PR#7 (paint metric correction, stack path normalization) and is what demotes the beforeSend recipe.

Verified with mint broken-links plus a cross-check that every anchor used by the Electron pages resolves to a real heading. The zh and en advanced-config pages have identical heading structure, line for line.

Two changes are known to be coming and are deliberately not reflected yet: FCP/LCP correction (correctPrewarmedViewTimings) and SDK-side stack path normalization. The source map section keeps "why the second step is needed" as its own subsection so the beforeSend flow can be demoted without restructuring the page.

Verification

mint broken-links passes; zh/en section counts match on all four pages.

🤖 Generated with Claude Code

Fiona2016 and others added 7 commits July 28, 2026 08:55
Electron is the first RUM platform that requires a two-process
integration: `@flashcatcloud/electron-sdk` in the main process and
`@flashcatcloud/browser-rum` in renderers, bridged over IPC through the
`DatadogEventBridge` object that dd-trace injects via preload. Integrating
only one side is the failure mode these pages are written to prevent.

Adds four pages per language under `rum/sdk/electron/`, mirroring the
harmony/flutter page set, and registers the group in docs.json for both
languages:

- sdk-integration: install, the `instrument` entry point that must precede
  `require('electron')`, bundler plugins (vite / webpack / esbuild), renderer
  setup, and the `allowedWebViewHosts` allowlist that gates the bridge
- advanced-config: full init options, batching, proxy, manual reporting,
  operation monitoring, source map upload
- compatible: support scope and v1 limits
- data-collection: per-process event types, fields, session rules, upload
  behavior

Content is derived from the SDK source rather than upstream Datadog docs,
since the fork changed intake URL rules, the site allowlist, and dropped the
spans track.

v1 limits documented explicitly: no native crash symbolication (crashes are
stored and shown as raw addresses), no Session Replay, no APM/distributed
tracing, and no source map resolution for main-process stacks. The
`file://` install-path instability that breaks source map matching for
`loadFile()` builds is called out with the custom-protocol workaround.

Doc paths match the console's `utils/docs.ts` mapping for the electron
platform. Package versions are left unpinned, per npm platform convention.

Verified with `mint broken-links`.
Two factual errors from the first commit, both found by W1's staging smoke
test and confirmed against the source.

1. The bridge is not opt-in. The earlier text read `validateAllowedWebViewHosts()`
   returning `[]` for `undefined` as "the bridge is disabled by default", but
   that is the SDK-side config default, not the allowlist the browser SDK
   actually sees. dd-trace's preload builds it as:

       const allowedHosts = [...new Set([location.hostname, ...configuredHosts])]

   The window's own hostname is always included, so a window's own page always
   self-matches and the bridge works with no configuration. `file://` included:
   `location.hostname` is `""` there, the allowlist becomes `[""]`, and
   `canUseEventBridge("")` still matches. `allowedWebViewHosts` is for
   additional third-party hosts in `<webview>` / `BrowserView`, not a switch.

2. Bridged renderer events keep `source: browser`. `Assembly.assembleRendererRumEvent()`
   overrides only `session.id` and `application.id` and adds
   `container.{source, view.id}` — the renderer's own `source` is preserved.
   Main-process events are `source: electron` with
   `view.url: electron://main-process`. Filtering on `source:electron` alone
   therefore returns main-process events only; the correct filter is
   `source:electron OR container.source:electron`.

Changes:
- Replace the "enable the bridge" sections with "the bridge needs no
  configuration" plus a "what a broken bridge looks like" section, since the
  real failure mode is a missing preload injection (main process not
  integrated, or bundled without the plugin), diagnosable via a missing
  `container.source`
- Add a source/container.source/view.url table to both the integration and
  data-collection pages, and correct the verification steps to use the OR filter
- Drop `allowedWebViewHosts` from the basic init examples; it is not needed
- Decouple the `app://` custom-protocol recommendation from the bridge. It now
  stands only on source map path stability, with an explicit note that the
  bridge works fine under `file://`
- Mark the `file://` install-path limitation as affecting source map
  resolution only, not collection
- Align the proxy section with the README: `site` stays required but is unused
  for URL building once `proxy` is set, and name the self-hosted use case

Verified with `mint broken-links`.
Fiona settled the open question: normalize stack paths in `beforeSend`. This
is not a new proposal — it is the workaround already given to customers — so
the section states it directly rather than weighing alternatives.

Restructures the source map section around the matching rule, in her framing:
the uploaded minified prefix and the path in the stack must correspond.
That resolves into two steps, and the whole section is organized around them:

1. Run the CLI where the source maps are and declare a prefix with
   `--minified-path-prefix`
2. Rewrite the stack path in the renderer's `beforeSend` to align with it

Both halves are shown as copy-pasteable code, and the prefix is lifted into a
`MINIFIED_PATH_PREFIX` constant so the two places that must agree are visually
obvious, with a warning naming them as the only coupling point.

Adds a "why the second step is needed" table: stack frames carry the runtime
install path, which is unknowable at build time — the macOS install location
is the user's choice, Windows embeds the user name, and Linux AppImage mounts
somewhere new on every launch. Uploading an install path as the prefix would
match exactly one machine.

The regex was checked against the real stack shapes measured during the W5
verification (`at r @ file:///…/dist/renderer.js:1:21`), for all three
platforms; `[^\s()]*?` rather than `\S*?` so a V8-format frame keeps its
enclosing paren.

The `app://` custom protocol drops to an optional tip under "when
normalization is unnecessary", alongside dev-server and remote-page setups
whose paths are already stable. It no longer leads, since `beforeSend` solves
the same problem without asking anyone to restructure page loading.

Main-process stacks are stated as unsupported for v1 across all six affected
pages: they are raw V8 format and the backend parser extracts zero frames.

Verified with `mint broken-links`.
v1 drops the hardcoded site allowlist: `site` becomes optional, defaults to
`browser.flashcat.cloud`, and is only checked for non-emptiness. The docs no
longer tell self-hosted users to pass a SaaS host as a placeholder and route
everything through `proxy`.

Self-hosted now splits into two cases, which is the part worth getting right:

- HTTPS intake: just set `site` to your own domain, no proxy involved
- Plain-HTTP intake: `site` cannot help, because the upload URL is built from
  the template `https://<site>/api/v2/rum` and the scheme is hardcoded. This
  needs `proxy`

That second case is easy to miss once the allowlist is gone — removing it
looks like it unblocks arbitrary endpoints, and it does not. It is called out
in all six affected pages: as a note under the `site` parameter, as its own
"when a proxy is required" subsection, and as a row in the limits table.

Also aligns two details with decisions from the release work:
- The console special-cases the synthetic main-process view and hides the
  performance section, so LCP/FCP no longer render as zero. Noted where the
  docs explain that the main process has no Web Vitals
- Section renames moved four anchors; all inbound links updated

Verified with `mint broken-links`.
…e, crash resilience

Review of PR#1..#5 on `origin/publish`, which landed after these pages were
written. Three capability changes and one stale claim.

Main-process stacks are now symbolicatable. `ErrorCollection.formatError()`
runs `toStackTraceString(computeStackTrace(error))`, so main-process stacks
come out in the backend's `at <fn> @ <url>:<line>:<col>` frame format instead
of V8's native shape, and frame URLs are the absolute paths of the bundled
main-process code — the same key source map upload uses. The docs said the
opposite in six places; all corrected, and users are now told to upload the
main-process bundle's source maps too.

Two caveats kept, because they are still true and easy to over-read:
- Native crash stacks remain address-based and unsymbolicated
- `beforeSend` is a renderer-only hook, so main-process path normalization
  depends on the install path being predictable. Called out in
  advanced-config and in the limits table rather than left implicit

`ProcessGoneCollection` was entirely undocumented. Added a data-collection
section and a compatibility row covering `render-process-gone` /
`child-process-gone`: the full `meta` shape, `is_crash: false` and why (the
host app is alive, and the backend escalates every `is_crash` to a critical
alert), the absence of a stack, `container.view.id` attribution for
renderers, and the deduplication rule — dump-producing reasons (`crashed`,
`oom`) are left to crash collection, `clean-exit` is silent, everything else
including `killed` is reported.

Crash collection degradation: dumps with no exception stream are now
reported with threads and binary images rather than dropped, and dumps are
deleted whether or not parsing succeeded. Both documented, with the field
table noting which values go missing.

dd-trace's instrumentation telemetry is off by default. Documented because
it is a network-behavior fact worth knowing (`127.0.0.1:8126`, and a direct
Datadog fallback when `DD_API_KEY` is set), including that it is controlled
by environment variable — `telemetry: false` on `tracer.init()` is silently
ignored on dd-trace 5.x — and that an explicit host setting is preserved.

Re-verified the `site` changes from the previous round against `config.ts`:
optional, `DEFAULT_SITE`, non-empty validation only. Already accurate.

The source map section keeps "why the second step is needed" as its own
subsection, so the `beforeSend` flow can be demoted to an alternative once
SDK-side path normalization lands without restructuring the page.

zh/en section counts match on all four pages. Verified with
`mint broken-links`.
…c correction

Two capabilities landed on the SDK's publish branch that the Electron docs
did not cover.

`correctPrewarmedViewTimings` (boolean, default true): a new section explains
why a window created with `show: false` and navigated ahead of time reports an
FCP/LCP inflated by the whole pre-warm interval, the activationStart formula
the correction applies, and a table of exactly when it fires — including the
two cases that surprise people: metrics are discarded for a window that was
never shown, and nothing happens for WebContentsView / <webview>.

`normalizeStackPaths` (boolean, default true) and `normalizeStackPath`
(callback): stacks from both processes now anchor on the application root as
`app:///<relative path>`, so the source map section is restructured around
"upload with the matching prefix" instead of the previous two-step recipe.
Added the table of which path shapes are and are not rewritten, and made the
prefix derivation explicit (app:///dist/x.js -> /dist), including the separate
uploads main-process and renderer bundles need.

The manual `beforeSend` rewrite is demoted from required step to an optional
advanced path, with the cases that still need it and a warning that a
hand-written regex typically covers a single platform and fails silently
elsewhere. It is kept working: the built-in normalization is a strict no-op on
already-relative paths.

Also removed the two limits these features close (file:// source map matching,
main-process path normalization) and replaced them with the honest ones that
remain (code outside the app root, BrowserWindow-only visibility tracking).

Verified with `mint broken-links` plus an anchor cross-check; zh/en heading
structure is identical line for line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
End-to-end validation surfaced three integration mistakes that produce no
error at all — the app keeps running, the SDK stays quiet, and only the data
is missing. Document each with its symptom, why it stays silent, and the fix:

- CSP silently kills Session Replay. Recording creates a blob Worker in the
  renderer and posts segments straight to the intake, so a common
  `script-src 'self'` policy blocks the whole pipeline: `session.has_replay`
  stays 0 with no segments, indistinguishable from never enabling recording.
  Requires `worker-src blob:` plus the intake origin in `connect-src`.
- Source map `--release-version` must match the renderer's `version`. Renderer
  events take `version` from `flashcatRum.init()` only; the main-process
  `init()` value never reaches them. A mismatch resolves nothing and reports
  nothing.
- Replay segments are lost during a network outage. They bypass the
  main-process disk-backed retry, and the browser transport only queues a
  retry when `navigator.onLine === false`, so an unreachable intake on a live
  network drops them outright. The first segment after recovery carries no
  full snapshot, leaving a garbled stretch in playback.

Session Replay is documented as supported (via `sessionReplayDirectUpload`)
in place of the previous "not supported" entries, since both replay pitfalls
depend on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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