diff --git a/docs.json b/docs.json index 6acb5be4..e3078f78 100644 --- a/docs.json +++ b/docs.json @@ -390,6 +390,15 @@ "zh/rum/sdk/web/faq" ] }, + { + "group": "Electron", + "pages": [ + "zh/rum/sdk/electron/sdk-integration", + "zh/rum/sdk/electron/advanced-config", + "zh/rum/sdk/electron/compatible", + "zh/rum/sdk/electron/data-collection" + ] + }, { "group": "Android", "pages": [ @@ -1628,6 +1637,15 @@ "en/rum/sdk/web/faq" ] }, + { + "group": "Electron", + "pages": [ + "en/rum/sdk/electron/sdk-integration", + "en/rum/sdk/electron/advanced-config", + "en/rum/sdk/electron/compatible", + "en/rum/sdk/electron/data-collection" + ] + }, { "group": "Android", "pages": [ diff --git a/en/rum/sdk/electron/advanced-config.mdx b/en/rum/sdk/electron/advanced-config.mdx new file mode 100644 index 00000000..4cd226f6 --- /dev/null +++ b/en/rum/sdk/electron/advanced-config.mdx @@ -0,0 +1,319 @@ +--- +title: "Electron SDK advanced configuration" +description: "Configure the full initialization options, batching, proxy, manual reporting APIs, and source map upload for the Electron RUM SDK" +keywords: ["RUM", "Electron SDK", "advanced configuration", "proxy", "source map", "manual reporting"] +--- + +This page describes the advanced options and manual reporting APIs of the Electron main-process SDK. Renderer-side advanced configuration is identical to the Web SDK — see [Web SDK advanced configuration](/en/rum/sdk/web/advanced-config). + +## Full initialization options + +```ts +import { init } from '@flashcatcloud/electron-sdk'; + +await init({ + applicationId: '', + clientToken: '', + service: 'my-electron-app', + site: 'browser.flashcat.cloud', + env: 'production', + version: app.getVersion(), + telemetrySampleRate: 20, + batchSize: 'MEDIUM', + uploadFrequency: 'NORMAL', +}); +``` + +| Option | Type | Required | Default | Description | +|--------|------|----------|---------|-------------| +| `applicationId` | `string` | Yes | — | RUM application ID | +| `clientToken` | `string` | Yes | — | Client token | +| `service` | `string` | Yes | — | Service name; must match the value used when uploading source maps | +| `site` | `string` | No | `browser.flashcat.cloud` | Reporting site, used directly as the intake host. Self-hosted deployments set their own domain; for a plain-HTTP intake see [When a proxy is required](#when-a-proxy-is-required) | +| `env` | `string` | No | — | Environment identifier such as `production` or `staging` | +| `version` | `string` | No | — | Application version; must match the value used when uploading source maps | +| `proxy` | `string` | No | — | Custom reporting endpoint, see [Self-hosted deployments and proxies](#self-hosted-deployments-and-proxies) | +| `allowedWebViewHosts` | `string[]` | No | `[]` | **Additional** hosts allowed to report through the bridge. A window's own host is always allowed, so configure this only to accept events from third-party pages in a `` / `BrowserView` | +| `telemetrySampleRate` | `number` | No | `20` | SDK internal telemetry sample rate (0–100); set to `0` to disable | +| `batchSize` | `'SMALL' \| 'MEDIUM' \| 'LARGE'` | No | `MEDIUM` | Batch size per upload | +| `uploadFrequency` | `'RARE' \| 'NORMAL' \| 'FREQUENT'` | No | `NORMAL` | Upload interval | +| `defaultPrivacyLevel` | `'mask' \| 'allow' \| 'mask-user-input'` | No | `mask` | Privacy level forwarded to renderers; Session Replay is not supported in this version, so it currently has no effect | + + +`init()` is asynchronous. It returns `false` when validation fails (for example a missing required option). The SDK does not start in that case and prints the specific reason to the console. + + +## Batching and upload frequency + +Uploads are disk-buffered: events are written to batch files under `app.getPath('userData')`, rotated once `batchSize` is reached, and uploaded at the `uploadFrequency` interval. A file is deleted only after a successful upload. + +| `batchSize` | Size | When to use | +|-------------|------|-------------| +| `SMALL` | 16 KiB | Low event volume, you want data to appear quickly | +| `MEDIUM` (default) | 512 KiB | General purpose | +| `LARGE` | 4 MiB | High event volume, you want fewer requests | + +| `uploadFrequency` | Interval | When to use | +|-------------------|----------|-------------| +| `RARE` | 30 seconds | Poor connectivity or power-sensitive applications | +| `NORMAL` (default) | 10 seconds | General purpose | +| `FREQUENT` | 5 seconds | Integration debugging and fast verification | + + +During integration, temporarily use `batchSize: 'SMALL'` with `uploadFrequency: 'FREQUENT'` so events reach the console faster, then revert to the defaults before shipping. + + +## Self-hosted deployments and proxies + +### Just set `site` + +For a self-hosted deployment whose intake speaks HTTPS, set `site` to your own domain — no proxy needed: + +```ts +await init({ + // ... + site: 'rum.example.internal', +}); +``` + +The upload endpoint becomes `https://rum.example.internal/api/v2/rum`. + +### When a proxy is required + +The SDK builds its upload URL from the template `https:///api/v2/rum`, and **the `https://` scheme is hardcoded**. `site` therefore cannot cover these cases, which require `proxy`: + +- The internal intake serves **plain HTTP** only, with no HTTPS +- The upload path is not `/api/v2/rum` and a gateway has to rewrite it +- Clients cannot reach the intake directly and need a single egress point + +```ts +await init({ + // ... + proxy: 'https://rum-proxy.example.internal/forward', +}); +``` + +The resulting request is `POST ?ddforward=%2Fapi%2Fv2%2Frum`. Your proxy must forward the request body to `/api/v2/rum` on your Flashduty instance, preserving the `DD-API-KEY` header. The body is newline-delimited JSON with a `Content-Type` of `text/plain;charset=UTF-8` — do not rewrite it. + + +Once `proxy` is set, `site` no longer contributes to the upload URL and can be omitted. + + + +With `proxy` set, the SDK uses the proxy host to detect and skip its own reporting requests, preventing a collection loop. Make sure the proxy value is a complete absolute URL. + + +## Report errors manually + +Exceptions you catch yourself in the main process are not collected automatically. Report them explicitly: + +```ts +import { addError } from '@flashcatcloud/electron-sdk'; + +try { + await syncWorkspace(); +} catch (error) { + addError(error, { + context: { component: 'sync', workspaceId: 'ws-1001' }, + }); +} +``` + +| Option | Type | Description | +|--------|------|-------------| +| `context` | `Record` | Custom attributes written to the event `context` | +| `startTime` | `number` | Timestamp of the error, defaults to now | + +Manually reported errors carry `error.source` of `custom` and `error.handling` of `handled`, so you can distinguish them from uncaught exceptions in the Explorer. + +## Stop the current session + +To cut a session short — for example on user logout — call `stopSession()`. The current session expires immediately, and the next activity signal starts a new one. + +```ts +import { stopSession } from '@flashcatcloud/electron-sdk'; + +stopSession(); +``` + +## Operation monitoring (preview) + +Track critical business workflows with paired start and end calls. The backend correlates them by `name` (and an optional `operationKey`) and emits `vital` events. + +```ts +import { startOperation, succeedOperation, failOperation } from '@flashcatcloud/electron-sdk'; + +startOperation('checkout'); +try { + await runCheckout(); + succeedOperation('checkout'); +} catch (error) { + failOperation('checkout', 'error'); +} + +// Parallel operations sharing a name are distinguished by operationKey +startOperation('upload', { operationKey: 'profile_pic' }); +startOperation('upload', { operationKey: 'cover_photo' }); +succeedOperation('upload', { operationKey: 'profile_pic' }); +failOperation('upload', 'abandoned', { operationKey: 'cover_photo' }); +``` + +| Parameter | Description | +|-----------|-------------| +| `name` | Required; only letters, digits, and `_` `.` `@` `$` `-` are allowed | +| `failureReason` | One of `'error'`, `'abandoned'`, or `'other'` | +| `options.operationKey` | Distinguishes parallel operations sharing a name | +| `options.context` | Custom attributes merged into the event `context` | +| `options.description` | Description written to `vital.description` | + +Because correlation happens on the backend, an operation **can start in one process and finish in the other** — for example `startOperation` when the renderer's checkout button is clicked, and `succeedOperation` once the main process has placed the order. + + +This API is in preview and its signatures may change before the stable release. + + +## Upload source maps + +Released Electron applications usually minify their JavaScript, so error stacks only contain minified file names and line/column numbers. Uploading source maps lets Flashduty show the original source location in the error details. + + +In the current version, **only renderer JavaScript stacks** can be resolved. Main-process stacks use the native Node.js V8 format, which the backend JavaScript stack parser does not recognize yet, so they are displayed as-is. Uploading source maps does not change how main-process errors are displayed. + + +### The matching rule + +**The prefix you upload must correspond to the path in the error stack.** + +The backend matches on the **path portion** of the URL only — scheme and host are ignored, so `file:///dist/renderer.js`, `app:///dist/renderer.js`, and `/dist/renderer.js` are equivalent. Integration therefore has two steps: + +1. Run the CLI in the directory holding your source maps and **declare a prefix** with `--minified-path-prefix` +2. **Rewrite the stack paths** in the renderer's `beforeSend` hook so they align with the prefix you uploaded + +### Why the second step is needed + +In a packaged Electron application, the paths in an error stack are the **runtime installation paths**. They are unknown at build time and differ per machine: + +| Platform | Actual path in the stack | Predictable at build time? | +|----------|--------------------------|----------------------------| +| macOS | `/Applications/My.app/Contents/Resources/app.asar/dist/renderer.js` | No — the user may install to `~/Applications` | +| Windows | `C:/Users//AppData/Local/Programs//resources/app.asar/dist/renderer.js` | No — contains the user name | +| Linux AppImage | `/tmp/.mount_XXXXXX/resources/app.asar/dist/renderer.js` | No — changes on every launch | + +Uploading with an installation path as the prefix would match exactly one machine. So the unstable prefix has to be **normalized to a fixed virtual prefix** before the event is sent, making every machine's stack look the same. + +### Step 1: generate and upload source maps + +Enable source map output in your renderer build configuration: + + +```ts vite.config.ts +export default defineConfig({ + build: { sourcemap: true }, +}); +``` + +```js webpack.config.js +module.exports = { + mode: 'production', + devtool: 'source-map', +}; +``` + +```ts build.ts +await esbuild.build({ + sourcemap: true, +}); +``` + + +Run the [Flashduty CLI](https://github.com/flashcatcloud/flashcat-cli) in the directory containing the source maps, declaring your chosen virtual prefix (`/dist` here): + +```bash +flashcat-cli sourcemaps upload \ + --service my-electron-app \ + --release-version 1.0.0 \ + --minified-path-prefix /dist \ + --api-key \ + ./out/renderer +``` + + +Do not ship `.map` files inside the distributed application. Remove them from the output directory after uploading and before packaging the asar, to avoid leaking your source code. + + +### Step 2: align the prefix in beforeSend + +Add `beforeSend` to the renderer initialization to replace installation paths in error stacks with the **same** `/dist` prefix: + +```ts renderer.ts +import { flashcatRum } from '@flashcatcloud/browser-rum'; + +// Must match the --minified-path-prefix used at upload time +const MINIFIED_PATH_PREFIX = '/dist'; + +flashcatRum.init({ + applicationId: '', + clientToken: '', + service: 'my-electron-app', + site: 'browser.flashcat.cloud', + version: '1.0.0', + sessionSampleRate: 100, + beforeSend: (event) => { + if (event.type === 'error' && event.error.stack) { + // Normalize "…/dist/renderer.js" to "/dist/renderer.js" + event.error.stack = event.error.stack.replace( + /(?:file:\/\/)?[^\s()]*?\/dist\//g, + `${MINIFIED_PATH_PREFIX}/` + ); + } + }, +}); +``` + + +`MINIFIED_PATH_PREFIX` in the code and `--minified-path-prefix` on the upload command **must match exactly**. These are the only two places that need to agree — get them wrong and nothing matches: the source maps upload successfully, but stacks are never resolved. + + + +Replace `/dist/` in the regular expression with the actual path segment of your build output directory. After the change, confirm in the console error details that stacks now take the stable `/dist/renderer.js` form before verifying resolution. + + + +`beforeSend` can only modify a subset of event fields, and `error.stack` is one of them. The callback needs no return value; returning `false` discards the whole event. See [Web SDK advanced configuration](/en/rum/sdk/web/advanced-config) for the full contract. + + +### When normalization is unnecessary + +If your renderer pages are loaded from a **stable URL**, stack paths are already identical across machines. Skip step 2 and upload with the real prefix: + +| Loading method | URL in the stack | `--minified-path-prefix` | +|----------------|------------------|--------------------------| +| Dev server | `http://localhost:5173/assets/index.js` | `http://localhost:5173/assets` | +| Remote page | `https://app.example.com/assets/index.js` | `https://app.example.com/assets` | +| Custom protocol | `app://assets/index.js` | `app://assets` | + + +A custom protocol (register `app://` with `protocol.handle()` and call `loadURL('app://index.html')`) also makes paths stable by construction, and is an alternative to normalization. It only affects source map paths — the bridge works fine under `file://` and needs no change to how pages are loaded. + + + +`--service` and `--release-version` must exactly match the `service` and `version` passed to the SDK. Make source map upload part of your release build and re-upload on every version. + + +For more details, see [Source mapping and error tracking](/en/rum/error-tracking/source-mapping). + +## Related pages + + + +Integrate both the main process and renderer processes. + + + +Review the support scope and current limits. + + + +See which event types and fields each process contributes. + + diff --git a/en/rum/sdk/electron/compatible.mdx b/en/rum/sdk/electron/compatible.mdx new file mode 100644 index 00000000..6f283398 --- /dev/null +++ b/en/rum/sdk/electron/compatible.mdx @@ -0,0 +1,104 @@ +--- +title: "Electron SDK compatibility" +description: "Review supported Electron versions, operating systems, bundlers, module formats, and current limits for the Electron RUM SDK" +keywords: ["RUM", "Electron SDK", "compatibility", "bundlers", "known limits"] +--- + +This page describes the Electron SDK support scope and current limits so you can confirm whether your project meets the requirements before integration. + +## Support scope + +| Item | Support | +|------|---------| +| Electron version | 39 and later (`peerDependencies: electron >= 39`) | +| Operating systems | macOS, Windows, Linux | +| Main-process package | `@flashcatcloud/electron-sdk` | +| Renderer package | `@flashcatcloud/browser-rum` (the same package as the Web SDK) | +| Module formats | Both CommonJS and ESM builds are shipped | +| RUM data source | Main-process events carry `source: "electron"`; renderer events keep `source: "browser"` with `container.source: "electron"` | +| Reporting | `POST https:///api/v2/rum` | + +## Bundlers + +| Tool | Support | Notes | +|------|---------|-------| +| Vite / electron-vite / Forge + Vite | Plugin provided | `@flashcatcloud/electron-sdk/vite-plugin` | +| Webpack / Forge + Webpack | Plugin provided | `@flashcatcloud/electron-sdk/webpack-plugin` | +| esbuild | Plugin provided | `@flashcatcloud/electron-sdk/esbuild-plugin` | +| No main-process bundling (running `.js` directly) | Supported | Write `import '@flashcatcloud/electron-sdk/instrument'` as the first import | +| Other bundlers | No plugin | You must guarantee the instrument entry point runs before `require('electron')` and keep `dd-trace` and the SDK external | + + +When the main process is bundled, the matching plugin is **required**. Otherwise the bundler's `require` hoisting breaks the `dd-trace` module hook order: the SDK still initializes and reports main-process data, but `BrowserWindow` preload injection fails and renderer processes never receive the bridge object. + + +## Renderer page loading + +A window's own host is always on the bridge allowlist, so **every loading method works out of the box** with no configuration. + +| Loading method | Bridge available | Notes | +|----------------|------------------|-------| +| `loadURL('http://localhost:')` | Yes | No configuration needed | +| `loadURL('https://')` | Yes | No configuration needed | +| Custom protocol (`protocol.handle()` + `loadURL('app://…')`) | Yes | No configuration needed | +| `loadFile()` (`file://`) | Yes | `location.hostname` is an empty string, the allowlist becomes `[""]`, and it still self-matches | +| Third-party pages in `` / `BrowserView` | Needs configuration | Add the other host to `allowedWebViewHosts`; matching supports subdomain suffixes | + +## Automatic collection + +| Capability | Support | Notes | +|------------|---------|-------| +| Main-process session | Supported | Persisted at `userData/_dd_s`, reused after restart | +| Main-process view | Supported | One view per main-process instance | +| Node uncaught exceptions / promise rejections | Supported | `process.on('uncaughtException' \| 'unhandledRejection')` | +| Main-process HTTP requests | Supported | Traced by `dd-trace` for `http`/`https`, `fetch`, and `net.fetch`, converted to `resource` | +| Native crash capture | Supported | Electron `crashReporter` writes minidumps, parsed and uploaded on the next startup | +| Native crash symbolication | Not supported | See "Current limits" | +| Renderer view / action / resource / error / Web Vitals | Supported | Collected by `@flashcatcloud/browser-rum` | +| Session Replay | Not supported | See "Current limits" | + +## Current limits + +| Limit | Description | +|-------|-------------| +| Native crash symbolication | There is no desktop (macOS / Windows / Linux) symbolication pipeline yet. Crash events are stored and displayed with thread stacks and module lists, but only as **raw addresses** — no function names or line numbers | +| Main-process JS stack resolution | Main-process stacks use the native Node.js V8 format, which the backend JavaScript stack parser does not recognize yet, so they are displayed as-is. Source map upload only affects renderer errors | +| Source map matching for `file://` pages | With `loadFile()`, stack paths are runtime installation paths that differ per machine. Normalize the prefix in `beforeSend` before uploading — see [Advanced configuration · Upload source maps](/en/rum/sdk/electron/advanced-config#upload-source-maps). This affects source map resolution only — collection and the bridge are unaffected | +| `source` on renderer events | Even with the bridge working, renderer events keep `source: browser` and are attributed through `container.source: electron`. Filter a whole application with `source:electron OR container.source:electron` | +| Session Replay | Not supported. The `defaultPrivacyLevel` parameter is forwarded to renderers but currently has no effect | +| APM / distributed tracing | Flashduty has no span intake today. IPC and child-process command spans collected by `dd-trace` are dropped locally; only HTTP spans become RUM `resource` events | +| Log reporting | Log-type events sent over the bridge from renderers are not forwarded to the backend in this version | +| Main-process Web Vitals | The main process is a Node.js runtime with no DOM or rendering pipeline, so it produces no LCP / INP / CLS, long task, or user action data. The console hides the performance section for main-process views | +| Session renewal signal | Only renderer `click` actions extend the session. A purely background main process enters a new session after 15 minutes without interaction | +| Upload scheme fixed to HTTPS | The `https://` in the `https:///api/v2/rum` template is hardcoded. If a self-hosted intake serves plain HTTP only, changing `site` does not help — use `proxy`. See [Advanced configuration](/en/rum/sdk/electron/advanced-config#when-a-proxy-is-required) | +| Main-process view counters | `view.action.count` and its siblings count main-process events only, not events bridged from renderers | + +## Symbolication compatibility + +| Frame type | Resolution | Files to upload | +|------------|------------|-----------------| +| Renderer JavaScript | Source maps restore the original file, function name, and line/column; packaged builds need stack paths normalized in `beforeSend` | `.map` files produced by the build | +| Main-process JavaScript | Not resolved in this version; displayed as the raw V8 stack | — | +| Native crash frames (C/C++) | Not resolved in this version | — | + +See [Advanced configuration · Upload source maps](/en/rum/sdk/electron/advanced-config#upload-source-maps) for the upload procedure. + + +The `service` and `version` values used at upload time must exactly match the ones passed to the SDK. Otherwise the console receives error events but cannot map stack frames back to source. Make source map upload part of your release build. + + +## Related pages + + + +Integrate both the main process and renderer processes. + + + +Configure batching, proxy, manual reporting, and source map upload. + + + +See which event types and fields each process contributes. + + diff --git a/en/rum/sdk/electron/data-collection.mdx b/en/rum/sdk/electron/data-collection.mdx new file mode 100644 index 00000000..d5e05027 --- /dev/null +++ b/en/rum/sdk/electron/data-collection.mdx @@ -0,0 +1,232 @@ +--- +title: "Electron SDK data collection" +sidebarTitle: "Data collection" +description: "Learn which event types, fields, and upload behavior the Electron RUM SDK collects in the main process and renderer processes" +keywords: ["RUM", "Electron SDK", "data collection", "main process", "renderer process", "crash"] +--- + +Electron RUM data comes from two processes and is uploaded by the main process. This page describes what each process collects. + +## Collection overview + +| Data type | Collected by | Default | Event type | +|-----------|--------------|---------|------------| +| Application session | Main process | Enabled | Written into every event's `session` | +| Main-process view | Main process | Enabled | `view` | +| Node uncaught exceptions and promise rejections | Main process | Enabled | `error` | +| Manually reported errors | Main process | Manual | `error` | +| Native crashes (minidump) | Main process | Enabled | `error` (`is_crash: true`) | +| Main-process HTTP requests | Main process | Enabled | `resource` | +| Operation monitoring | Main / renderer | Manual (preview) | `vital` | +| Page views, user actions, front-end resources, JS errors, Web Vitals | Renderer process | Enabled | `view` / `action` / `resource` / `error` | +| SDK internal telemetry | Main process | Enabled (20% sampled) | `telemetry` | + +## Common attributes of main-process events + +RUM events produced **by the main process itself** are enriched with the following common context before sending. Renderer events go through a different assembly path — see [How renderer events are identified](#how-renderer-events-are-identified). + +| Field | Description | +|-------|-------------| +| `application.id` | RUM application ID, from `applicationId` | +| `service` | Service name, from `service` | +| `version` | Application version, from `version` | +| `session.id` | Session ID generated by the main process | +| `session.type` | Always `user` | +| `source` | Always `electron` | +| `view.id` | The active view when the event occurred | +| `view.name` / `view.url` | Always `main process` / `electron://main-process` | +| `ddtags` | Includes `sdk_version:` | +| `_dd.format_version` | Always `2` | + +## How renderer events are identified + +When the bridge is working, the main process does **not** rewrite the `source` of renderer events. It overrides three fields and adds one: + +| Field | What the main process does | +|-------|----------------------------| +| `session.id` | Overridden with the main-process session ID | +| `application.id` | Overridden with the configured application ID | +| `container.source` | Added as `electron` | +| `container.view.id` | Added as the current main-process view ID | + +The renderer's own `source`, `view`, `service`, and other attributes are preserved. The two kinds of events are therefore identified differently: + +| 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 in the Explorer returns **main-process events only**. To select everything an Electron application produces, use `source:electron OR container.source:electron`. + + + +When the bridge is broken (the main process has not integrated the SDK, or it is bundled without the plugin so the preload is never injected), the renderer behaves as a standalone web application and uploads directly: `source` is still `browser`, but there is **no `container` field** and the session is unrelated to the main process. See [What a broken bridge looks like](/en/rum/sdk/electron/sdk-integration#what-a-broken-bridge-looks-like). + + +## Session + +The main process owns the session lifecycle. Session state is persisted in the `_dd_s` file under `app.getPath('userData')`, so an unexpired session survives an application restart. + +| Rule | Value | +|------|-------| +| Inactivity timeout | 15 minutes | +| Maximum session duration | 4 hours | +| Activity signal | `click` actions bridged from the renderer | +| On expiry | Sends a final view update with `is_active: false` and deletes `_dd_s` | +| On renewal | After expiry, the next activity signal creates a new session and a new view | + + +Only renderer clicks currently extend the session. If your application runs main-process background work for a long time without UI interaction, the session expires after 15 minutes and later main-process events belong to a new session. + + +## Main-process view + +The main process has no concept of a page, so the SDK maintains **one view per main-process instance** to carry main-process events and measure elapsed time. + +| Field | Description | +|-------|-------------| +| `view.id` | Unique ID generated by the SDK | +| `view.time_spent` | Time since the view was created | +| `view.is_active` | Whether the view is still active; set to `false` when the session expires | +| `view.action.count` / `view.error.count` / `view.resource.count` | Counts of main-process events in this view (renderer events excluded) | +| `_dd.document_version` | View update revision, incremented on every update | + +View updates are sent: + +- Immediately when the view is created +- When a main-process `action` / `error` / `resource` event updates a counter, throttled to a 3-second window +- As a keep-alive update every 5 minutes +- As a final update when the session expires; a new view is created when the session is renewed + +## Error collection + +### Node runtime errors + +At initialization the SDK registers `process.on('uncaughtException')` and `process.on('unhandledRejection')`. + +| Source | `error.source` | `error.handling` | +|--------|----------------|------------------| +| Uncaught exception / unhandled promise rejection | `source` | `unhandled` | +| `addError()` | `custom` | `handled` | + +Error events include `error.id`, `error.message`, `error.stack`, and `error.type` (taken from `Error.name`). When the thrown value is not an `Error` instance, the SDK serializes it and prefixes the message with `Uncaught` or `Provided`; no stack is available in that case. + + +Main-process `error.stack` is the native Node.js V8 stack, reported verbatim. The current version does not apply source map resolution to main-process stacks, so the console shows the raw stack. Renderer JS stacks are not subject to this limit. + + +Manual reports can carry business context: + +```ts +import { addError } from '@flashcatcloud/electron-sdk'; + +try { + await syncWorkspace(); +} catch (error) { + addError(error, { context: { component: 'sync', workspaceId } }); +} +``` + +### Native crashes + +At startup the SDK enables Electron's `crashReporter` (`uploadToServer: false`, `ignoreSystemCrashHandler: true`). On a crash, Electron writes a `.dmp` minidump under `app.getPath('crashDumps')`. + +The process is already dead at crash time, so nothing can be reported then. On the **next startup**, after `app.whenReady()`, the SDK scans that directory recursively, parses each dump with a built-in WASM minidump processor, emits a RUM error event, and deletes the `.dmp` once it has been uploaded. + +Crash event fields: + +| Field | Description | +|-------|-------------| +| `error.is_crash` | Always `true` | +| `error.message` | Always `Application crashed` | +| `error.category` | Always `Exception` | +| `error.type` / `error.meta.exception_type` | Crash type from the minidump | +| `error.source_type` | `macos`, `windows`, or `linux` depending on the operating system | +| `error.meta.code_type` | CPU architecture | +| `error.meta.process` | Application name | +| `error.stack` | Call stack of the crashing thread | +| `error.threads` | Stacks of all threads, with `crashed` marking the crashing one | +| `error.binary_images` | Loaded modules with `uuid` (debug identifier), load address range, architecture, and a system-module flag | + + +In the current version, native crash stacks are stored and displayed as **raw addresses**. Flashduty does not yet provide desktop (macOS / Windows / Linux) native symbolication. You can still identify the crashing module and its frequency, but not function names or line numbers. **Renderer** JavaScript stacks are unaffected and resolve normally once source maps are uploaded. + + +## Main-process network requests + +`dd-trace` automatically traces HTTP requests made by the main process (`http` / `https` modules, `fetch`, `net.fetch`). The SDK subscribes to the `dd-trace` export channel and converts HTTP spans into RUM `resource` events. + +| Field | Description | +|-------|-------------| +| `resource.type` | Always `native`, distinguishing these from renderer `xhr` / `fetch` resources | +| `resource.url` | Request URL | +| `resource.method` | HTTP method, defaults to `GET` | +| `resource.status_code` | HTTP status code | +| `resource.duration` | Request duration | +| `_dd.trace_id` / `_dd.span_id` | Trace identifiers | + +The SDK's own requests to the intake (or proxy) are detected and skipped so they do not feed back into collection. + + +`dd-trace` also traces IPC calls and child-process command execution, but those spans need an APM pipeline to be displayed. In the current version only HTTP spans become RUM resource events; the remaining spans are dropped locally and never uploaded. + + +## Renderer process data + +Renderer processes use `@flashcatcloud/browser-rum` and collect exactly what the Web SDK collects: page views, user actions, `fetch` / XHR / static resources, JS errors, long tasks, and Web Vitals (LCP, INP, CLS, and others). See [Web SDK data collection](/en/rum/sdk/web/data-collection) for field details. + +When the bridge is working, these events go to the main process over IPC for unified upload and additionally receive the main-process `session.id`, `application.id`, and `container` fields — see [How renderer events are identified](#how-renderer-events-are-identified). + + +The main process is a Node.js runtime with no DOM and no rendering pipeline, so it produces **no** Web Vitals, long tasks, or user action data. The console special-cases the synthetic main-process view and hides the performance section on its detail page, so you will not see misleading zero values for LCP or FCP. Page performance analysis is based on renderer data. + + +## Operation monitoring (preview) + +`startOperation` / `succeedOperation` / `failOperation` track the start and end of critical business workflows (login, checkout, file upload) and emit `vital` events. The backend correlates start and end steps by `name` and an optional `operationKey`, so an operation **can start in one process and finish in the other**. + +| Field | Description | +|-------|-------------| +| `vital.name` | Operation name; only letters, digits, and `_` `.` `@` `$` `-` are allowed | +| `vital.description` | Optional description | +| `context` | Optional custom attributes | + + +This API is in preview and its signatures may change before the stable release. + + +## SDK internal telemetry + +The SDK reports its own runtime errors to help diagnose SDK issues. The default sample rate is 20%, adjustable through `telemetrySampleRate`; set it to `0` to disable telemetry entirely. Telemetry events use a fixed `service` of `electron-sdk` and are not counted toward your RUM event volume. + +## Upload behavior + +The main process buffers all events to disk per track before uploading: + +| Setting | Value | +|---------|-------| +| Buffer directory | The `rum/` subdirectory under `app.getPath('userData')` | +| Batch size | Determined by `batchSize`: `SMALL` 16 KiB, `MEDIUM` 512 KiB, `LARGE` 4 MiB; default `MEDIUM` | +| Upload interval | Determined by `uploadFrequency`: `RARE` 30s, `NORMAL` 10s, `FREQUENT` 5s; default `NORMAL` | +| Endpoint | `POST https:///api/v2/rum` | +| Request body | Newline-delimited JSON, one event per line | +| Authentication | `DD-API-KEY` header carrying `clientToken` | + +Events are first written to a `.tmp` file, rotated to `.log` once the batch size is reached, and then sent by the upload loop. **A file is deleted only after a successful upload**, so data survives network outages and forced application termination — pending batches are resent on the next startup. + +## Related pages + + + +Integrate both the main process and renderer processes. + + + +Configure batching, proxy, manual reporting, and source map upload. + + + +Review the support scope and current limits. + + diff --git a/en/rum/sdk/electron/sdk-integration.mdx b/en/rum/sdk/electron/sdk-integration.mdx new file mode 100644 index 00000000..a5f971b4 --- /dev/null +++ b/en/rum/sdk/electron/sdk-integration.mdx @@ -0,0 +1,303 @@ +--- +title: "Electron SDK integration" +description: "Integrate the Flashduty RUM SDK into Electron desktop applications, covering both the main process and renderer processes" +keywords: ["RUM", "Electron SDK", "desktop monitoring", "main process", "renderer process"] +--- + +An Electron application runs a **main process** (Node.js) and one or more **renderer processes** (Chromium). Because the two runtimes are completely different, Electron RUM integration requires two packages: + +| Process | Package | What it collects | +|---------|---------|------------------| +| Main process | `@flashcatcloud/electron-sdk` | Session, view, Node errors, native crashes, main-process network requests | +| Renderer process | `@flashcatcloud/browser-rum` | Page views, user actions, front-end resources, JS errors, Web Vitals | + + +Installing only one half is the most common integration mistake. With only the main-process SDK you lose all front-end interaction data. With only the renderer SDK you lose the session, main-process errors, and native crashes, and events never carry the `container` information that correlates the two processes. Integrate both processes. + + +## How it works + +The main-process SDK is the **single exit point** for the whole pipeline. It does three things: + +1. Uses `dd-trace` to hook `require('electron')`, wrap `BrowserWindow`, and inject a preload script into every renderer process that exposes a global `DatadogEventBridge` object. +2. The renderer's `@flashcatcloud/browser-rum` detects that bridge and sends its collected events back to the main process over IPC instead of uploading them itself. +3. The main process enriches events from both sides — its own events get the full set of common attributes, while renderer events only have `session.id` / `application.id` overridden and `container` added — then buffers them to disk in batches and uploads them to `POST https:///api/v2/rum`. + +```mermaid +graph TB + subgraph Electron application + subgraph Renderer process + BR["@flashcatcloud/browser-rum"] + end + subgraph Main process + DDT["dd-trace"] + SDK["@flashcatcloud/electron-sdk"] + end + end + FC[("Flashduty RUM")] + BR -->|DatadogEventBridge / IPC| SDK + DDT -->|HTTP span| SDK + SDK -->|POST /api/v2/rum| FC +``` + + +Internal module names, plugin names, and the bridge object keep the `Datadog` / `dd-` prefix (for example `DatadogEventBridge` and `datadogVitePlugin`). This follows the upstream fork naming convention and does not affect data routing — events are only sent to the Flashduty `site` you configure. + + +## Prerequisites + +- Electron 39 or later (the SDK declares `peerDependencies: electron >= 39`) +- An **Electron** application created on the [RUM applications](https://console.flashcat.cloud/rum/apps) page of the Flashduty console, with its **Application ID** and **Client Token** +- Network access from your application to `https://browser.flashcat.cloud/api/v2/rum` (or your own endpoint for self-hosted deployments) + +## Install + +```bash +# Main process +npm install @flashcatcloud/electron-sdk + +# Renderer process +npm install @flashcatcloud/browser-rum +``` + +## Main process integration + +### Import the instrument entry point + +`@flashcatcloud/electron-sdk/instrument` must run **before any `electron` import**. It initializes `dd-trace`, which has to register its module hooks before `require('electron')` happens. Otherwise `BrowserWindow` preload injection never takes effect and renderer processes never receive the bridge object. + +```ts main.ts +// Must be the first import in the file +import '@flashcatcloud/electron-sdk/instrument'; + +import { app, BrowserWindow } from 'electron'; +``` + + +Do not let a formatter or import-sorting rule move this line down. If your project uses ESLint's `import/order` or `simple-import-sort`, add an ignore comment for this line. + + +### Initialize the SDK + +Call `init()` before creating any `BrowserWindow`. It is asynchronous and returns `true` when the configuration is valid and initialization succeeded. + +```ts main.ts +import '@flashcatcloud/electron-sdk/instrument'; + +import { app, BrowserWindow } from 'electron'; +import { init } from '@flashcatcloud/electron-sdk'; + +void app.whenReady().then(async () => { + await init({ + applicationId: '', + clientToken: '', + service: 'my-electron-app', + site: 'browser.flashcat.cloud', + env: 'production', + version: app.getVersion(), + }); + + createWindow(); +}); +``` + +#### Required parameters + + +Application ID, available on the applications page + + + +Client token, available on the applications page + + + +Service name used to distinguish services. Use the same value when uploading source maps + + + +`clientToken` is only for client-side RUM reporting — never put a server-side key in client code. + + +#### Reporting site + + +Reporting site, used directly as the intake host. SaaS users can leave it unset; self-hosted deployments set their own domain + + + +The SDK builds the upload URL as `https:///api/v2/rum` — the **`https://` scheme is hardcoded**. If your internal intake is plain HTTP, no value of `site` will help; you must use `proxy` instead. See [When a proxy is required](/en/rum/sdk/electron/advanced-config#when-a-proxy-is-required). + + +See [Advanced configuration](/en/rum/sdk/electron/advanced-config) for the full list of optional parameters. + +## Bundler plugins + +`dd-trace` depends on runtime module loading order, but bundlers (Vite, Webpack, esbuild) reorder, inline, or hoist `require()` calls, which strips `import '@flashcatcloud/electron-sdk/instrument'` of its "runs first" position. The SDK therefore ships three bundler plugins that: + +- Mark `dd-trace` and `@flashcatcloud/electron-sdk` as external so they stay runtime `require`s +- Prepend the instrument initialization to the very top of the main-process entry chunk (**so you no longer need to write that import by hand**) +- Copy the `dd-trace` preload script and the externalized dependencies into the build output's `node_modules`, so packaged applications (such as Electron Forge asars) can resolve them at runtime + +Pick **one** that matches your build setup. + + + +For electron-vite and Electron Forge + Vite. + +```ts vite.config.ts +import { defineConfig } from 'vite'; +import { datadogVitePlugin } from '@flashcatcloud/electron-sdk/vite-plugin'; + +export default defineConfig({ + plugins: [datadogVitePlugin()], +}); +``` + + +Add the plugin to the **main process** build configuration. In electron-vite that is the `main` section, not `renderer`. + + + + +For Electron Forge + Webpack. + +```js webpack.main.config.js +const { DatadogWebpackPlugin } = require('@flashcatcloud/electron-sdk/webpack-plugin'); + +module.exports = { + plugins: [new DatadogWebpackPlugin()], +}; +``` + +The plugin also excludes `dd-trace` and the SDK from `@vercel/webpack-asset-relocator-loader`, which would otherwise break `dd-trace`'s internal dynamic `require.resolve`. + + + +```ts build.ts +import * as esbuild from 'esbuild'; +import { datadogEsbuildPlugin } from '@flashcatcloud/electron-sdk/esbuild-plugin'; + +await esbuild.build({ + entryPoints: ['src/main.ts'], + bundle: true, + platform: 'node', + outfile: 'dist/main.js', + plugins: [datadogEsbuildPlugin()], +}); +``` + + + + +With ESM output, static imports are evaluated before module code runs, so `dd-trace`'s hooks cannot intercept `import 'electron'`. All three plugins handle this by registering the preload directly through `session.defaultSession.registerPreloadScript()`, which is equivalent. No extra configuration is needed. + + +## Renderer process integration + +Pages loaded by renderer processes are integrated exactly like a [Web SDK](/en/rum/sdk/web/sdk-integration) NPM installation — the initialization code is unchanged. + +```ts renderer.ts +import { flashcatRum } from '@flashcatcloud/browser-rum'; + +flashcatRum.init({ + applicationId: '', + clientToken: '', + service: 'my-electron-app', + site: 'browser.flashcat.cloud', + env: 'production', + version: '1.0.0', + sessionSampleRate: 100, + trackResources: true, + trackLongTasks: true, + trackUserInteractions: true, +}); +``` + + +Keep `applicationId`, `clientToken`, `service`, `env`, and `version` identical to the main-process `init()`. Otherwise the two sides land in different applications or versions and cannot be correlated in a single session. + + +### The bridge needs no configuration + +The renderer side requires **no extra wiring**. When the injected preload assembles its host allowlist, it always includes the window's own `location.hostname`: + +```js +const allowedHosts = [...new Set([location.hostname, ...configuredHosts])] +``` + +A window's own page therefore **always matches the allowlist**, and the bridge works out of the box. `file://` is no exception — `location.hostname` is an empty string there, the allowlist becomes `[""]`, and it still self-matches. + + +`allowedWebViewHosts` is not a bridge switch — it is an allowlist for **additional hosts**. Configure it only when you also want to accept events from **third-party** pages loaded in a `` or `BrowserView`, for example `allowedWebViewHosts: ['partner.example.com']`. Matching supports subdomain suffixes. + + +### What a broken bridge looks like + +The bridge fails because of a **missing preload injection**, not because of configuration — usually the main process has not integrated the SDK, or it is bundled without the matching [bundler plugin](#bundler-plugins), so the `instrument` entry point loses its "runs first" position. + +| Behavior | Bridge working | Bridge broken | +|----------|----------------|---------------| +| `container.source` on renderer events | `electron` | field absent | +| Upload path | Batched by the main process | Each renderer uploads directly to the intake | +| Session | Shares the main-process `session.id` | Renderer generates its own session | +| Offline retry | Buffered to disk under the main process `userData`, resent after restart | Relies on browser-side buffering, lost when the process exits | +| User activity | Renderer clicks extend the main-process session | No effect on each other | + + +`container.source` is a reliable signal: when renderer events carry it, the preload was injected and the events really did travel through the main process. + + +### The `source` value of each process + +When the bridge is working, the main process only overrides `session.id` and `application.id` on renderer events and adds `container` — renderer events **keep their own `source: browser`**. The two kinds of events are therefore identified differently: + +| 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 in the Explorer returns **main-process events only**. To select everything an Electron application produces, use `source:electron OR container.source:electron`. + + +## Verify the integration + + + +The main-process log prints SDK initialization output. When `init()` returns `false` it also prints the specific configuration error, such as a missing required option. + + + +- Main process: make an HTTP request and throw an uncaught exception +- Renderer process: click page elements, change routes, and issue a `fetch` + + + +Open the corresponding RUM application and filter by `source:electron OR container.source:electron` in the [Explorer](/en/rum/explorer/overview) to confirm that `view`, `action`, `resource`, and `error` events appear. + +The default upload interval is one batch every 10 seconds, so wait a moment before refreshing. + + + +Seeing both main-process events (`view.url` of `electron://main-process`) and renderer events (`action`, Web Vitals) under the same session means the bridge is working. + +If renderer events have **no `container.source` field**, the preload was not injected: check that the main process calls `init()`, and that a [bundler plugin](#bundler-plugins) is applied when the main process is bundled. + + + +## Next steps + + + +Configure batching, proxy, manual reporting, and source map upload. + + + +Review supported Electron versions, operating systems, bundlers, and current limits. + + + +See which event types, fields, and upload behavior each process contributes. + + diff --git a/zh/rum/sdk/electron/advanced-config.mdx b/zh/rum/sdk/electron/advanced-config.mdx new file mode 100644 index 00000000..27842b64 --- /dev/null +++ b/zh/rum/sdk/electron/advanced-config.mdx @@ -0,0 +1,319 @@ +--- +title: "Electron SDK 高级配置" +description: "配置 Electron RUM SDK 的完整初始化参数、上报批次、代理、手动上报 API 与 sourcemap 上传" +keywords: ["RUM", "Electron SDK", "高级配置", "代理", "sourcemap", "手动上报"] +--- + +本文介绍 Electron 主进程 SDK 的进阶配置项与手动上报 API。渲染进程的进阶配置与 Web SDK 完全一致,见 [Web SDK 高级配置](/zh/rum/sdk/web/advanced-config)。 + +## 完整初始化参数 + +```ts +import { init } from '@flashcatcloud/electron-sdk'; + +await init({ + applicationId: '', + clientToken: '', + service: 'my-electron-app', + site: 'browser.flashcat.cloud', + env: 'production', + version: app.getVersion(), + telemetrySampleRate: 20, + batchSize: 'MEDIUM', + uploadFrequency: 'NORMAL', +}); +``` + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| `applicationId` | `string` | 是 | — | RUM 应用 ID | +| `clientToken` | `string` | 是 | — | 客户端 Token | +| `service` | `string` | 是 | — | 服务名称,需与 sourcemap 上传时一致 | +| `site` | `string` | 否 | `browser.flashcat.cloud` | 上报站点,直接作为 intake 域名。私有化部署填自己的域名;纯 HTTP intake 见[什么时候必须用 proxy](#什么时候必须用-proxy) | +| `env` | `string` | 否 | — | 环境标识,如 `production`、`staging` | +| `version` | `string` | 否 | — | 应用版本号,需与 sourcemap 上传时一致 | +| `proxy` | `string` | 否 | — | 自定义上报地址,见[私有化部署与代理上报](#私有化部署与代理上报) | +| `allowedWebViewHosts` | `string[]` | 否 | `[]` | **额外**允许通过桥接上报的 host。窗口自身的 host 始终被允许,因此只有接收 `` / `BrowserView` 中第三方页面的事件时才需要配置 | +| `telemetrySampleRate` | `number` | 否 | `20` | SDK 自身遥测采样率(0–100),设为 `0` 关闭 | +| `batchSize` | `'SMALL' \| 'MEDIUM' \| 'LARGE'` | 否 | `MEDIUM` | 单批上报大小 | +| `uploadFrequency` | `'RARE' \| 'NORMAL' \| 'FREQUENT'` | 否 | `NORMAL` | 上报间隔 | +| `defaultPrivacyLevel` | `'mask' \| 'allow' \| 'mask-user-input'` | 否 | `mask` | 传递给渲染进程的隐私级别;当前版本不支持 Session Replay,该参数暂无实际效果 | + + +`init()` 是异步的,返回 `false` 表示配置校验失败(例如缺少必填项),此时 SDK 不会启动,并在控制台打印具体原因。 + + +## 上报批次与频率 + +上报是磁盘缓冲的:事件先写入 `app.getPath('userData')` 下的批次文件,达到 `batchSize` 后轮转,再按 `uploadFrequency` 的间隔上传,上传成功才删除文件。 + +| `batchSize` | 单批大小 | 适用场景 | +|-------------|----------|----------| +| `SMALL` | 16 KiB | 事件量小、希望尽快看到数据 | +| `MEDIUM`(默认) | 512 KiB | 通用 | +| `LARGE` | 4 MiB | 事件量大、希望减少请求次数 | + +| `uploadFrequency` | 间隔 | 适用场景 | +|-------------------|------|----------| +| `RARE` | 30 秒 | 弱网或对功耗敏感 | +| `NORMAL`(默认) | 10 秒 | 通用 | +| `FREQUENT` | 5 秒 | 调试接入、需要快速验证 | + + +接入调试阶段可临时使用 `batchSize: 'SMALL'` + `uploadFrequency: 'FREQUENT'`,让事件更快出现在控制台;上线前改回默认值。 + + +## 私有化部署与代理上报 + +### 直接改 site + +私有化部署且 intake 支持 HTTPS 时,把 `site` 填成你自己的域名即可,不需要代理: + +```ts +await init({ + // ... + site: 'rum.example.internal', +}); +``` + +上报地址即 `https://rum.example.internal/api/v2/rum`。 + +### 什么时候必须用 proxy + +SDK 拼接上报地址的模板是 `https:///api/v2/rum`,**其中的 `https://` 是写死的**。因此以下场景 `site` 解决不了,必须改用 `proxy`: + +- 内网 intake 只提供**纯 HTTP**,没有 HTTPS +- 上报路径不是 `/api/v2/rum`,需要网关改写 +- 客户端网络无法直连 intake,需要统一出网口 + +```ts +await init({ + // ... + proxy: 'https://rum-proxy.example.internal/forward', +}); +``` + +实际请求形如 `POST ?ddforward=%2Fapi%2Fv2%2Frum`。代理服务需要把请求体转发到你的 Flashduty 实例的 `/api/v2/rum`,并保留 `DD-API-KEY` 请求头。请求体是换行分隔的 JSON,`Content-Type` 为 `text/plain;charset=UTF-8`,转发时不要改写。 + + +设置 `proxy` 后,`site` 不再参与拼接上报地址,可以省略不填。 + + + +配置 `proxy` 后,SDK 会用代理的 host 来识别并跳过自身上报请求,避免循环采集。请确保代理地址是完整的绝对 URL。 + + +## 手动上报错误 + +主进程中被你自己 `try/catch` 掉的异常不会被自动采集,需要显式上报: + +```ts +import { addError } from '@flashcatcloud/electron-sdk'; + +try { + await syncWorkspace(); +} catch (error) { + addError(error, { + context: { component: 'sync', workspaceId: 'ws-1001' }, + }); +} +``` + +| 选项 | 类型 | 说明 | +|------|------|------| +| `context` | `Record` | 写入事件 `context` 的自定义属性 | +| `startTime` | `number` | 错误发生的时间戳,缺省为当前时间 | + +手动上报的错误 `error.source` 为 `custom`、`error.handling` 为 `handled`,可在查看器中与未捕获异常区分。 + +## 结束当前会话 + +需要在用户登出等场景下主动切断会话时,调用 `stopSession()`。当前会话立即过期,后续活跃信号会开启一个新会话。 + +```ts +import { stopSession } from '@flashcatcloud/electron-sdk'; + +stopSession(); +``` + +## Operation 监控(预览) + +用成对的起止调用跟踪关键业务流程,服务端按 `name`(及可选的 `operationKey`)关联,生成 `vital` 事件。 + +```ts +import { startOperation, succeedOperation, failOperation } from '@flashcatcloud/electron-sdk'; + +startOperation('checkout'); +try { + await runCheckout(); + succeedOperation('checkout'); +} catch (error) { + failOperation('checkout', 'error'); +} + +// 同名并行操作用 operationKey 区分 +startOperation('upload', { operationKey: 'profile_pic' }); +startOperation('upload', { operationKey: 'cover_photo' }); +succeedOperation('upload', { operationKey: 'profile_pic' }); +failOperation('upload', 'abandoned', { operationKey: 'cover_photo' }); +``` + +| 参数 | 说明 | +|------|------| +| `name` | 必填,仅允许字母、数字和 `_` `.` `@` `$` `-` | +| `failureReason` | `'error'`、`'abandoned'` 或 `'other'` | +| `options.operationKey` | 区分同名的并行操作 | +| `options.context` | 合并进事件 `context` 的自定义属性 | +| `options.description` | 写入 `vital.description` 的描述 | + +由于关联发生在服务端,你可以**在一个进程开始、在另一个进程结束**——例如渲染进程点击「结算」时 `startOperation`,主进程完成落单后 `succeedOperation`。 + + +该 API 处于预览阶段,签名可能在正式版前调整。 + + +## 上传 sourcemap + +Electron 应用发布时通常会压缩 JavaScript 产物,错误堆栈因此只有压缩后的文件名和行列号。上传 sourcemap 后,Flashduty 会在异常详情中展示还原后的源码位置。 + + +当前版本**只有渲染进程的 JavaScript 错误栈**支持反混淆。主进程错误栈是 Node.js 原生的 V8 格式,服务端的 JavaScript 栈解析器暂不识别,会按原始栈展示。上传 sourcemap 不会改变主进程错误的展示效果。 + + +### 匹配原则 + +**上报的压缩前缀,要和错误栈里的 path 能对应上。** + +服务端只用 URL 的 **path 部分**做匹配,协议和 host 会被忽略——`file:///dist/renderer.js`、`app:///dist/renderer.js` 与 `/dist/renderer.js` 三者等价。因此接入分两步: + +1. 在有 sourcemap 的目录执行 CLI 上传,用 `--minified-path-prefix` **指定一个前缀** +2. 在渲染进程的 `beforeSend` 钩子里**处理错误栈的 path**,与上报的前缀对齐 + +### 为什么需要第二步 + +Electron 打包后,错误栈里的路径是**应用运行时的安装路径**,构建期不可知,且逐台机器不同: + +| 平台 | 栈里的实际路径 | 构建期可预知? | +|------|----------------|----------------| +| macOS | `/Applications/My.app/Contents/Resources/app.asar/dist/renderer.js` | 否,用户可能装到 `~/Applications` | +| Windows | `C:/Users/<用户名>/AppData/Local/Programs//resources/app.asar/dist/renderer.js` | 否,含用户名 | +| Linux AppImage | `/tmp/.mount_XXXXXX/resources/app.asar/dist/renderer.js` | 否,每次启动都变 | + +如果直接拿安装路径当前缀上传,一次上传只能匹配一台机器。所以要在上报前把这段不稳定的前缀**归一化成一个固定的虚拟前缀**,让所有机器的栈都长一样。 + +### 第一步:生成并上传 sourcemap + +在渲染进程的打包配置中开启 sourcemap 输出: + + +```ts vite.config.ts +export default defineConfig({ + build: { sourcemap: true }, +}); +``` + +```js webpack.config.js +module.exports = { + mode: 'production', + devtool: 'source-map', +}; +``` + +```ts build.ts +await esbuild.build({ + sourcemap: true, +}); +``` + + +在 sourcemap 所在目录执行 [Flashduty CLI](https://github.com/flashcatcloud/flashcat-cli),用 `--minified-path-prefix` 指定你选定的虚拟前缀(这里用 `/dist`): + +```bash +flashcat-cli sourcemaps upload \ + --service my-electron-app \ + --release-version 1.0.0 \ + --minified-path-prefix /dist \ + --api-key \ + ./out/renderer +``` + + +不要把 `.map` 文件打进最终分发的应用包。请在上传后、打包 asar 之前把它们从产物目录中移除,避免泄露源码。 + + +### 第二步:在 beforeSend 里对齐前缀 + +在渲染进程初始化时加上 `beforeSend`,把错误栈里的安装路径替换成**同一个** `/dist` 前缀: + +```ts renderer.ts +import { flashcatRum } from '@flashcatcloud/browser-rum'; + +// 必须与上传时的 --minified-path-prefix 完全一致 +const MINIFIED_PATH_PREFIX = '/dist'; + +flashcatRum.init({ + applicationId: '', + clientToken: '', + service: 'my-electron-app', + site: 'browser.flashcat.cloud', + version: '1.0.0', + sessionSampleRate: 100, + beforeSend: (event) => { + if (event.type === 'error' && event.error.stack) { + // 把 "…<任意安装路径>/dist/renderer.js" 归一成 "/dist/renderer.js" + event.error.stack = event.error.stack.replace( + /(?:file:\/\/)?[^\s()]*?\/dist\//g, + `${MINIFIED_PATH_PREFIX}/` + ); + } + }, +}); +``` + + +代码里的 `MINIFIED_PATH_PREFIX` 与上传命令的 `--minified-path-prefix` **必须逐字一致**。这是两处唯一需要对齐的地方,写错就匹配不上——sourcemap 能上传成功,但堆栈不会被还原。 + + + +正则里的 `/dist/` 要换成你构建产物目录在路径中的实际片段。改完后建议先在控制台的异常详情里确认栈已变成 `/dist/renderer.js` 这种稳定形态,再验证反混淆效果。 + + + +`beforeSend` 只能修改事件的部分字段,`error.stack` 在可修改之列。回调不需要返回值;返回 `false` 会丢弃整条事件。完整说明见 [Web SDK 高级配置](/zh/rum/sdk/web/advanced-config)。 + + +### 无需归一化的场景 + +如果渲染进程页面本来就通过**稳定 URL** 加载,栈里的路径在所有机器上一致,可以跳过第二步,直接用真实前缀上传: + +| 页面加载方式 | 堆栈中的 URL 形态 | `--minified-path-prefix` | +|--------------|-------------------|--------------------------| +| 开发态 dev server | `http://localhost:5173/assets/index.js` | `http://localhost:5173/assets` | +| 远程页面 | `https://app.example.com/assets/index.js` | `https://app.example.com/assets` | +| 自定义协议 | `app://assets/index.js` | `app://assets` | + + +自定义协议(通过 `protocol.handle()` 注册 `app://` 并 `loadURL('app://index.html')`)也能让路径天然稳定,可作为归一化之外的另一种选择。它只影响 sourcemap 路径——桥接在 `file://` 下本来就能正常工作,不需要为此改变页面加载方式。 + + + +`--service` 与 `--release-version` 必须与 SDK 初始化中的 `service` 和 `version` 完全一致。请把 sourcemap 上传纳入发布构建流程,并在每次版本发布时重新上传。 + + +更多说明见[源码映射与异常追踪](/zh/rum/error-tracking/source-mapping)。 + +## 相关页面 + + + +完成主进程与渲染进程的双进程接入。 + + + +了解支持范围与当前限制。 + + + +查看两个进程分别采集的事件类型与字段。 + + diff --git a/zh/rum/sdk/electron/compatible.mdx b/zh/rum/sdk/electron/compatible.mdx new file mode 100644 index 00000000..29ac6c5d --- /dev/null +++ b/zh/rum/sdk/electron/compatible.mdx @@ -0,0 +1,104 @@ +--- +title: "Electron SDK 兼容性" +description: "了解 Electron RUM SDK 支持的 Electron 版本、操作系统、打包工具、模块格式和当前限制" +keywords: ["RUM", "Electron SDK", "兼容性", "打包工具", "已知限制"] +--- + +本文说明 Electron SDK 的支持范围和当前限制,帮助你在接入前判断工程是否满足要求。 + +## 支持范围 + +| 项目 | 支持情况 | +|------|----------| +| Electron 版本 | 39 及以上(`peerDependencies: electron >= 39`) | +| 操作系统 | macOS、Windows、Linux | +| 主进程包 | `@flashcatcloud/electron-sdk` | +| 渲染进程包 | `@flashcatcloud/browser-rum`(与 Web SDK 同一个包) | +| 模块格式 | 同时提供 CommonJS 与 ESM 产物 | +| RUM 数据源 | 主进程事件写入 `source: "electron"`;渲染进程事件保持 `source: "browser"`,附带 `container.source: "electron"` | +| 数据上报 | `POST https:///api/v2/rum` | + +## 打包工具 + +| 工具 | 支持情况 | 说明 | +|------|----------|------| +| Vite / electron-vite / Forge + Vite | 提供插件 | `@flashcatcloud/electron-sdk/vite-plugin` | +| Webpack / Forge + Webpack | 提供插件 | `@flashcatcloud/electron-sdk/webpack-plugin` | +| esbuild | 提供插件 | `@flashcatcloud/electron-sdk/esbuild-plugin` | +| 不打包主进程(直接运行 `.js`) | 支持 | 手写 `import '@flashcatcloud/electron-sdk/instrument'` 作为第一行导入即可 | +| 其他打包工具 | 未提供插件 | 需要你自行保证 instrument 入口先于 `require('electron')` 执行,并把 `dd-trace` 与 SDK 保留为 external | + + +主进程被打包时**必须**使用对应插件,否则打包工具的 `require` 提升会破坏 `dd-trace` 的模块挂钩顺序:SDK 仍能初始化并上报主进程数据,但 `BrowserWindow` 的 preload 注入会失效,渲染进程拿不到桥接对象。 + + +## 渲染进程页面加载方式 + +窗口自身的 host 始终在桥接白名单内,因此**所有加载方式都开箱即用**,无需配置。 + +| 加载方式 | 桥接可用 | 说明 | +|----------|----------|------| +| `loadURL('http://localhost:')` | 支持 | 无需配置 | +| `loadURL('https://')` | 支持 | 无需配置 | +| 自定义协议(`protocol.handle()` + `loadURL('app://…')`) | 支持 | 无需配置 | +| `loadFile()`(`file://`) | 支持 | `location.hostname` 为空字符串,白名单为 `[""]`,仍然自匹配 | +| `` / `BrowserView` 里的第三方页面 | 需配置 | 把对方 host 加入 `allowedWebViewHosts`,匹配规则支持子域名后缀 | + +## 自动采集能力 + +| 能力 | 支持情况 | 说明 | +|------|----------|------| +| 主进程会话 | 支持 | 持久化在 `userData/_dd_s`,重启可续用 | +| 主进程 view | 支持 | 每个主进程实例一个 view | +| Node 未捕获异常 / Promise 拒绝 | 支持 | `process.on('uncaughtException' \| 'unhandledRejection')` | +| 主进程 HTTP 请求 | 支持 | 由 `dd-trace` 追踪 `http`/`https`、`fetch`、`net.fetch`,转成 `resource` | +| 原生崩溃采集 | 支持 | Electron `crashReporter` 写 minidump,下次启动解析上报 | +| 原生崩溃符号化 | 不支持 | 见「当前限制」 | +| 渲染进程 view / action / resource / error / Web Vitals | 支持 | 由 `@flashcatcloud/browser-rum` 采集 | +| Session Replay | 不支持 | 见「当前限制」 | + +## 当前限制 + +| 限制 | 说明 | +|------|------| +| 原生崩溃符号化 | 桌面端(macOS / Windows / Linux)尚无符号解析链路。崩溃事件能正常落库并展示线程栈与模块列表,但只有**原始地址**,不会还原为函数名和行号 | +| 主进程 JS 栈反混淆 | 主进程错误栈是 Node.js 原生的 V8 格式,服务端 JavaScript 栈解析器暂不识别,按原始栈展示。上传 sourcemap 只对渲染进程错误生效 | +| `file://` 页面的 sourcemap 匹配 | 使用 `loadFile()` 时堆栈路径是运行时安装路径,逐台机器不同。需在 `beforeSend` 里把前缀归一化后再上传,见[高级配置 · 上传 sourcemap](/zh/rum/sdk/electron/advanced-config#上传-sourcemap)。这只影响 sourcemap 反混淆,不影响数据采集与桥接 | +| 渲染进程事件的 `source` | 桥接生效时渲染进程事件仍是 `source: browser`,靠 `container.source: electron` 标识归属。筛选整个应用需用 `source:electron OR container.source:electron` | +| Session Replay | 当前不支持。`defaultPrivacyLevel` 参数会传递给渲染进程,但暂不产生实际效果 | +| APM / 分布式追踪 | Flashduty 当前没有 span 上报入口。`dd-trace` 采集的 IPC 调用与子进程命令 span 在本地丢弃,只有 HTTP span 会转成 RUM `resource` 事件 | +| 日志上报 | 渲染进程通过桥接发来的 log 类型事件当前不会转发到服务端 | +| 主进程 Web Vitals | 主进程是 Node.js 运行时,没有 DOM 和渲染管线,不会产生 LCP / INP / CLS、long task 和用户操作数据。控制台会对主进程 view 隐藏性能指标区 | +| 会话续期信号 | 目前只有渲染进程的 `click` action 会续期会话。纯后台运行的主进程会在 15 分钟无交互后进入新会话 | +| 上报协议固定 HTTPS | 上报地址模板 `https:///api/v2/rum` 中的 `https://` 是写死的。私有化部署若 intake 只提供纯 HTTP,改 `site` 无效,必须走 `proxy`,见[高级配置](/zh/rum/sdk/electron/advanced-config#什么时候必须用-proxy) | +| 主进程 view 计数 | `view.action.count` 等计数只统计主进程事件,不包含桥接过来的渲染进程事件 | + +## 符号解析兼容性 + +| 栈帧类型 | 解析方式 | 所需上传文件 | +|----------|----------|--------------| +| 渲染进程 JavaScript | 使用 sourcemap 还原源文件、函数名和行列号;打包后需在 `beforeSend` 里归一化栈路径 | 构建产生的 `.map` 文件 | +| 主进程 JavaScript | 当前不解析,按原始 V8 栈展示 | — | +| 原生崩溃帧(C/C++) | 当前不解析 | — | + +上传方式见[高级配置 · 上传 sourcemap](/zh/rum/sdk/electron/advanced-config#上传-sourcemap)。 + + +上传时的 `service` 与 `version` 必须与 SDK 初始化中的值完全一致,否则控制台可以收到错误事件,但无法把栈帧还原到源码位置。请把 sourcemap 上传纳入发布构建流程。 + + +## 相关页面 + + + +完成主进程与渲染进程的双进程接入。 + + + +配置上报批次、代理、手动上报与 sourcemap 上传。 + + + +查看两个进程分别采集的事件类型与字段。 + + diff --git a/zh/rum/sdk/electron/data-collection.mdx b/zh/rum/sdk/electron/data-collection.mdx new file mode 100644 index 00000000..c38fea97 --- /dev/null +++ b/zh/rum/sdk/electron/data-collection.mdx @@ -0,0 +1,232 @@ +--- +title: "Electron SDK 数据收集" +sidebarTitle: "数据收集" +description: "了解 Electron RUM SDK 在主进程与渲染进程分别采集的事件类型、字段和上报行为" +keywords: ["RUM", "Electron SDK", "数据收集", "主进程", "渲染进程", "崩溃"] +--- + +Electron RUM 的数据来自两个进程,最终由主进程统一上报。本文按进程说明采集内容。 + +## 采集概览 + +| 数据类型 | 采集进程 | 默认状态 | 事件类型 | +|----------|----------|----------|----------| +| 应用会话 | 主进程 | 开启 | 写入所有事件的 `session` | +| 主进程 view | 主进程 | 开启 | `view` | +| Node 未捕获异常与 Promise 拒绝 | 主进程 | 开启 | `error` | +| 手动上报错误 | 主进程 | 手动 | `error` | +| 原生崩溃(minidump) | 主进程 | 开启 | `error`(`is_crash: true`) | +| 主进程 HTTP 请求 | 主进程 | 开启 | `resource` | +| Operation 监控 | 主进程 / 渲染进程 | 手动(预览) | `vital` | +| 页面 view、用户操作、前端资源、JS 错误、Web Vitals | 渲染进程 | 开启 | `view` / `action` / `resource` / `error` | +| SDK 自身遥测 | 主进程 | 开启(采样 20%) | `telemetry` | + +## 主进程事件的公共属性 + +**主进程自己产生的** RUM 事件在发送前会被补充以下公共上下文。渲染进程事件走另一条装配路径,见[渲染进程事件的标识](#渲染进程事件的标识)。 + +| 字段 | 说明 | +|------|------| +| `application.id` | RUM 应用 ID,来自 `applicationId` | +| `service` | 服务名称,来自 `service` | +| `version` | 应用版本,来自 `version` | +| `session.id` | 主进程生成的会话 ID | +| `session.type` | 固定为 `user` | +| `source` | 固定为 `electron` | +| `view.id` | 事件发生时的活跃 view ID | +| `view.name` / `view.url` | 固定为 `main process` / `electron://main-process` | +| `ddtags` | 包含 `sdk_version:` | +| `_dd.format_version` | 固定为 `2` | + +## 渲染进程事件的标识 + +桥接生效时,主进程**不会**改写渲染进程事件的 `source`。它只覆盖三处、补上一处: + +| 字段 | 主进程的处理 | +|------|--------------| +| `session.id` | 覆盖为主进程会话 ID | +| `application.id` | 覆盖为主进程配置的应用 ID | +| `container.source` | 补充为 `electron` | +| `container.view.id` | 补充为主进程当前 view ID | + +渲染进程自己的 `source`、`view`、`service` 等属性一律保留。因此两类事件的标识不同: + +| 事件来源 | `source` | `container.source` | `view.url` | +|----------|----------|--------------------|------------| +| 主进程 | `electron` | 无 | `electron://main-process` | +| 渲染进程窗口 | `browser` | `electron` | 页面 URL | + + +在查看器里筛选时,只用 `source:electron` **只能查到主进程事件**。要选中一个 Electron 应用产生的全部数据,请用 `source:electron OR container.source:electron`。 + + + +桥接未生效时(主进程未接入 SDK,或主进程打包时缺少插件导致 preload 未注入),渲染进程会作为独立的 Web 应用直连上报:`source` 仍是 `browser`,但**没有 `container` 字段**,会话也与主进程无关。详见[接入指南](/zh/rum/sdk/electron/sdk-integration#桥接未生效时会怎样)。 + + +## 会话 + +主进程负责会话生命周期,会话状态持久化在 `app.getPath('userData')` 下的 `_dd_s` 文件中,应用重启后可以续用未过期的会话。 + +| 规则 | 值 | +|------|----| +| 无活跃过期时间 | 15 分钟 | +| 会话最大时长 | 4 小时 | +| 活跃信号 | 渲染进程桥接过来的 `click` 类型 action | +| 过期行为 | 发送一条 `is_active: false` 的最终 view 更新,并删除 `_dd_s` | +| 续期行为 | 会话过期后再次产生活跃信号时,创建新会话并开启新 view | + + +当前只有渲染进程的点击会续期会话。如果你的应用长时间只有主进程后台活动而没有界面交互,会话会在 15 分钟后过期,此后的主进程事件会归属到新会话。 + + +## 主进程 view + +主进程没有页面概念,SDK 为每个主进程实例维护**一个 view**,用于承载主进程事件并计算停留时长。 + +| 字段 | 说明 | +|------|------| +| `view.id` | SDK 生成的唯一 ID | +| `view.time_spent` | 从 view 创建到当前的时长 | +| `view.is_active` | view 是否仍活跃;会话过期时置为 `false` | +| `view.action.count` / `view.error.count` / `view.resource.count` | 该 view 内主进程事件计数(不含渲染进程事件) | +| `_dd.document_version` | view 更新版本号,每次更新递增 | + +view 更新的发送时机: + +- view 创建时立即发送一条 +- 主进程产生 `action` / `error` / `resource` 事件时更新计数,更新以 3 秒为窗口做节流 +- 每 5 分钟发送一次保活更新 +- 会话过期时发送最终更新,会话续期时创建新 view + +## 错误采集 + +### Node 运行时错误 + +SDK 在初始化时注册 `process.on('uncaughtException')` 与 `process.on('unhandledRejection')`。 + +| 来源 | `error.source` | `error.handling` | +|------|----------------|------------------| +| 未捕获异常 / 未处理 Promise 拒绝 | `source` | `unhandled` | +| `addError()` 手动上报 | `custom` | `handled` | + +错误事件包含 `error.id`、`error.message`、`error.stack`、`error.type`(取自 `Error.name`)。抛出的不是 `Error` 实例时,SDK 会把值序列化并加上 `Uncaught` / `Provided` 前缀作为消息,此时没有堆栈。 + + +主进程的 `error.stack` 是 Node.js 原生的 V8 栈,原样上报。当前版本不对主进程栈做 sourcemap 反混淆,控制台展示的是原始栈。渲染进程的 JS 错误栈不受此限制。 + + +手动上报可以附带业务上下文: + +```ts +import { addError } from '@flashcatcloud/electron-sdk'; + +try { + await syncWorkspace(); +} catch (error) { + addError(error, { context: { component: 'sync', workspaceId } }); +} +``` + +### 原生崩溃 + +SDK 启动时会开启 Electron 的 `crashReporter`(`uploadToServer: false`、`ignoreSystemCrashHandler: true`),崩溃时由 Electron 在 `app.getPath('crashDumps')` 下写出 `.dmp` minidump 文件。 + +崩溃发生的那一刻进程已经死亡,无法上报。SDK 在**下一次启动**且 `app.whenReady()` 后递归扫描该目录,用内置的 WASM minidump 解析器逐个解析,生成 RUM error 事件,上报成功后删除对应 `.dmp` 文件。 + +崩溃事件的字段: + +| 字段 | 说明 | +|------|------| +| `error.is_crash` | 固定为 `true` | +| `error.message` | 固定为 `Application crashed` | +| `error.category` | 固定为 `Exception` | +| `error.type` / `error.meta.exception_type` | minidump 中的崩溃类型 | +| `error.source_type` | 按操作系统取 `macos`、`windows` 或 `linux` | +| `error.meta.code_type` | CPU 架构 | +| `error.meta.process` | 应用名称 | +| `error.stack` | 崩溃线程的调用栈 | +| `error.threads` | 所有线程的栈,`crashed` 标记崩溃线程 | +| `error.binary_images` | 加载的模块列表,含 `uuid`(debug identifier)、加载地址区间、架构,并标记系统模块 | + + +当前版本的原生崩溃栈以**原始地址形式**存储和展示,Flashduty 尚未提供桌面端(macOS / Windows / Linux)原生符号化能力。你可以据此判断崩溃模块与频次,但无法直接看到函数名和行号。**渲染进程**的 JavaScript 错误栈不受影响,上传 sourcemap 后可正常还原。 + + +## 主进程网络请求 + +`dd-trace` 会自动追踪主进程发起的 HTTP 请求(`http` / `https` 模块、`fetch`、`net.fetch`)。SDK 订阅 `dd-trace` 的导出通道,把其中的 HTTP span 转换为 RUM `resource` 事件。 + +| 字段 | 说明 | +|------|------| +| `resource.type` | 固定为 `native`,用于与渲染进程的 `xhr` / `fetch` 资源区分 | +| `resource.url` | 请求 URL | +| `resource.method` | 请求方法,缺省为 `GET` | +| `resource.status_code` | HTTP 状态码 | +| `resource.duration` | 请求耗时 | +| `_dd.trace_id` / `_dd.span_id` | 链路标识 | + +SDK 自身上报到 intake(或代理)的请求会被识别并跳过,不会产生循环采集。 + + +`dd-trace` 同时会追踪 IPC 调用和子进程命令执行,但这些 span 需要 APM 链路才能展示。当前版本只有 HTTP span 会转成 RUM resource 事件,其余 span 在本地丢弃,不会上报。 + + +## 渲染进程数据 + +渲染进程使用 `@flashcatcloud/browser-rum`,采集内容与 Web SDK 完全一致:页面 view、用户操作、`fetch` / XHR / 静态资源、JS 错误、long task 和 Web Vitals(LCP、INP、CLS 等)。字段明细见 [Web SDK 数据收集](/zh/rum/sdk/web/data-collection)。 + +桥接生效时,这些事件经 IPC 交给主进程统一上报,并额外获得主进程的 `session.id`、`application.id` 与 `container` 字段——具体见[渲染进程事件的标识](#渲染进程事件的标识)。 + + +主进程是 Node.js 运行时,没有 DOM 也没有渲染管线,因此**不会**产生 Web Vitals、long task 和用户操作数据。控制台已对主进程合成 view 特判:详情页不再展示性能指标区,不会出现 LCP / FCP 为 0 的误导数值。按页面性能维度分析时,数据来自渲染进程。 + + +## Operation 监控(预览) + +`startOperation` / `succeedOperation` / `failOperation` 用于跟踪关键业务流程(登录、结算、文件上传等)的起止,生成 `vital` 事件。服务端按 `name` 和可选的 `operationKey` 关联起止步骤,因此**可以在一个进程开始、在另一个进程结束**。 + +| 字段 | 说明 | +|------|------| +| `vital.name` | 操作名称,仅允许字母、数字和 `_` `.` `@` `$` `-` | +| `vital.description` | 可选描述 | +| `context` | 可选的自定义属性 | + + +该 API 处于预览阶段,签名可能在正式版前调整。 + + +## SDK 自身遥测 + +SDK 会上报自身运行时的内部错误,用于定位 SDK 问题。默认采样率为 20%,可通过 `telemetrySampleRate` 调整,设为 `0` 可完全关闭。遥测事件的 `service` 固定为 `electron-sdk`,不会计入你的 RUM 事件量口径。 + +## 上报行为 + +主进程把所有事件按 track 落盘成批后上传: + +| 配置 | 值 | +|------|----| +| 落盘目录 | `app.getPath('userData')` 下的 `rum/` 子目录 | +| 单批大小 | `batchSize` 决定:`SMALL` 16 KiB、`MEDIUM` 512 KiB、`LARGE` 4 MiB,默认 `MEDIUM` | +| 上传间隔 | `uploadFrequency` 决定:`RARE` 30 秒、`NORMAL` 10 秒、`FREQUENT` 5 秒,默认 `NORMAL` | +| 上报地址 | `POST https:///api/v2/rum` | +| 请求体 | 换行分隔的 JSON(每行一个事件) | +| 认证 | 请求头 `DD-API-KEY` 携带 `clientToken` | + +事件先写入 `.tmp` 文件,达到批次大小后轮转为 `.log`,再由上传循环发送。**上传成功才删除文件**,因此网络中断或应用被强杀时数据不会丢失,下次启动会继续发送残留批次。 + +## 相关页面 + + + +完成主进程与渲染进程的双进程接入。 + + + +配置上报批次、代理、手动上报与 sourcemap 上传。 + + + +了解支持范围与当前限制。 + + diff --git a/zh/rum/sdk/electron/sdk-integration.mdx b/zh/rum/sdk/electron/sdk-integration.mdx new file mode 100644 index 00000000..07c3056e --- /dev/null +++ b/zh/rum/sdk/electron/sdk-integration.mdx @@ -0,0 +1,303 @@ +--- +title: "Electron SDK 接入指南" +description: "在 Electron 桌面应用中接入 Flashduty RUM SDK,完成主进程与渲染进程的双进程采集" +keywords: ["RUM", "Electron SDK", "桌面应用监控", "主进程", "渲染进程"] +--- + +Electron 应用由**主进程**(Node.js)和**渲染进程**(Chromium)组成,两者的运行时完全不同。因此 Electron RUM 接入需要安装两个包: + +| 进程 | 安装的包 | 采集内容 | +|------|----------|----------| +| 主进程 | `@flashcatcloud/electron-sdk` | 会话、view、Node 错误、原生崩溃、主进程网络请求 | +| 渲染进程 | `@flashcatcloud/browser-rum` | 页面 view、用户操作、前端资源请求、JS 错误、Web Vitals | + + +只装一半是最常见的接入错误。只装主进程 SDK 会丢失全部前端交互数据;只装渲染进程 SDK 则拿不到会话、主进程错误和原生崩溃,事件也不会带上关联两个进程的 `container` 信息。请两个进程都完成接入。 + + +## 工作原理 + +主进程 SDK 是整个链路的**统一出口**。它做三件事: + +1. 通过 `dd-trace` 挂钩 `require('electron')`,包装 `BrowserWindow` 并向每个渲染进程注入 preload 脚本,暴露全局对象 `DatadogEventBridge` +2. 渲染进程的 `@flashcatcloud/browser-rum` 检测到该桥接对象后,把采集到的事件通过 IPC 发回主进程,而不是自己直连上报 +3. 主进程给两侧事件统一补充上下文——自己的事件补全套公共属性,渲染进程事件只覆盖 `session.id` / `application.id` 并补 `container`——然后落盘成批,上报到 `POST https:///api/v2/rum` + +```mermaid +graph TB + subgraph Electron 应用 + subgraph 渲染进程 + BR["@flashcatcloud/browser-rum"] + end + subgraph 主进程 + DDT["dd-trace"] + SDK["@flashcatcloud/electron-sdk"] + end + end + FC[("Flashduty RUM")] + BR -->|DatadogEventBridge / IPC| SDK + DDT -->|HTTP span| SDK + SDK -->|POST /api/v2/rum| FC +``` + + +包内部的模块名、插件名和桥接对象名保留了 `Datadog` / `dd-` 前缀(例如 `DatadogEventBridge`、`datadogVitePlugin`)。这是 fork 上游命名的约定,不影响数据归属——事件只会上报到你配置的 Flashduty `site`。 + + +## 前提条件 + +- Electron 39 及以上(SDK 的 `peerDependencies` 要求) +- 在 Flashduty 控制台的 [RUM 应用管理](https://console.flashcat.cloud/rum/apps)页面创建或选择一个 **Electron** 类型应用,获取 **Application ID** 和 **Client Token** +- 确认应用运行环境可以访问 `https://browser.flashcat.cloud/api/v2/rum`(私有化部署则为你自己的上报地址) + +## 安装 + +```bash +# 主进程 +npm install @flashcatcloud/electron-sdk + +# 渲染进程 +npm install @flashcatcloud/browser-rum +``` + +## 主进程接入 + +### 引入 instrument 入口 + +`@flashcatcloud/electron-sdk/instrument` 必须在**任何 `electron` 导入之前**执行。它负责初始化 `dd-trace`,而 `dd-trace` 需要在 `require('electron')` 发生前完成模块挂钩,否则 `BrowserWindow` 的 preload 注入不会生效,渲染进程也就拿不到桥接对象。 + +```ts main.ts +// 必须是文件的第一行导入 +import '@flashcatcloud/electron-sdk/instrument'; + +import { app, BrowserWindow } from 'electron'; +``` + + +不要让格式化工具或 `import` 排序规则把这一行移到后面。如果你的项目使用 ESLint 的 `import/order` 或 `simple-import-sort`,请为该行添加忽略注释。 + + +### 初始化 SDK + +在创建任何 `BrowserWindow` 之前调用 `init()`。它是异步的,返回 `true` 表示配置合法、初始化成功。 + +```ts main.ts +import '@flashcatcloud/electron-sdk/instrument'; + +import { app, BrowserWindow } from 'electron'; +import { init } from '@flashcatcloud/electron-sdk'; + +void app.whenReady().then(async () => { + await init({ + applicationId: '', + clientToken: '', + service: 'my-electron-app', + site: 'browser.flashcat.cloud', + env: 'production', + version: app.getVersion(), + }); + + createWindow(); +}); +``` + +#### 必填参数 + + +应用 ID,在应用管理页面获取 + + + +客户端 Token,在应用管理页面获取 + + + +服务名称,用于区分不同的服务。上传 sourcemap 时需要使用相同的值 + + + +`clientToken` 只用于客户端 RUM 上报,请不要在客户端代码中写入服务端密钥。 + + +#### 上报站点 + + +上报站点,直接作为 intake 域名使用。SaaS 用户无需配置;私有化部署填你自己的域名 + + + +SDK 拼接的上报地址固定为 `https:///api/v2/rum`——**协议头 `https://` 是写死的**。如果你的内网 intake 只有纯 HTTP,`site` 填什么都没用,必须改用 `proxy`,见[什么时候必须用 proxy](/zh/rum/sdk/electron/advanced-config#什么时候必须用-proxy)。 + + +完整可选参数见[高级配置](/zh/rum/sdk/electron/advanced-config)。 + +## 打包工具插件 + +`dd-trace` 依赖运行时的模块加载顺序。而打包工具(Vite、Webpack、esbuild)会重排、内联或提升 `require()`,让 `import '@flashcatcloud/electron-sdk/instrument'` 失去「最先执行」的位置。SDK 因此提供了三个打包插件,它们会: + +- 把 `dd-trace` 与 `@flashcatcloud/electron-sdk` 标记为 external,保留为运行时 `require` +- 在主进程入口 chunk 的最顶部注入 instrument 初始化代码(**因此使用插件后无需再手写那行 import**) +- 把 `dd-trace` 的 preload 脚本和被 external 的依赖复制进构建产物的 `node_modules`,保证打包后的应用(如 Electron Forge 的 asar)在运行时能解析到它们 + +请按你的构建方式**任选其一**。 + + + +适用于 electron-vite、Electron Forge + Vite。 + +```ts vite.config.ts +import { defineConfig } from 'vite'; +import { datadogVitePlugin } from '@flashcatcloud/electron-sdk/vite-plugin'; + +export default defineConfig({ + plugins: [datadogVitePlugin()], +}); +``` + + +请把插件加到**主进程**的构建配置上。electron-vite 的配置中对应 `main` 段,而不是 `renderer` 段。 + + + + +适用于 Electron Forge + Webpack。 + +```js webpack.main.config.js +const { DatadogWebpackPlugin } = require('@flashcatcloud/electron-sdk/webpack-plugin'); + +module.exports = { + plugins: [new DatadogWebpackPlugin()], +}; +``` + +插件同时会把 `dd-trace` 和 SDK 从 `@vercel/webpack-asset-relocator-loader` 中排除——该 loader 会破坏 `dd-trace` 内部的动态 `require.resolve`。 + + + +```ts build.ts +import * as esbuild from 'esbuild'; +import { datadogEsbuildPlugin } from '@flashcatcloud/electron-sdk/esbuild-plugin'; + +await esbuild.build({ + entryPoints: ['src/main.ts'], + bundle: true, + platform: 'node', + outfile: 'dist/main.js', + plugins: [datadogEsbuildPlugin()], +}); +``` + + + + +输出 ESM 格式时,静态 `import` 会先于模块代码求值,`dd-trace` 的钩子无法拦截 `import 'electron'`。三个插件都对 ESM 做了处理:改为直接调用 `session.defaultSession.registerPreloadScript()` 注册 preload,效果等价。你不需要额外配置。 + + +## 渲染进程接入 + +渲染进程加载的页面按 [Web SDK](/zh/rum/sdk/web/sdk-integration) 的 NPM 方式接入即可,无需改动初始化写法。 + +```ts renderer.ts +import { flashcatRum } from '@flashcatcloud/browser-rum'; + +flashcatRum.init({ + applicationId: '', + clientToken: '', + service: 'my-electron-app', + site: 'browser.flashcat.cloud', + env: 'production', + version: '1.0.0', + sessionSampleRate: 100, + trackResources: true, + trackLongTasks: true, + trackUserInteractions: true, +}); +``` + + +`applicationId`、`clientToken`、`service`、`env`、`version` 建议与主进程 `init()` 保持一致,否则两侧数据会被归到不同的应用或版本,无法在同一个会话里串起来。 + + +### 桥接无需配置 + +渲染进程侧**不需要任何额外接线**。主进程注入的 preload 在组装 host 白名单时,总是把窗口自身的 `location.hostname` 并进去: + +```js +const allowedHosts = [...new Set([location.hostname, ...configuredHosts])] +``` + +因此窗口自身的页面**永远命中白名单**,桥接开箱即用。`file://` 也不例外——此时 `location.hostname` 是空字符串,白名单为 `[""]`,仍然自匹配。 + + +`allowedWebViewHosts` 不是桥接开关,而是**额外 host 的白名单**。只有当你想接收 `` 或 `BrowserView` 里加载的**第三方页面**的事件时才需要配置它,例如 `allowedWebViewHosts: ['partner.example.com']`。匹配规则支持子域名后缀。 + + +### 桥接未生效时会怎样 + +桥接失效的原因不是配置,而是 **preload 没有被注入**——通常是主进程未接入 SDK,或主进程被打包但没挂对应的[打包工具插件](#打包工具插件),导致 `instrument` 入口失去了最先执行的位置。 + +| 行为 | 桥接生效 | 桥接未生效 | +|------|----------|------------| +| 渲染进程事件的 `container.source` | `electron` | 字段缺失 | +| 上报出口 | 主进程统一批量上报 | 渲染进程各自直连 intake | +| 会话 | 与主进程共享同一个 `session.id` | 渲染进程独立生成会话 | +| 离线补报 | 事件落盘到主进程 `userData`,重启后续传 | 依赖浏览器端缓冲,进程退出即丢失 | +| 用户活跃度 | 渲染进程的点击会续期主进程会话 | 互不影响 | + + +`container.source` 是判断桥接是否生效的可靠信号:渲染进程事件带上它,说明 preload 注入成功、事件确实经主进程上报。 + + +### 两个进程的 source 取值 + +桥接生效时,主进程只覆盖渲染进程事件的 `session.id` 和 `application.id`,并补上 `container`;渲染进程事件**保留自己的 `source: browser`**。两类事件的标识因此不同: + +| 事件来源 | `source` | `container.source` | `view.url` | +|----------|----------|--------------------|------------| +| 主进程 | `electron` | 无 | `electron://main-process` | +| 渲染进程窗口 | `browser` | `electron` | 页面 URL | + + +在查看器里筛选时,只用 `source:electron` **只能查到主进程事件**。要选中一个 Electron 应用产生的全部数据,请用 `source:electron OR container.source:electron`。 + + +## 验证接入 + + + +主进程日志中会输出 SDK 的初始化信息。`init()` 返回 `false` 时会打印具体的配置错误(如缺少必填项)。 + + + +- 主进程:发起一次 HTTP 请求、抛一个未捕获异常 +- 渲染进程:点击页面元素、切换路由、发起一次 `fetch` + + + +打开对应的 RUM 应用,在[查看器](/zh/rum/explorer/overview)中按 `source:electron OR container.source:electron` 过滤,确认出现 `view`、`action`、`resource`、`error` 事件。 + +默认上报频率为 10 秒一批,请稍等片刻再刷新。 + + + +在同一条会话下同时看到主进程事件(`view.url` 为 `electron://main-process`)和渲染进程事件(`action`、Web Vitals),说明桥接已生效。 + +若渲染进程事件**没有 `container.source` 字段**,说明 preload 未被注入:请检查主进程是否已调用 `init()`,以及主进程被打包时是否挂了[打包工具插件](#打包工具插件)。 + + + +## 下一步 + + + +配置上报批次、代理、手动错误上报与 sourcemap 上传。 + + + +了解支持的 Electron 版本、操作系统、打包工具与当前限制。 + + + +查看两个进程分别采集的事件类型、字段与上报行为。 + +