@groundcover/browser 2.0.0 → 2.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,45 @@ All notable changes to `@groundcover/browser` are documented here. This project
4
4
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and the
5
5
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format.
6
6
 
7
+ ## [2.1.0-rc.1] — 2026-10-07
8
+
9
+ Second release candidate of `2.1.0`. Tunes canvas capture's default from measurements; nothing
10
+ changes unless `options.replay.canvas.enabled` is set.
11
+
12
+ ### Changed
13
+
14
+ - **`replay.canvas.fps` defaults to 1** (was 4). rrweb re-encodes every canvas on every tick whether
15
+ or not it changed, so the rate drives CPU even on a static page, while bytes grow only when content
16
+ changes. On a six-chart test dashboard, 1 fps used about 12% of a CPU core against about 35% at
17
+ 4 fps, with the same bytes. Set `fps` explicitly for animated canvases.
18
+ - **README: canvas capture recipes** for dashboards, animated canvases, constrained bandwidth and
19
+ CPU-sensitive pages, with measured costs for scale.
20
+
21
+ ## [2.1.0-rc.0] — 2026-10-06
22
+
23
+ First release candidate of `2.1.0`, on top of `2.0.0`. Adds opt-in canvas capture to session replay;
24
+ nothing changes unless `options.replay.canvas.enabled` is set.
25
+
26
+ ### Added
27
+
28
+ - **Session replay can capture `<canvas>`-rendered content** (e.g. chart data series that replayed
29
+ as empty frames) via `options.replay.canvas`. Off by default. `enabled` maps to rrweb
30
+ `recordCanvas`, `fps` (default 4, clamped to 1–15 with a warning) to `sampling.canvas`, and
31
+ `imageType` / `imageQuality` (default `image/webp` at 0.6) to `dataURLOptions`. Canvas frames are
32
+ pixel snapshots: they increase replay bandwidth and page CPU, and are **not** covered by `privacy`
33
+ text/input masking. WebGL capture is best-effort only (the page's context must use
34
+ `preserveDrawingBuffer: true`).
35
+ - **Changing `replay.canvas` through `updateConfig` restarts the active recording**, like
36
+ `replay.blockedSelectors` and `privacy`. A partial update merges into the current canvas options,
37
+ so `{ canvas: { fps: 8 } }` keeps capture enabled.
38
+
39
+ ### Fixed
40
+
41
+ - **rrweb recorder errors stay inside the SDK.** Observer failures that fire after `record()`
42
+ returns are routed through the SDK's error handler instead of reaching the host page.
43
+ - **`updateConfig` restarts the recording only for keys the update itself sets.** An inherited
44
+ (non-own) `privacy` or `replay` key no longer splits the recording without changing the config.
45
+
7
46
  ## [2.0.0] — 2026-10-06
8
47
 
9
48
  Stable release of the `2.0.0-rc.0` and `2.0.0-rc.1` line. **Contains breaking changes from `1.x`** —
