@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/CHANGELOG.md +57 -0
- package/README.md +387 -190
- package/assets/logo.png +0 -0
- package/dist/index.d.mts +56 -31
- package/dist/index.d.ts +56 -31
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +25 -5
package/README.md
CHANGED
|
@@ -1,269 +1,466 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./assets/logo.png" alt="groundcover" width="240" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">RUM — Browser SDK</h1>
|
|
4
6
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
//
|
|
33
|
-
|
|
62
|
+
// Tie events to a user
|
|
63
|
+
groundcover.identifyUser({ id: "u_123", email: "john@acme.com", organization: "acme" });
|
|
34
64
|
|
|
35
|
-
//
|
|
36
|
-
groundcover.
|
|
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
|
-
//
|
|
46
|
-
groundcover.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
75
|
+
---
|
|
57
76
|
|
|
58
|
-
|
|
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.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
+
### `options.transport`
|
|
77
159
|
|
|
78
|
-
|
|
160
|
+
Batching and delivery of outgoing events.
|
|
79
161
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
181
|
+
### Updating config at runtime
|
|
94
182
|
|
|
95
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
+
---
|
|
109
199
|
|
|
110
|
-
|
|
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
|
-
|
|
230
|
+
options: {
|
|
231
|
+
privacy: {
|
|
232
|
+
level: "mask-sensitive",
|
|
233
|
+
maskSelectors: [".pii", "#ssn"],
|
|
234
|
+
sensitiveKeys: ["account_no"],
|
|
235
|
+
},
|
|
236
|
+
}
|
|
114
237
|
```
|
|
115
238
|
|
|
116
|
-
|
|
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.
|
|
120
|
-
|
|
121
|
-
feature: "checkout",
|
|
122
|
-
cart: { items: 3, total: 99.99 },
|
|
123
|
-
});
|
|
271
|
+
groundcover.startReplayRecording();
|
|
272
|
+
groundcover.stopReplayRecording();
|
|
124
273
|
```
|
|
125
274
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
304
|
+
**Event lifecycle (in order):**
|
|
145
305
|
|
|
146
|
-
|
|
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
|
-
|
|
314
|
+
Supporting pieces:
|
|
149
315
|
|
|
150
|
-
-
|
|
151
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
325
|
+
---
|
|
156
326
|
|
|
157
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
373
|
+
---
|
|
176
374
|
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
387
|
+
---
|
|
239
388
|
|
|
240
|
-
|
|
389
|
+
## API reference
|
|
241
390
|
|
|
242
|
-
|
|
391
|
+
All methods are available on the default export and on `window.groundcover`.
|
|
243
392
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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.
|