@groundcover/browser 0.1.0-rc.7 → 1.0.0

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/README.md CHANGED
@@ -1,269 +1,466 @@
1
- # groundcover RUM Browser Monitoring
1
+ <p align="center">
2
+ <img src="./assets/logo.png" alt="groundcover" width="240" />
3
+ </p>
2
4
 
3
- groundcover’s Real User Monitoring (RUM) SDK allows you to capture front-end performance data, user events, and errors from your web applications.
5
+ <h1 align="center">RUM — Browser SDK</h1>
4
6
 
5
- See the dedicated [groundcover documentation](https://docs.groundcover.com/capabilities/real-user-monitoring-rum/) for more details.
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/@groundcover/browser"><img src="https://img.shields.io/npm/v/@groundcover/browser.svg" alt="npm version" /></a>
9
+ <a href="https://bundlejs.com/?q=@groundcover/browser"><img src="https://img.shields.io/bundlejs/size/@groundcover/browser" alt="minzipped size" /></a>
10
+ <a href="https://github.com/groundcover-com/rum/actions/workflows/ci.yml"><img src="https://github.com/groundcover-com/rum/actions/workflows/ci.yml/badge.svg" alt="CI status" /></a>
11
+ </p>
6
12
 
7
- ## Installation
13
+ groundcover’s Real User Monitoring (RUM) SDK captures front-end **performance**, **user
14
+ interactions**, **errors**, **logs**, **distributed traces**, and **session replay** from your web
15
+ application — with privacy masking **on by default**.
16
+
17
+ > Session replay is powered by [**rrweb**](https://github.com/rrweb-io/rrweb) under the hood.
18
+
19
+ See the [groundcover RUM documentation](https://docs.groundcover.com/capabilities/real-user-monitoring-rum/)
20
+ for the full product guide.
21
+
22
+ ---
23
+
24
+ ## Table of contents
25
+
26
+ - [Quick start](#quick-start)
27
+ - [Configuration reference](#configuration-reference)
28
+ - [Privacy & data masking](#privacy--data-masking)
29
+ - [Session replay](#session-replay)
30
+ - [Architecture](#architecture)
31
+ - [Web Worker offloading](#web-worker-offloading)
32
+ - [Session lifecycle](#session-lifecycle)
33
+ - [Performance considerations](#performance-considerations)
34
+ - [API reference](#api-reference)
35
+ - [Migrating to 1.0.0](#migrating-to-100)
36
+
37
+ ---
38
+
39
+ ## Quick start
8
40
 
9
41
  ```bash
10
42
  npm install @groundcover/browser
11
43
  ```
12
44
 
13
- ## Initialize the SDK
14
-
15
45
  ```ts
16
46
  import groundcover from "@groundcover/browser";
17
47
 
18
48
  groundcover.init({
19
49
  apiKey: "your-api-key",
20
- cluster: "your-cluster",
21
- environment: "production",
22
50
  dsn: "your-dsn",
51
+ cluster: "your-cluster",
23
52
  appId: "your-app-id",
53
+ environment: "production",
24
54
  });
25
55
  ```
26
56
 
27
- ### Micro-frontend Session Synchronization
28
-
29
- For micro-frontend architectures, you can synchronize sessions across different frontend applications by passing a shared sessionId:
57
+ That single call installs every instrumentation (page loads, DOM interactions, network requests,
58
+ errors, console logs, navigation, performance, and session replay) and starts sending data. From
59
+ here you can enrich it:
30
60
 
31
61
  ```ts
32
- // Main application generates a sessionId
33
- const sharedSessionId = "session-12345";
62
+ // Tie events to a user
63
+ groundcover.identifyUser({ id: "u_123", email: "john@acme.com", organization: "acme" });
34
64
 
35
- // Initialize SDK in main app
36
- groundcover.init({
37
- apiKey: "your-api-key",
38
- cluster: "your-cluster",
39
- environment: "production",
40
- dsn: "your-dsn",
41
- appId: "your-app-id",
42
- sessionId: sharedSessionId, // Use shared sessionId
43
- });
65
+ // Capture a handled error
66
+ groundcover.captureException(new Error("Checkout failed"), { feature: "checkout" });
44
67
 
45
- // Initialize SDK in micro-frontend with same sessionId
46
- groundcover.init({
47
- apiKey: "your-api-key",
48
- cluster: "your-cluster",
49
- environment: "production",
50
- dsn: "your-dsn",
51
- appId: "micro-frontend-app-id",
52
- sessionId: sharedSessionId, // Same sessionId for session continuity
53
- });
68
+ // Emit a structured log
69
+ groundcover.logger.warn("Payment retry", { provider: "stripe", attempt: 2 });
70
+
71
+ // Emit a custom business event
72
+ groundcover.sendCustomEvent({ event: "plan_upgraded", attributes: { plan: "pro" } });
54
73
  ```
55
74
 
56
- ## Identify users
75
+ ---
57
76
 
58
- Link RUM data to specific users:
77
+ ## Configuration reference
78
+
79
+ `init()` takes connection/identity fields at the top level and all behavioral knobs under
80
+ `options`, grouped by concern.
59
81
 
60
82
  ```ts
61
- groundcover.identifyUser({
62
- id: "John Doe",
63
- email: "john@groundcover.com",
64
- organization: "groundcover",
83
+ groundcover.init({
84
+ // ── Connection & identity ──
85
+ apiKey, dsn, cluster, appId,
86
+ environment, namespace, releaseId,
87
+ user, sessionId,
88
+
89
+ options: {
90
+ debug,
91
+ sessionSampleRate, eventSampleRate,
92
+ enabledEvents, excludedUrls,
93
+ sessionMaxDuration,
94
+ beforeSend, enrichEvent,
95
+ privacy: { /* … */ },
96
+ tracing: { /* … */ },
97
+ transport: { /* … */ },
98
+ replay: { /* … */ },
99
+ },
65
100
  });
66
101
  ```
67
102
 
68
- ## Send custom events
103
+ ### Top-level (connection & identity)
104
+
105
+ | Field | Type | Required | Description |
106
+ |---|---|---|---|
107
+ | `apiKey` | `string` | ✅ | Your groundcover RUM API key. |
108
+ | `dsn` | `string` | ✅ | RUM intake endpoint. A bare host is upgraded to `https://`. |
109
+ | `cluster` | `string` | ✅ | Target groundcover cluster. |
110
+ | `appId` | `string` | ✅ | Application identifier; reported as `service.name`. |
111
+ | `environment` | `string` | — | Deployment environment (e.g. `production`, `staging`). |
112
+ | `namespace` | `string` | — | Logical grouping/namespace for the app. |
113
+ | `releaseId` | `string` | — | Release/version identifier (e.g. a semver or commit hash). |
114
+ | `user` | `Partial<UserIdentifiers>` | — | Identify the user at init (same shape as [`identifyUser`](#identifyuser)). |
115
+ | `sessionId` | `string` | — | Use a shared session id — see [micro-frontends](#micro-frontend-session-synchronization). |
116
+ | `options` | `Partial<SDKOptions>` | — | Behavioral configuration, below. |
117
+
118
+ ### `options` — top-level knobs
119
+
120
+ | Option | Type | Default | Description |
121
+ |---|---|---|---|
122
+ | `debug` | `boolean` | `false` | Verbose SDK console logging. |
123
+ | `sessionSampleRate` | `number` | `1` | Fraction of sessions captured (0–1). |
124
+ | `eventSampleRate` | `number` | `1` | Fraction of events captured (0–1). |
125
+ | `enabledEvents` | `Array<…>` | `[]` | Instrumentations to enable. **Empty = all.** Members: `dom`, `network`, `exceptions`, `logs`, `pageload`, `navigation`, `performance`, `replay`. |
126
+ | `excludedUrls` | `Array<string \| RegExp>` | — | Network requests matching these are not captured. |
127
+ | `sessionMaxDuration` | `number` | `4h` | Target max session length in ms (1 min – 8 h). See [session lifecycle](#session-lifecycle). |
128
+ | `beforeSend` | `(event) => boolean` | — | Drop gate — return `false` to discard an event. Runs after redaction/enrichment; see [event lifecycle](#architecture). |
129
+ | `enrichEvent` | `(event) => event` | — | Mutate/replace an event before batching. Runs right after `beforeSend`; see [event lifecycle](#architecture). |
130
+
131
+ ### `options.privacy`
132
+
133
+ Drives both replay and non-replay masking. **On by default.** See
134
+ [Privacy & data masking](#privacy--data-masking) for the full table.
135
+
136
+ ### `options.tracing`
69
137
 
70
- Send custom events to groundcover:
138
+ Distributed-tracing header propagation for outgoing requests.
139
+
140
+ | Option | Type | Default | Description |
141
+ |---|---|---|---|
142
+ | `propagationUrls` | `string[]` | `[]` | Request URLs (prefix/`*`-glob match) that receive injected tracing headers. |
143
+ | `propagationHeaders` | `string[]` | `[]` | Header names to read/propagate onto traced requests. |
144
+ | `traceIdHeaderName` | `string` | `""` | Header carrying the trace id. |
145
+ | `spanIdHeaderName` | `string` | `""` | Header carrying the span id. |
146
+ | `origin` | `{ name: string; value: string }` | `{ name: "", value: "" }` | Origin tag stamped on injected trace headers. |
71
147
 
72
148
  ```ts
73
- groundcover.track("custom-event");
149
+ options: {
150
+ tracing: {
151
+ propagationUrls: ["https://api.acme.com/*"],
152
+ traceIdHeaderName: "x-groundcover-trace-id",
153
+ spanIdHeaderName: "x-groundcover-span-id",
154
+ },
155
+ }
74
156
  ```
75
157
 
76
- ## Capture structured logs
158
+ ### `options.transport`
77
159
 
78
- Use the `logger` API to send log messages with structured attributes that become queryable fields on the backend. The API mirrors Sentry's [`Sentry.logger`](https://docs.sentry.io/platforms/javascript/logs/) pattern — one method per level, second arg is the attributes object:
160
+ Batching and delivery of outgoing events.
79
161
 
80
- ```ts
81
- groundcover.logger.warn("Checkout failed", {
82
- orderId: "ord_42",
83
- region: "us-east",
84
- cart: { items: 3, total: 99.99 },
85
- });
162
+ | Option | Type | Default | Description |
163
+ |---|---|---|---|
164
+ | `batchSize` | `number` | `10` | Max events buffered before a batch flushes. |
165
+ | `batchTimeout` | `number` | `10000` | Max ms a batch waits before flushing regardless of size. |
166
+ | `compression` | `boolean` | `true` | Gzip-compress batches (offloaded to the Web Worker when available). |
86
167
 
87
- groundcover.logger.info("User signed in", { userId: "u_123" });
88
- groundcover.logger.error("Payment gateway timeout", { provider: "stripe" });
89
- ```
168
+ ### `options.replay`
169
+
170
+ Session-replay recording controls. **This is noise reduction, not privacy** — to mask sensitive
171
+ content use [`privacy`](#privacy--data-masking).
172
+
173
+ | Option | Type | Default | Description |
174
+ |---|---|---|---|
175
+ | `blockedSelectors` | `string[]` | — | CSS selectors whose elements (and subtrees) are excluded from recording. Useful for noisy extension-injected DOM (e.g. Grammarly). |
90
176
 
91
- Available levels: `log`, `info`, `warn`, `error`, `debug`, `trace`. Nested attribute objects are automatically flattened to dotted keys (e.g. `cart.items`, `cart.total`).
177
+ > ⚠️ Don’t confuse `options.replay.blockedSelectors` (recording noise reduction) with
178
+ > `options.privacy.replay.*` (rrweb masking callbacks). They live at different levels and serve
179
+ > different purposes.
92
180
 
93
- ### Structured `console.*` capture
181
+ ### Updating config at runtime
94
182
 
95
- The SDK auto-captures `console.log` / `info` / `warn` / `error` / `debug` / `trace` calls. When any argument is a plain object, its keys are promoted to structured log attributes alongside the stringified message — no code changes required:
183
+ `updateConfig` mirrors the `init` shape; nested groups merge one level deep (and `tracing.origin` /
184
+ `privacy.replay` one level deeper), so a partial update preserves the other keys in the group:
96
185
 
97
186
  ```ts
98
- console.log("User action emitted", {
99
- appId: "web",
100
- environment: "prod",
101
- userAction: { type: "click", target: "checkout-button" },
187
+ groundcover.updateConfig({
188
+ options: {
189
+ transport: { batchSize: 20 }, // batchTimeout/compression are preserved
190
+ privacy: { level: "mask-all" },
191
+ },
102
192
  });
103
- // → log event attributes include: appId, environment, userAction.type, userAction.target
104
193
  ```
105
194
 
106
- Reserved keys (`message`, `level`, `location`) cannot be overridden by user-supplied attributes — `message` and `level` are stamped by the SDK, and `location` is always set from the current browser URL/path/title.
195
+ For `user`, an omitted key leaves the current identity untouched, passing an object merges into it,
196
+ and passing `null` clears it (e.g. on logout).
107
197
 
108
- ## Capture errors
198
+ ---
109
199
 
110
- Capture errors from your web application:
200
+ ## Privacy & data masking
201
+
202
+ Masking is **on by default** (`privacy.level: 'mask-sensitive'`). A single `level` is the master
203
+ switch; finer toggles and hooks refine it.
204
+
205
+ | `level` | Replay inputs | Replay text | DOM events | Network / logs / errors |
206
+ |---|---|---|---|---|
207
+ | `mask-sensitive` **(default)** | sensitive masked | `[data-private]` + `maskSelectors` masked | sensitive masked | redacted |
208
+ | `mask-all` | masked | masked (`*`) | all masked | redacted |
209
+ | `allow` | — | — | — | off (auth headers are still stripped) |
210
+
211
+ Under `mask-sensitive`, an input/element is masked when it is a `type="password"`, sits under a
212
+ `[data-private]` ancestor or a `maskSelectors` match, or has an `id`/`name`/`class`/`aria-label`/
213
+ `placeholder` matching a built-in sensitive-key pattern or your `sensitiveKeys`. Non-sensitive inputs
214
+ and static page text stay visible; use `[data-private]` / `maskSelectors` to mask static content.
215
+
216
+ | Option | Type | Default | Description |
217
+ |---|---|---|---|
218
+ | `level` | `'mask-sensitive' \| 'mask-all' \| 'allow'` | `'mask-sensitive'` | Master masking switch. |
219
+ | `maskSelectors` | `string[]` | — | CSS selectors whose text/inputs are always masked (replay + DOM), unless `level: 'allow'`. |
220
+ | `maskNetworkBodies` | `boolean` | `true` | Redact network request/response bodies. |
221
+ | `maskNetworkQueryParams` | `boolean` | `true` | Redact URL query params. |
222
+ | `maskLogs` | `boolean` | `true` | Redact console/log messages and attributes. |
223
+ | `maskErrors` | `boolean` | `true` | Redact error messages, metadata and stack-frame URLs. |
224
+ | `sensitiveKeys` | `string[]` | — | Extra case-insensitive key substrings treated as sensitive, merged with built-ins. |
225
+ | `redact` | `(field) => value \| undefined` | — | Per-leaf custom redactor for non-replay events. Return `undefined` to defer to the SDK default. |
226
+ | `replay.maskTextFn` | `(text, el) => string` | — | rrweb `maskTextFn` pass-through (replay only). |
227
+ | `replay.maskInputFn` | `(text, el) => string` | — | rrweb `maskInputFn` pass-through (replay only). |
111
228
 
112
229
  ```ts
113
- groundcover.captureException(new Error("Something went wrong"));
230
+ options: {
231
+ privacy: {
232
+ level: "mask-sensitive",
233
+ maskSelectors: [".pii", "#ssn"],
234
+ sensitiveKeys: ["account_no"],
235
+ },
236
+ }
114
237
  ```
115
238
 
116
- You can also attach metadata to provide additional context:
239
+ #### Built-in sensitive patterns
240
+
241
+ These are always treated as sensitive (case-insensitive); your `sensitiveKeys` are merged in on top.
242
+
243
+ - **Key substrings** — matched as a substring of a body/query key or a DOM element attribute
244
+ (`id`/`name`/`class`/`aria-label`/`placeholder`):
245
+ `token`, `secret`, `passwd`, `password`, `api_key`, `access_key`, `write_key`, `auth`, `bearer`,
246
+ `credential`, `cvv`, `ssn`, `credit_card`, `card_number` (the `_` in the last five is optional —
247
+ `apikey` / `api-key` also match).
248
+ - **Request/response headers** — always stripped regardless of `level`: `authorization`, `cookie`,
249
+ `set-cookie`, plus any header name containing `token`, `key`, `secret`, `passwd`, `password`,
250
+ `auth`, `bearer`, or `credential`.
251
+ - **Query / form param names** — matched as a whole key (not inside JSON bodies), for OAuth-style
252
+ callbacks: `code`, `state`, `session_state`, `id_token`, `access_token`, `refresh_token`, `token`.
253
+
254
+ To opt out entirely: `privacy: { level: "allow" }`.
255
+
256
+ ---
257
+
258
+ ## Session replay
259
+
260
+ Replay is recorded with [**rrweb**](https://github.com/rrweb-io/rrweb). The SDK wires rrweb’s
261
+ record-time masking from your [`privacy`](#privacy--data-masking) config:
262
+
263
+ - `privacy.level` / `privacy.maskSelectors` → rrweb `maskAllInputs` + `maskTextSelector`
264
+ - `privacy.replay.maskTextFn` / `maskInputFn` → rrweb `maskTextFn` / `maskInputFn`
265
+ - `options.replay.blockedSelectors` → rrweb `blockSelector` (noise reduction)
266
+
267
+ Recording starts automatically when `replay` is enabled (it is, by default — empty `enabledEvents`).
268
+ You can also control it manually:
117
269
 
118
270
  ```ts
119
- groundcover.captureException(new Error("Payment failed"), {
120
- userId: "123",
121
- feature: "checkout",
122
- cart: { items: 3, total: 99.99 },
123
- });
271
+ groundcover.startReplayRecording();
272
+ groundcover.stopReplayRecording();
124
273
  ```
125
274
 
126
- ## Optional Configuration
127
-
128
- You can customize SDK behavior (event sampling, data masking, enabled events). The following properties are customizable:
129
-
130
- ```typescript
131
- export interface SDKOptions {
132
- batchSize: number;
133
- batchTimeout: number;
134
- eventSampleRate: number;
135
- sessionSampleRate: number;
136
- environment: string;
137
- debug: boolean;
138
- tracePropagationUrls: string[];
139
- sessionMaxDuration?: number; // optional; default 4h, range 1 minute – 8 hours (ms)
140
- privacy?: PrivacyOptions; // data masking; ON by default — see "Privacy & data masking"
141
- }
275
+ > rrweb mask options are fixed at `record()` time. Changing privacy config at runtime via
276
+ > `updateConfig` automatically **restarts** the active recording so the new masking applies.
277
+
278
+ ---
279
+
280
+ ## Architecture
281
+
282
+ Every captured event flows through one pipeline from instrumentation to intake. The two public
283
+ hooks — **`beforeSend`** (drop gate) and **`enrichEvent`** (transform) — run in the events pool,
284
+ *after* privacy redaction and internal enrichment, so they always see the final, already-redacted
285
+ event.
286
+
287
+ ```mermaid
288
+ flowchart TD
289
+ L["Listeners (events/listeners)<br/>dom · network · errors · logs<br/>navigation · pageload · performance · replay"]
290
+ R["Privacy redaction<br/>(getPrivacy) — bodies, headers,<br/>query params, logs, errors"]
291
+ EI["Internal enrichment<br/>id · span/trace ids · timestamps · location"]
292
+ BS{"beforeSend(event)<br/>returns false?"}
293
+ DROP["✗ dropped"]
294
+ EN["enrichEvent(event)<br/>mutate / replace"]
295
+ P["events-pool<br/>batch by transport.batchSize / transport.batchTimeout"]
296
+ T["transporter<br/>gzip (transport.compression)"]
297
+ I[("groundcover intake")]
298
+
299
+ L --> R --> EI --> BS
300
+ BS -- "false" --> DROP
301
+ BS -- "keep" --> EN --> P --> T --> I
142
302
  ```
143
303
 
144
- `sessionMaxDuration` sets a **target** maximum wall-clock session length (default 4 hours, must be between 1 minute and 8 hours).
304
+ **Event lifecycle (in order):**
145
305
 
146
- It is enforced **lazily, on activity — not by a background timer**, so it is **not a hard upper bound**. Once the cap has elapsed, the *next* user/business event (click, navigation, log, custom, network, exception, …) flushes pending events under the current session id, generates a fresh id, and resumes replay recording if it had been active.
306
+ 1. **Capture** — a listener builds the raw event.
307
+ 2. **Redact** — sensitive payload is masked per the resolved [`privacy`](#privacy--data-masking) config.
308
+ 3. **Enrich (internal)** — the SDK stamps id / span & trace ids / timestamps / `location` (location query params are redacted here too).
309
+ 4. **`beforeSend(event)`** — your hook; return `false` to **drop** the event. Receives the fully-redacted, enriched event.
310
+ 5. **`enrichEvent(event)`** — your hook; mutate or replace the event before it’s buffered.
311
+ 6. **Batch** — buffered in the events pool, flushed at `transport.batchSize` or `transport.batchTimeout`.
312
+ 7. **Send** — gzipped (`transport.compression`) and delivered to intake.
147
313
 
148
- Because rotation is activity-gated, a session that goes idle keeps its id past the cap until the next qualifying event:
314
+ Supporting pieces:
149
315
 
150
- - A dormant or backgrounded tab that produces no further events stays on the same session id — by design, so the SDK doesn't mint "phantom" sessions for tabs nobody is using.
151
- - **Session-replay batches don't count as activity** (rrweb emits a snapshot heartbeat every ~30s regardless of interaction), so a tab that's only recording replay won't rotate on that alone. Such a tab is ultimately bounded by the 30-minute replay-inactivity stop, not by this cap.
316
+ - **`instrumentation-manager`** installs listeners per `enabledEvents`.
317
+ - **`config-manager`** holds the resolved `SDKConfig` and a memoized `getPrivacy()` read by every
318
+ listener; runtime `updateConfig` invalidates the privacy memo.
319
+ - **`session-manager`** owns the session lifecycle — see [below](#session-lifecycle).
320
+ - **`worker`** offloads replay packing + gzip off the main thread.
152
321
 
153
- The flush is best-effort and the rotation always proceeds regardless of delivery success. Values outside the allowed range or otherwise invalid fall back to the default with a `console.warn`.
322
+ > Note: `beforeSend`/`enrichEvent` do **not** see explicitly-sent `sendCustomEvent` payloads pre-redacted —
323
+ > those are deliberately-provided and are not auto-redacted; scrub them yourself or via `enrichEvent`.
154
324
 
155
- ## Privacy & data masking
325
+ ---
156
326
 
157
- **Masking is ON by default.** With no `privacy` config the SDK runs at
158
- `privacy.level: 'mask-sensitive'`, which drives masking for both session replay
159
- and non-replay events (DOM / network / logs / errors).
327
+ ## Web Worker offloading
160
328
 
161
- ```typescript
162
- groundcover.init({
163
- /* … */
164
- options: {
165
- privacy: {
166
- level: "mask-sensitive", // 'mask-sensitive' (default) | 'mask-all' | 'allow'
167
- maskSelectors: [".cc-number", "[data-secret]"], // always masked (replay + DOM)
168
- sensitiveKeys: ["account_number"], // extra case-insensitive key substrings
169
- // maskNetworkBodies / maskNetworkQueryParams / maskLogs / maskErrors: default true
170
- },
171
- },
172
- });
329
+ The SDK spawns a dedicated Web Worker on init and offloads CPU-heavy work — session-replay event
330
+ packing and outgoing-batch gzip — off the main thread so it doesn’t compete with your page’s UI
331
+ work.
332
+
333
+ The worker is bundled inline (no extra files to host) and spawned from a `blob:` URL. On
334
+ environments that don’t permit blob-sourced workers — strict Content Security Policy without
335
+ `worker-src 'self' blob:`, sandboxed iframes, SSR — the SDK silently falls back to the main-thread
336
+ implementation. Behavior, wire format, and the public API are identical in both modes.
337
+
338
+ ---
339
+
340
+ ## Session lifecycle
341
+
342
+ `sessionMaxDuration` sets a **target** maximum wall-clock session length (default 4 hours; must be
343
+ between 1 minute and 8 hours).
344
+
345
+ It is enforced **lazily, on activity — not by a background timer**, so it is **not a hard upper
346
+ bound**. Once the cap has elapsed, the *next* user/business event (click, navigation, log, custom,
347
+ network, exception, …) flushes pending events under the current session id, mints a fresh id, and
348
+ resumes replay recording if it had been active.
349
+
350
+ Because rotation is activity-gated, a session that goes idle keeps its id past the cap until the
351
+ next qualifying event:
352
+
353
+ - A dormant or backgrounded tab that produces no further events stays on the same session id — by
354
+ design, so the SDK doesn’t mint “phantom” sessions for tabs nobody is using.
355
+ - **Session-replay batches don’t count as activity** (rrweb emits a snapshot heartbeat every ~30s
356
+ regardless of interaction), so a tab that’s only recording replay won’t rotate on that alone. It’s
357
+ ultimately bounded by the 30-minute replay-inactivity stop, not by this cap.
358
+
359
+ Sessions are also bounded by a 30-minute inactivity gap, enforced the same lazy way. The flush is
360
+ best-effort and the rotation always proceeds regardless of delivery success. Invalid values fall
361
+ back to the default with a `console.warn`.
362
+
363
+ ### Micro-frontend session synchronization
364
+
365
+ Pass a shared `sessionId` so multiple frontends report under one session:
366
+
367
+ ```ts
368
+ const sharedSessionId = "session-12345";
369
+ groundcover.init({ /* …app… */ apiKey, dsn, cluster, appId: "shell", sessionId: sharedSessionId });
370
+ groundcover.init({ /* …mfe… */ apiKey, dsn, cluster, appId: "micro-frontend", sessionId: sharedSessionId });
173
371
  ```
174
372
 
175
- ### Levels
373
+ ---
176
374
 
177
- | Level | Inputs / keystrokes | Static page text | Network bodies, query params, logs, errors |
178
- |---|---|---|---|
179
- | `mask-sensitive` *(default)* | only **sensitive** form controls masked; all others captured | captured (except `data-private` / `maskSelectors` subtrees) | redacted by sensitive-key matching |
180
- | `mask-all` | **all** inputs masked | masked | redacted |
181
- | `allow` | captured | captured | not redacted |
182
-
183
- Under the default `mask-sensitive`, there are **two kinds of signal**.
184
-
185
- **Explicit / container signals** — mask the element *and its whole subtree*,
186
- whether it holds a form input or static text:
187
-
188
- - it **or any ancestor** has a `data-private` attribute;
189
- - it **or any ancestor** matches one of `privacy.maskSelectors`;
190
- - one of its `id` / `name` / `class` / `aria-label` / `placeholder` attributes
191
- contains a legacy `maskFields` substring.
192
-
193
- **Built-in field heuristic** — applies **only to form controls**
194
- (`<input>`, `<textarea>`, `<select>`), so ordinary static page text stays
195
- visible:
196
-
197
- - it is `<input type="password">`;
198
- - one of its `id` / `name` / `class` / `aria-label` / `placeholder` attributes
199
- matches a built-in sensitive-key pattern (`password`, `secret`, `token`,
200
- `api-key`, `access-key`, `auth`/`authorization`, `bearer`, `credential`,
201
- `cvv`, `ssn`, `card-number`, …) or one of your `privacy.sensitiveKeys`.
202
-
203
- So a non-secret form control (search, name, description, date, numeric field) is
204
- captured with its real value, and a **static** element is never masked by the
205
- field heuristic alone — e.g. `<div id="token">` stays visible. To mask a
206
- sensitive **static** value, mark it with `data-private` or a `maskSelectors`
207
- entry (both also apply in session replay). Use `'mask-all'` to mask every input
208
- plus all static text, or `'allow'` to disable masking entirely.
209
-
210
- For **session replay**, non-input/static text (e.g. a value in a
211
- `contenteditable` or a plain `<div>`) is masked when it sits under a
212
- `data-private` or `privacy.maskSelectors` subtree; the built-in field heuristic
213
- does not mask arbitrary text.
214
-
215
- Regardless of level (even `allow`), built-in sensitive request/response **headers**
216
- (`authorization`, `cookie`, `set-cookie`, and `token`/`key`/`secret`/`password`-like
217
- names) are always stripped.
218
-
219
- ### Advanced
220
-
221
- - `privacy.redact(field)` — per-leaf hook for full control over non-replay
222
- redaction (network/log/error). Return a replacement primitive, or `undefined`
223
- to defer to the SDK default.
224
- - `privacy.replay.maskTextFn` / `privacy.replay.maskInputFn` — rrweb
225
- pass-throughs for session-replay text/input masking. A custom `maskInputFn`
226
- takes over input masking in the replay (it is called for every input).
227
-
228
- > The legacy `enableMasking` / `maskFields` options are **deprecated** but still
229
- > honored: `enableMasking: true` → `'mask-all'`, explicit `enableMasking: false`
230
- > → `'allow'`, and `maskFields` entries match DOM element attributes. Prefer
231
- > `privacy.level` / `privacy.maskSelectors` / `privacy.sensitiveKeys`.
232
-
233
- Attributes you send **explicitly** via `groundcover.sendCustomEvent(...)` are
234
- **not** auto-redacted — scrub them before sending (or via `enrichEvent`).
375
+ ## Performance considerations
235
376
 
236
- ## Web Worker offloading
377
+ - **Batching & compression** — events are buffered (`transport.batchSize` / `transport.batchTimeout`) and
378
+ gzipped (`transport.compression`) to minimize request count and payload size.
379
+ - **Web Worker offload** — replay packing and gzip run off the main thread (see above).
380
+ - **Sampling** — cap volume with `sessionSampleRate` / `eventSampleRate`.
381
+ - **Scope what you capture** — drop noisy endpoints with `excludedUrls`, and disable unused
382
+ instrumentations via `enabledEvents`.
383
+ - **Lazy session rotation** — no background timers; rotation work happens only on real activity.
384
+ - **Bundle size** — ships ESM + CJS with built-in types; the published bundle is size-budgeted in
385
+ CI.
237
386
 
238
- The SDK spawns a dedicated Web Worker on init and offloads CPU-heavy work — session-replay event packing and outgoing-batch gzip — off the main thread so it doesn't compete with the host page's UI work.
387
+ ---
239
388
 
240
- The worker is bundled inline (no extra files to host) and spawned from a `blob:` URL. On environments that don't permit blob-sourced workers — strict Content Security Policy without `worker-src 'self' blob:`, sandboxed iframes, SSR — the SDK silently falls back to the existing main-thread implementation. Behavior, wire format, and the public API are identical in both modes.
389
+ ## API reference
241
390
 
242
- You can pass the values by calling the init function:
391
+ All methods are available on the default export and on `window.groundcover`.
243
392
 
244
- ```typescript
245
- groundcover.init({
246
- apiKey: "your-api-key",
247
- cluster: "your-cluster",
248
- environment: "production",
249
- dsn: "your-dsn",
250
- appId: "your-app-id",
251
- sessionId: "optional-session-id", // For micro-frontend synchronization
252
- options: {
253
- batchSize: 50,
254
- sessionSampleRate: 0.5, // 50% sessions sampled
255
- eventSampleRate: 0.5,
256
- },
393
+ | Method | Description |
394
+ |---|---|
395
+ | `init(config)` | Initialize the SDK and install instrumentation. |
396
+ | `identifyUser(user)` | Attach user identity to subsequent events. |
397
+ | `sendCustomEvent({ event, attributes })` | Emit a custom business event. |
398
+ | `captureException(error, metadata?)` | Capture a handled error with optional context. |
399
+ | `logger.{log,info,warn,error,debug,trace}(message, attributes?)` | Structured logging (see below). |
400
+ | `updateConfig({ options?, user?, … })` | Update config at runtime (merges nested groups one level deep). |
401
+ | `startNavigation(metadata)` / `endNavigation(metadata)` | Manual navigation spans (when `navigation` isn’t auto-tracked). |
402
+ | `getSessionId()` / `setSessionId(id?)` | Read / override the current session id. |
403
+ | `startReplayRecording()` / `stopReplayRecording()` | Manually control session replay. |
404
+
405
+ ### Structured logs
406
+
407
+ `groundcover.logger` mirrors Sentry’s `logger` pattern — one method per level, second arg is the
408
+ attributes object. Nested objects are flattened to dotted keys:
409
+
410
+ ```ts
411
+ groundcover.logger.warn("Checkout failed", {
412
+ orderId: "ord_42",
413
+ cart: { items: 3, total: 99.99 }, // → cart.items, cart.total
257
414
  });
258
415
  ```
259
416
 
260
- Or via the updateConfig function (options are nested under `options`,
261
- matching the `init` shape):
417
+ The SDK also auto-captures `console.*` calls; when any argument is a plain object, its keys are
418
+ promoted to structured log attributes. Reserved keys (`message`, `level`, `location`) are always
419
+ set by the SDK and can’t be overridden.
262
420
 
263
- ```typescript
264
- groundcover.updateConfig({
265
- options: {
266
- batchSize: 20,
267
- },
421
+ ### `identifyUser`
422
+
423
+ ```ts
424
+ groundcover.identifyUser({
425
+ id: "u_123",
426
+ email: "john@acme.com",
427
+ organization: "acme",
268
428
  });
269
429
  ```
430
+
431
+ ### `captureException`
432
+
433
+ ```ts
434
+ groundcover.captureException(new Error("Payment failed"), {
435
+ userId: "123",
436
+ feature: "checkout",
437
+ });
438
+ ```
439
+
440
+ ---
441
+
442
+ ## Migrating to 1.0.0
443
+
444
+ `1.0.0` restructures `options` by concern (a clean break from `0.x`) and removes the deprecated
445
+ masking flags. Update your config as follows:
446
+
447
+ | `0.x` | `1.0.0` |
448
+ |---|---|
449
+ | `environment` *(duplicated in `options`)* | top-level `environment` only |
450
+ | `userIdentifier` | `user` |
451
+ | `options.sessionReplay.blockedSelectors` | `options.replay.blockedSelectors` |
452
+ | `options.tracePropagationUrls` | `options.tracing.propagationUrls` |
453
+ | `options.tracePropagationHeaders` | `options.tracing.propagationHeaders` |
454
+ | `options.tracePropagationTraceIdHeaderName` | `options.tracing.traceIdHeaderName` |
455
+ | `options.tracePropagationSpanIdHeaderName` | `options.tracing.spanIdHeaderName` |
456
+ | `options.traceOrigin` | `options.tracing.origin` |
457
+ | `options.batchSize` / `options.batchTimeout` | `options.transport.batchSize` / `options.transport.batchTimeout` |
458
+ | `options.enableCompression` | `options.transport.compression` |
459
+ | `options.enableMasking: true` *(removed — ignored)* | set `options.privacy.level: 'mask-all'` |
460
+ | `options.enableMasking: false` *(removed — ignored)* | set `options.privacy.level: 'allow'` |
461
+ | `options.maskFields` *(removed — ignored)* | set `options.privacy.maskSelectors` / `options.privacy.sensitiveKeys` |
462
+
463
+ > ⚠️ The removed masking flags are **ignored, not auto-mapped** — passing `enableMasking` /
464
+ > `maskFields` logs a `console.warn` and has **no effect**. You must set the corresponding `privacy`
465
+ > option yourself (right-hand column). Masking remains **on by default** (`mask-sensitive`); if you
466
+ > previously relied on masking being off, set `privacy: { level: 'allow' }` explicitly.