Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
60c0e8d
✨ add session sampling and error-session capture (sessionOnError)
Fiona2016 Oct 4, 2026
99bc14b
✅ cover sampling and sessionOnError end to end
Fiona2016 Oct 4, 2026
e64803d
📝 document sessionSampleRate and sessionOnError
Fiona2016 Oct 4, 2026
7296ea5
👌 serialize session state writes and simplify the withheld buffer
Fiona2016 Oct 4, 2026
7b2e15b
✅ make the sessionOnError scenarios robust to a loaded machine
Fiona2016 Oct 4, 2026
5e572b2
fix: judge a crash's session as of the crash, and write an exit-time …
Fiona2016 Oct 5, 2026
e49e1c5
fix: write everything pending before an exit, and mark an error sessi…
Fiona2016 Oct 5, 2026
311aaa5
fix: release every held detail of an error session, with or without i…
Fiona2016 Oct 5, 2026
6432c6a
fix: keep a queued event reachable by the exit flush until its append…
Fiona2016 Oct 5, 2026
b3dbd1d
fix: keep the batch queue and the upload cycle alive after a failure,…
Fiona2016 Oct 5, 2026
c3bdcc7
test: prove the exit-time write on a host that ends the process from …
Fiona2016 Oct 5, 2026
7f71eae
fix: count event bytes without a window, and write the session mark w…
Fiona2016 Oct 5, 2026
51ad155
fix: replace state files atomically on exit, delete an ended session'…
Fiona2016 Oct 5, 2026
db7771b
fix: report a crash whose view is gone from the history, under a view…
Fiona2016 Oct 5, 2026
92d81dd
fix: take an error reported while quitting, or right before an exit, …
Fiona2016 Oct 8, 2026
5cb46e9
fix: replace state files whole in request order, and validate what is…
Fiona2016 Oct 8, 2026
374f243
fix: accept only RUM event types from a renderer, and read renderer f…
Fiona2016 Oct 8, 2026
8f6e032
docs: describe the exit points the flush covers, the retention limits…
Fiona2016 Oct 8, 2026
292a7e2
🐛 write every event as it arrives once the process is exiting
Fiona2016 Oct 10, 2026
eb44687
🐛 keep a crash dump until its report is on disk, and release a recove…
Fiona2016 Oct 10, 2026
f92f243
🐛 read a damaged session state as damaged, not as a legacy one
Fiona2016 Oct 10, 2026
f9fc148
🐛 drop a renderer event the intake would drop for a fractional number
Fiona2016 Oct 10, 2026
55e94bd
✅ cover an error reported from a later exit listener, and a renderer …
Fiona2016 Oct 10, 2026
71a870c
📝 state what a session that never errors uploads, and what the exit f…
Fiona2016 Oct 10, 2026
1109b4d
🎨 wrap the renderer-envelope assertion the way the formatter expects
Fiona2016 Oct 10, 2026
df9435f
🐛 drop renderer events that name a prototype property
Fiona2016 Oct 14, 2026
248418b
🐛 check renderer numbers only at the paths the intake decodes as inte…
Fiona2016 Oct 14, 2026
0aeadd9
🔒 keep renderer text out of bridge parse-failure telemetry
Fiona2016 Oct 14, 2026
da7a86e
🐛 report a failed batch append from the next flush, so a crash dump i…
Fiona2016 Oct 14, 2026
8209584
📝 state what a withheld session uploads
Fiona2016 Oct 14, 2026
d82527a
🐛 require a whole number at every path the intake decodes as an integer
Fiona2016 Oct 14, 2026
05ddefa
🔒 scan the whole bridge message for a prototype property name
Fiona2016 Oct 14, 2026
409b619
💡 state why a kept crash dump may report twice
Fiona2016 Oct 14, 2026
b449595
📝 name the FlashCat RUM ingest and route telemetry past the withheld …
Fiona2016 Oct 14, 2026
13ae34f
🐛 check the session plan on every event type but view
Fiona2016 Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 72 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ await esbuild.build({

## Available Features

- **Sessions** — Session-based event grouping
- **Sessions** — Session-based event grouping, with [sampling](#sampling) and error-session capture
- **RUM Views** — One view per main process instance
- **RUM Errors** — Capture Node errors and crashes in main process, with sourcemap-ready stacks
- **RUM Resources** — Capture RUM resources from main process network calls
Expand Down Expand Up @@ -373,22 +373,77 @@ before the logout still resolve to the user who was logged in then.

### Configuration Options

| Option | Type | Required | Default | Description |
| ----------------------------- | ---------------------------------------- | -------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `clientToken` | `string` | Yes | — | FlashCat client token |
| `applicationId` | `string` | Yes | — | RUM application ID |
| `site` | `string` | No | `browser.flashcat.cloud` | Intake host, used verbatim — e.g. `browser.flashcat.cloud` (production), `jira.flashcat.cloud` (staging), or your own host |
| `service` | `string` | Yes | — | Service name |
| `env` | `string` | No | — | Application environment |
| `version` | `string` | No | — | Application version |
| `telemetrySampleRate` | `number` | No | `20` | Telemetry sample rate (0–100) |
| `batchSize` | `'SMALL' \| 'MEDIUM' \| 'LARGE'` | No | — | Batch size for event uploads |
| `uploadFrequency` | `'RARE' \| 'NORMAL' \| 'FREQUENT'` | No | — | Upload frequency for event batches |
| `defaultPrivacyLevel` | `'mask' \| 'allow' \| 'mask-user-input'` | No | `'mask'` | Default privacy level for renderer session replay |
| `allowedWebViewHosts` | `string[]` | No | `[]` | Extra hostnames allowed for the renderer bridge (the window's own host is always allowed) |
| `proxy` | `string` | No | — | Proxy URL to upload through instead of `site`. See [Self-hosted deployments](#self-hosted-deployments) |
| `normalizeStackPaths` | `boolean` | No | `true` | Rewrite stack frame paths to `app:///<path relative to the app root>`. See [Error stacks and sourcemaps](#error-stacks-and-sourcemaps) |
| `correctPrewarmedViewTimings` | `boolean` | No | `true` | Rebase FCP/LCP of pre-warmed windows onto the moment they became visible. See [Pre-warmed windows](#pre-warmed-windows) |
| Option | Type | Required | Default | Description |
| ----------------------------- | ---------------------------------------- | -------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clientToken` | `string` | Yes | — | FlashCat client token |
| `applicationId` | `string` | Yes | — | RUM application ID |
| `site` | `string` | No | `browser.flashcat.cloud` | Intake host, used verbatim — e.g. `browser.flashcat.cloud` (production), `jira.flashcat.cloud` (staging), or your own host |
| `service` | `string` | Yes | — | Service name |
| `env` | `string` | No | — | Application environment |
| `version` | `string` | No | — | Application version |
| `sessionSampleRate` | `number` | No | `100` | Percentage of sessions collected (0–100). An out-of-range value fails `init`. See [Sampling](#sampling) |
| `sessionOnError` | `boolean` | No | `false` | Keeps the sessions `sessionSampleRate` did not draw in memory; a withheld session uploads no RUM events unless it reports an error, not even at quit. See [Sampling](#sampling) |
| `telemetrySampleRate` | `number` | No | `20` | Telemetry sample rate (0–100) |
| `batchSize` | `'SMALL' \| 'MEDIUM' \| 'LARGE'` | No | — | Batch size for event uploads |
| `uploadFrequency` | `'RARE' \| 'NORMAL' \| 'FREQUENT'` | No | — | Upload frequency for event batches |
| `defaultPrivacyLevel` | `'mask' \| 'allow' \| 'mask-user-input'` | No | `'mask'` | Default privacy level for renderer session replay |
| `allowedWebViewHosts` | `string[]` | No | `[]` | Extra hostnames allowed for the renderer bridge (the window's own host is always allowed) |
| `proxy` | `string` | No | — | Proxy URL to upload through instead of `site`. See [Self-hosted deployments](#self-hosted-deployments) |
| `normalizeStackPaths` | `boolean` | No | `true` | Rewrite stack frame paths to `app:///<path relative to the app root>`. See [Error stacks and sourcemaps](#error-stacks-and-sourcemaps) |
| `correctPrewarmedViewTimings` | `boolean` | No | `true` | Rebase FCP/LCP of pre-warmed windows onto the moment they became visible. See [Pre-warmed windows](#pre-warmed-windows) |

### Sampling

`sessionSampleRate` is drawn once per session, in the main process, and decides for the renderers
too: their events reach the intake through the main process, which drops those of a session it did
not draw. A session resumed after a restart keeps the decision it was created with, and reports the
rate it was drawn at. A renderer's own `sessionSampleRate` does not apply to the events it sends over
the bridge: the main process decides, and its rate is what every event reports.

`sessionOnError` keeps the sessions the rate did not draw on standby instead of dropping them:

- The session's events — main process and renderers alike — are held in memory: up to 60 seconds
of what preceded the error, subject to the limits (64 KiB of detail, 200 events, plus up to 50
views). The buffer itself never touches the disk; what is persisted locally is the session's
state and the main-process view identifiers, which a native crash needs to be attributed on the
next launch. A withheld session uploads no RUM events unless it reports an error — not even at
quit.
- At the session's first error, what is held is handed to the upload batch together with the error,
0–3 s later (spread per session, so that one outage does not make every client upload at once),
and the session then reports as it happens, like any drawn session. When the application may be
about to exit, a release already earned is written to disk before returning instead: on an
uncaught exception in the main process (provided the application's own `uncaughtException`
listener is registered after `await init(...)` has completed, so that the SDK's runs first), on
`before-quit` and `will-quit`, and on the process `exit` event, which `process.exit()` and
`app.exit()` still run. Queued events are written synchronously at each of these points, and
once the exit event has started every later event is written as it arrives; an asynchronous
append already in progress is not completed by the drain. A listener registered _before_
`init()` that ends the process synchronously pre-empts the SDK's: nothing of that error is seen.
- A session that ends without an error is thrown away whole, late events included.

Only errors the application reports count: an error a renderer's `beforeSend` dropped, or one the
SDK reports about itself, does not. A native crash counts too: it is reported on the next launch,
when the session's held events are long gone, so the crash comes with the view it happened in and
nothing before it.

Such a session reports `session.sampled_for_error: true` on its views and a
`_dd.configuration.session_sample_rate` of `0` on every RUM event, so that it is counted as itself
rather than extrapolated by the rate.

```ts
await init({
// ...
sessionSampleRate: 0, // no session is collected unconditionally…
sessionOnError: true, // …but every session that reports an error is
});
```

> **Session Replay is not withheld.** The renderer uploads its replay itself, past the main process.
> For a session the rate did not draw, renderers are told there is no session, so they record
> nothing. A session kept by `sessionOnError` cannot be hidden from them that way — its renderer
> events must reach the main process to be held — so with `sessionReplayDirectUpload` on, its replay
> is uploaded whether or not the session ever reports an error. Do not combine the two until the
> Browser SDK can withhold replay for a bridged session.

### Pre-warmed windows

Expand Down
38 changes: 32 additions & 6 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ graph TB
end
end

DD[(Datadog)]
DD[(FlashCat RUM ingest)]

%% Browser SDK → Electron SDK via bridge
BP -->|"RUM events<br/>(IPC bridge)"| SDK
Expand All @@ -28,7 +28,7 @@ graph TB
%% Electron SDK internal sources
SDK -->|"span → resource conversion<br/>RUM data collection<br/>RUM APIs<br/>session + main process view"| SDK

%% Electron SDK → Datadog
%% Electron SDK → FlashCat RUM ingest
SDK -->|"enriched events<br/>(HTTP)"| DD

%% Styling
Expand All @@ -45,7 +45,7 @@ graph TB

### Main Process

The Electron SDK captures RUM sessions, collects RUM events, and forwards them to Datadog, enriching events from dd-trace and the Browser SDK with RUM and Electron context along the way. dd-trace instruments network requests, command executions, and IPC calls, then forwards spans to the Electron SDK through the `diagnostics_channel`.
The Electron SDK captures RUM sessions, collects RUM events, and forwards them to the FlashCat RUM ingest, enriching events from dd-trace and the Browser SDK with RUM and Electron context along the way. dd-trace instruments network requests, command executions, and IPC calls, then forwards spans to the Electron SDK through the `diagnostics_channel`.

More details in the [How tracing works](#how-tracing-works) section.

Expand Down Expand Up @@ -75,6 +75,7 @@ flowchart LR
end

subgraph Transport
WB[WithheldEventBuffer]
BM[BatchManager]
BP[BatchProducer]
BC[BatchConsumer]
Expand All @@ -83,17 +84,42 @@ flowchart LR
RUM -- RawRumEvent --> COMBINE
TEL -- RawTelemetryEvent --> COMBINE
CC -. "application.id, service, ..." .-> HOOKS
SC -. "session.id" .-> HOOKS
SC -. "session.id, sampling" .-> HOOKS
VC -. "view.id, view.name, ..." .-> HOOKS
HOOKS --> COMBINE
COMBINE -- ServerEvent --> BM
COMBINE -- "ServerEvent (RUM)" --> WB
COMBINE -- "ServerEvent (telemetry)" --> BM
WB -- "released / not withheld" --> BM
BM --> BP
BM --> BC
BP -. "write" .-> DISK[Disk]
BC -. "read" .-> DISK[Disk]
BC -. "send" .-> INT[HTTP intake]
```

### Sampling and withheld sessions

`SessionManager` draws each session once (`sessionSampleRate`, then `sessionOnError` for what the
rate missed) and persists the result, with the rate it was drawn at, with the session — in `_dd_s`
and in the session history — so a resumed session and a crash reported on the next launch are
judged, and reported, by the draw they were made under.
`SessionContext`'s RUM hook turns the draw into event attributes: it discards the events of a session
not drawn, and of a withheld session that ended without an error (its late events), and stamps
`session.sampled_for_error` and `_dd.configuration.session_sample_rate`.

`WithheldEventBuffer` sits between assembly and the RUM batch, so it sees main-process and renderer
events alike, already final. It holds a withheld session's events and releases them at the session's
first error; telemetry bypasses it. `CrashCollection` releases the session of a crash reported on the
next launch itself, rebuilding the crashed view from the view history. When the application may be
about to exit (`APP_MAY_EXIT`: an uncaught exception in the main process, `before-quit`, `will-quit`,
and the process `exit` event), the buffer releases what has earned its release and `Transport` then
writes every queued event, and the session state, before returning, since the batch otherwise writes
on later turns of the event loop; an append already issued is not completed synchronously. Each pass
writes only what arrived since the one before, so an error reported between two of them is taken
along by the next; and once the process `exit` event has started — the last pass, after which
nothing runs — every event is released and written as it arrives. The session state and history files are replaced whole, through a temp file and a
rename, in the order the writes were requested (`StateFile`). See the JSDoc of each class.

### Event Manager

The `EventManager` provides a handler-based pipeline for processing events.
Expand Down Expand Up @@ -254,7 +280,7 @@ Two pieces close it, and the order between them is what makes it work:

`init-order.scenario.ts` pins both down — a window opened before the SDK is ready, and an application that never initializes at all.

**`''` is load-bearing, and is not the same as not implementing the getter.** The Browser SDK reads an empty answer as "the host has no session right now" and stops attributing data until the host answers with an id again; it only falls back to its own placeholder session id for a host too old to implement `getSessionId()` at all. That distinction is what keeps Session Replay off a fake session: the renderer uploads its segments itself instead of handing them to the main process, so nothing here can discard them after the fact, and a placeholder id is a constant every application built on this SDK would share. It also means the main process must never answer with the id an expired session used to have — that would attach segments to a session that has ended. `getActiveSessionId` in `src/index.ts` answers `''` for anything but an active session, and `bridge-window.scenario.ts` pins both the expiry and the renewal down.
**`''` is load-bearing, and is not the same as not implementing the getter.** The Browser SDK reads an empty answer as "the host has no session right now" and stops attributing data until the host answers with an id again; it only falls back to its own placeholder session id for a host too old to implement `getSessionId()` at all. That distinction is what keeps Session Replay off a fake session: the renderer uploads its segments itself instead of handing them to the main process, so nothing here can discard them after the fact, and a placeholder id is a constant every application built on this SDK would share. It also means the main process must never answer with the id an expired session used to have — that would attach segments to a session that has ended. `getActiveSessionId` in `src/index.ts` answers `''` for anything but an active session, and `bridge-window.scenario.ts` pins both the expiry and the renewal down. It answers `''` for a session the sampling draw did not keep, too, which is what keeps the renderer from recording it; a session withheld by `sessionOnError` is answered with its id, since its renderer events have to reach the main process — so the renderer's replay of such a session is not withheld (see README, Sampling).

See `src/preload/`, `src/bridge/BridgeHandler.ts`, `src/domain/AnonymousId.ts`, `src/assembly/commonContext.ts`, `src/domain/tracing/`, `src/entries/instrument.ts`, `src/entries/vite-plugin.ts`, `src/entries/webpack-plugin.ts`, and `src/entries/esbuild-plugin.ts`.

Expand Down
Loading
Loading