package/README.md CHANGED
@@ -179,17 +179,86 @@ content use [`privacy`](#privacy--data-masking).
179
179
  | Option | Type | Default | Description |
180
180
  |---|---|---|---|
181
181
  | `blockedSelectors` | `string[]` | — | CSS selectors whose elements (and subtrees) are excluded from recording. Useful for noisy extension-injected DOM (e.g. Grammarly). |
182
+ | `canvas` | `ReplayCanvasOptions` | off | **Opt-in** capture of `<canvas>`-rendered content (charts, WebGL widgets). Off by default — see [Canvas capture](#canvas-capture-opt-in) for the fields and tradeoffs. |
182
183
 
183
184
  > ⚠️ Don’t confuse `options.replay.blockedSelectors` (recording noise reduction) with
184
185
  > `options.privacy.replay.*` (rrweb masking callbacks). They live at different levels and serve
185
186
  > different purposes.
186
187
 
188
+ #### Canvas capture (opt-in)
189
+
190
+ By default rrweb only serializes the DOM/SVG around a `<canvas>`, so anything **drawn to a canvas**
191
+ — chart data series, WebGL scenes — is **not** recorded and replays as an empty frame. Set
192
+ `options.replay.canvas.enabled` to capture canvas pixels as periodic image snapshots.
193
+
194
+ ```ts
195
+ groundcover.init({
196
+ // …
197
+ options: {
198
+ enabledEvents: ["replay" /* … */],
199
+ replay: {
200
+ canvas: {
201
+ enabled: true, // default false
202
+ fps: 1, // default 1, clamped to [1, 15]
203
+ imageType: "image/webp", // default 'image/webp' ('image/jpeg' | 'image/png')
204
+ imageQuality: 0.6, // default 0.6 (lossy formats only)
205
+ },
206
+ },
207
+ },
208
+ });
209
+ ```
210
+
211
+ | Field | Type | Default | Maps to rrweb |
212
+ |---|---|---|---|
213
+ | `enabled` | `boolean` | `false` | `recordCanvas` |
214
+ | `fps` | `number` | `1` (clamped `[1, 15]`) | `sampling.canvas` |
215
+ | `imageType` | `'image/webp' \| 'image/jpeg' \| 'image/png'` | `'image/webp'` | `dataURLOptions.type` |
216
+ | `imageQuality` | `number` (0–1) | `0.6` | `dataURLOptions.quality` |
217
+
218
+ > ⚠️ **Canvas capture is expensive and off by default for good reason.** Enable it only when you
219
+ > knowingly accept the tradeoffs:
220
+ > - **Bandwidth** — every snapshot is a serialized image, so replay payloads grow substantially on
221
+ > canvas-heavy pages. Keep `fps` low and prefer lossy `image/webp`/`image/jpeg`.
222
+ > - **CPU** — every canvas on the page is re-encoded every `1/fps` seconds, changed or not, on the
223
+ > recorded (end-user) device. `fps` is the main CPU lever.
224
+ > - **Privacy** — canvas pixels **bypass `privacy` text/input masking entirely**. Whatever is drawn
225
+ > is captured verbatim; do not enable canvas capture on views that render sensitive data to a
226
+ > canvas.
227
+
228
+ > **WebGL:** capture is **best-effort and not officially supported** by this option. Snapshotting a
229
+ > WebGL/WebGL2 canvas only yields pixels if the page created its context with
230
+ > `preserveDrawingBuffer: true`; otherwise frames may come back blank. 2D canvases (the common chart
231
+ > case) are captured reliably.
232
+
233
+ ##### Recipes
234
+
235
+ Canvas capture's cost depends on the page: how many canvases, how big they are, and how often they
236
+ redraw. Start from the closest recipe and adjust:
237
+
238
+ | Page | Config | Why |
239
+ |---|---|---|
240
+ | Dashboards and charts that redraw every few seconds or on interaction | `{ enabled: true }` | The 1 fps default costs the least CPU, and charts that change slower than once a second send the same bytes at any rate. |
241
+ | Animated canvases (live charts, maps, games) | `{ enabled: true, fps: 4, imageQuality: 0.4 }` | Smoother playback. Every changed canvas sends a full image per frame, so bytes grow with `fps` × the number of animated canvases. |
242
+ | Constrained bandwidth | `{ enabled: true, imageQuality: 0.3 }` plus `replay.blockedSelectors` | Lower quality shrinks each image; blocked canvases are skipped entirely. |
243
+ | CPU-sensitive pages (low-end devices) | `{ enabled: true, imageType: "image/png" }` | PNG encodes much faster than webp but produces about twice the bytes. |
244
+
245
+ For scale, on a test dashboard with six canvas charts on a 2× display, canvas capture added about
246
+ 280 KB/min while the charts were static and about 1 MB/min while they refreshed every 10 s. The
247
+ recorder used about 12% of a CPU core at 1 fps and about 35% at 4 fps, with the same bytes. PNG at
248
+ 4 fps used about 10%.
249
+
250
+ Changing `replay.canvas` (or `replay.blockedSelectors`) via `updateConfig` restarts the active
251
+ recording so the new options take effect at rrweb `record()` time. A partial `canvas` update merges
252
+ into the current one, so `{ canvas: { fps: 8 } }` keeps capture enabled; pass `enabled: false` to
253
+ turn it off.
254
+
187
255
  ### Updating config at runtime
188
256
 
189
257
  `updateConfig` accepts these optional fields: `cluster`, `environment`, `namespace`, `releaseId`,
190
258
  `dsn`, `apiKey`, `options`, and `user`. `appId` cannot be updated. `sessionId` is not accepted;
191
- use the separate `setSessionId()` API. Nested groups merge one level deep (and `tracing.origin` /
192
- `privacy.replay` one level deeper), so a partial update preserves the other keys in the group:
259
+ use the separate `setSessionId()` API. Nested groups merge one level deep (and `tracing.origin`,
260
+ `privacy.replay` and `replay.canvas` one level deeper), so a partial update preserves the other keys in
261
+ the group:
193
262
 
194
263
  ```ts
195
264
  groundcover.updateConfig({
@@ -320,6 +389,7 @@ record-time masking from your [`privacy`](#privacy--data-masking) config:
320
389
  - `privacy.level` / `privacy.maskSelectors` → rrweb `maskAllInputs` + `maskTextSelector`
321
390
  - `privacy.replay.maskTextFn` / `maskInputFn` → rrweb `maskTextFn` / `maskInputFn`
322
391
  - `options.replay.blockedSelectors` → rrweb `blockSelector` (noise reduction)
392
+ - `options.replay.canvas` → rrweb `recordCanvas` + `sampling.canvas` + `dataURLOptions` (opt-in, off by default — see [Canvas capture](#canvas-capture-opt-in))
323
393
 
324
394
  Replay recording does **not** start automatically — even when `replay` is in `enabledEvents`. You
325
395
  must start it explicitly (e.g. after obtaining user consent):
package/dist/index.d.mts CHANGED
@@ -324,6 +324,45 @@ type EventTypes = {
324
324
  };
325
325
  /** The browser SDK's listener names for `enabledEvents`. */
326
326
  type BrowserEventName = "dom" | "network" | "exceptions" | "logs" | "pageload" | "navigation" | "performance" | "replay";
327
+ /**
328
+ * Opt-in canvas capture for session replay. Charts and other `<canvas>`-drawn
329
+ * content (2D or WebGL) are NOT recorded by default; rrweb only serializes the
330
+ * DOM/SVG around them, so canvas-rendered data series replay as empty frames.
331
+ * Enabling this makes rrweb snapshot canvas pixels.
332
+ *
333
+ * ⚠️ HIGH COST — off by default for good reason:
334
+ * - Bandwidth: canvas frames serialize as images, inflating replay payloads.
335
+ * - CPU: snapshotting canvases is costly on the recorded (customer) page.
336
+ * - Privacy: canvas pixels bypass text/input masking entirely — whatever is
337
+ * drawn is captured verbatim, regardless of the `privacy` config.
338
+ */
339
+ interface ReplayCanvasOptions {
340
+ /**
341
+ * Enable canvas capture. Defaults to `false` (canvas is not recorded). Maps
342
+ * to rrweb's `recordCanvas`.
343
+ */
344
+ enabled?: boolean;
345
+ /**
346
+ * Snapshot frame rate, in frames per second. Defaults to 1 and is clamped to
347
+ * [1, 15]; out-of-range values are clamped with a `console.warn`. Maps to
348
+ * rrweb's `sampling.canvas`. Every canvas is re-encoded on every tick, so a
349
+ * higher rate costs CPU even when nothing changes; it adds bytes only for
350
+ * canvases that change faster than the rate. Raise it for animated canvases.
351
+ */
352
+ fps?: number;
353
+ /**
354
+ * Image format for canvas snapshots. Defaults to `'image/webp'` (much smaller
355
+ * than rrweb's default lossless `image/png`). Maps to rrweb
356
+ * `dataURLOptions.type`.
357
+ */
358
+ imageType?: "image/webp" | "image/jpeg" | "image/png";
359
+ /**
360
+ * Lossy compression quality in [0, 1] for `image/webp` / `image/jpeg`.
361
+ * Defaults to 0.6 (ignored for `image/png`). Maps to rrweb
362
+ * `dataURLOptions.quality`.
363
+ */
364
+ imageQuality?: number;
365
+ }
327
366
  /**
328
367
  * Session-replay (rrweb) recording controls. This is NOT a privacy surface —
329
368
  * to mask sensitive content use `privacy`.
@@ -339,6 +378,12 @@ interface ReplayOptions {
339
378
  * sensitive content use `privacy` instead.
340
379
  */
341
380
  blockedSelectors?: string[];
381
+ /**
382
+ * Opt-in canvas capture (chart data series, WebGL-backed widgets). Off by
383
+ * default. Capturing canvas is bandwidth-, CPU-, and privacy-sensitive — see
384
+ * {@link ReplayCanvasOptions}.
385
+ */
386
+ canvas?: ReplayCanvasOptions;
342
387
  }
343
388
  /** Session-recording (rrweb replay) masking — the only place replay can be masked. */
344
389
  interface ReplayMaskingOptions {
@@ -369,8 +414,8 @@ interface SDKOptions extends Omit<SDKOptions$1, "privacy" | "enabledEvents" | "b
369
414
  */
370
415
  privacy?: PrivacyOptions;
371
416
  /**
372
- * Session-replay recording controls (noise reduction). For replay MASKING use
373
- * `privacy` instead. See {@link ReplayOptions}.
417
+ * Session-replay recording controls (noise reduction, opt-in canvas capture).
418
+ * For replay MASKING use `privacy` instead. See {@link ReplayOptions}.
374
419
  */
375
420
  replay?: ReplayOptions;
376
421
  }
package/dist/index.d.ts CHANGED
@@ -324,6 +324,45 @@ type EventTypes = {
324
324
  };
325
325
  /** The browser SDK's listener names for `enabledEvents`. */
326
326
  type BrowserEventName = "dom" | "network" | "exceptions" | "logs" | "pageload" | "navigation" | "performance" | "replay";
327
+ /**
328
+ * Opt-in canvas capture for session replay. Charts and other `<canvas>`-drawn
329
+ * content (2D or WebGL) are NOT recorded by default; rrweb only serializes the
330
+ * DOM/SVG around them, so canvas-rendered data series replay as empty frames.
331
+ * Enabling this makes rrweb snapshot canvas pixels.
332
+ *
333
+ * ⚠️ HIGH COST — off by default for good reason:
334
+ * - Bandwidth: canvas frames serialize as images, inflating replay payloads.
335
+ * - CPU: snapshotting canvases is costly on the recorded (customer) page.
336
+ * - Privacy: canvas pixels bypass text/input masking entirely — whatever is
337
+ * drawn is captured verbatim, regardless of the `privacy` config.
338
+ */
339
+ interface ReplayCanvasOptions {
340
+ /**
341
+ * Enable canvas capture. Defaults to `false` (canvas is not recorded). Maps
342
+ * to rrweb's `recordCanvas`.
343
+ */
344
+ enabled?: boolean;
345
+ /**
346
+ * Snapshot frame rate, in frames per second. Defaults to 1 and is clamped to
347
+ * [1, 15]; out-of-range values are clamped with a `console.warn`. Maps to
348
+ * rrweb's `sampling.canvas`. Every canvas is re-encoded on every tick, so a
349
+ * higher rate costs CPU even when nothing changes; it adds bytes only for
350
+ * canvases that change faster than the rate. Raise it for animated canvases.
351
+ */
352
+ fps?: number;
353
+ /**
354
+ * Image format for canvas snapshots. Defaults to `'image/webp'` (much smaller
355
+ * than rrweb's default lossless `image/png`). Maps to rrweb
356
+ * `dataURLOptions.type`.
357
+ */
358
+ imageType?: "image/webp" | "image/jpeg" | "image/png";
359
+ /**
360
+ * Lossy compression quality in [0, 1] for `image/webp` / `image/jpeg`.
361
+ * Defaults to 0.6 (ignored for `image/png`). Maps to rrweb
362
+ * `dataURLOptions.quality`.
363
+ */
364
+ imageQuality?: number;
365
+ }
327
366
  /**
328
367
  * Session-replay (rrweb) recording controls. This is NOT a privacy surface —
329
368
  * to mask sensitive content use `privacy`.
@@ -339,6 +378,12 @@ interface ReplayOptions {
339
378
  * sensitive content use `privacy` instead.
340
379
  */
341
380
  blockedSelectors?: string[];
381
+ /**
382
+ * Opt-in canvas capture (chart data series, WebGL-backed widgets). Off by
383
+ * default. Capturing canvas is bandwidth-, CPU-, and privacy-sensitive — see
384
+ * {@link ReplayCanvasOptions}.
385
+ */
386
+ canvas?: ReplayCanvasOptions;
342
387
  }
343
388
  /** Session-recording (rrweb replay) masking — the only place replay can be masked. */
344
389
  interface ReplayMaskingOptions {
@@ -369,8 +414,8 @@ interface SDKOptions extends Omit<SDKOptions$1, "privacy" | "enabledEvents" | "b
369
414
  */
370
415
  privacy?: PrivacyOptions;
371
416
  /**
372
- * Session-replay recording controls (noise reduction). For replay MASKING use
373
- * `privacy` instead. See {@link ReplayOptions}.
417
+ * Session-replay recording controls (noise reduction, opt-in canvas capture).
418
+ * For replay MASKING use `privacy` instead. See {@link ReplayOptions}.
374
419
  */
375
420
  replay?: ReplayOptions;
376
421
  }