@groundcover/browser 0.1.0 → 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 +371 -193
- 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,222 +1,242 @@
|
|
|
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**.
|
|
8
16
|
|
|
9
|
-
|
|
10
|
-
npm install @groundcover/browser
|
|
11
|
-
```
|
|
17
|
+
> Session replay is powered by [**rrweb**](https://github.com/rrweb-io/rrweb) under the hood.
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
See the [groundcover RUM documentation](https://docs.groundcover.com/capabilities/real-user-monitoring-rum/)
|
|
20
|
+
for the full product guide.
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
import groundcover from "@groundcover/browser";
|
|
22
|
+
---
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
apiKey: "your-api-key",
|
|
20
|
-
cluster: "your-cluster",
|
|
21
|
-
environment: "production",
|
|
22
|
-
dsn: "your-dsn",
|
|
23
|
-
appId: "your-app-id",
|
|
24
|
-
});
|
|
25
|
-
```
|
|
24
|
+
## Table of contents
|
|
26
25
|
|
|
27
|
-
|
|
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)
|
|
28
36
|
|
|
29
|
-
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install @groundcover/browser
|
|
43
|
+
```
|
|
30
44
|
|
|
31
45
|
```ts
|
|
32
|
-
|
|
33
|
-
const sharedSessionId = "session-12345";
|
|
46
|
+
import groundcover from "@groundcover/browser";
|
|
34
47
|
|
|
35
|
-
// Initialize SDK in main app
|
|
36
48
|
groundcover.init({
|
|
37
49
|
apiKey: "your-api-key",
|
|
38
|
-
cluster: "your-cluster",
|
|
39
|
-
environment: "production",
|
|
40
50
|
dsn: "your-dsn",
|
|
41
|
-
appId: "your-app-id",
|
|
42
|
-
sessionId: sharedSessionId, // Use shared sessionId
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
// Initialize SDK in micro-frontend with same sessionId
|
|
46
|
-
groundcover.init({
|
|
47
|
-
apiKey: "your-api-key",
|
|
48
51
|
cluster: "your-cluster",
|
|
52
|
+
appId: "your-app-id",
|
|
49
53
|
environment: "production",
|
|
50
|
-
dsn: "your-dsn",
|
|
51
|
-
appId: "micro-frontend-app-id",
|
|
52
|
-
sessionId: sharedSessionId, // Same sessionId for session continuity
|
|
53
54
|
});
|
|
54
55
|
```
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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:
|
|
59
60
|
|
|
60
61
|
```ts
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
email: "john@groundcover.com",
|
|
64
|
-
organization: "groundcover",
|
|
65
|
-
});
|
|
66
|
-
```
|
|
62
|
+
// Tie events to a user
|
|
63
|
+
groundcover.identifyUser({ id: "u_123", email: "john@acme.com", organization: "acme" });
|
|
67
64
|
|
|
68
|
-
|
|
65
|
+
// Capture a handled error
|
|
66
|
+
groundcover.captureException(new Error("Checkout failed"), { feature: "checkout" });
|
|
69
67
|
|
|
70
|
-
|
|
68
|
+
// Emit a structured log
|
|
69
|
+
groundcover.logger.warn("Payment retry", { provider: "stripe", attempt: 2 });
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
groundcover.
|
|
71
|
+
// Emit a custom business event
|
|
72
|
+
groundcover.sendCustomEvent({ event: "plan_upgraded", attributes: { plan: "pro" } });
|
|
74
73
|
```
|
|
75
74
|
|
|
76
|
-
|
|
75
|
+
---
|
|
77
76
|
|
|
78
|
-
|
|
77
|
+
## Configuration reference
|
|
78
|
+
|
|
79
|
+
`init()` takes connection/identity fields at the top level and all behavioral knobs under
|
|
80
|
+
`options`, grouped by concern.
|
|
79
81
|
|
|
80
82
|
```ts
|
|
81
|
-
groundcover.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
groundcover.init({
|
|
84
|
+
// ── Connection & identity ──
|
|
85
|
+
apiKey, dsn, cluster, appId,
|
|
86
|
+
environment, namespace, releaseId,
|
|
87
|
+
user, sessionId,
|
|
86
88
|
|
|
87
|
-
|
|
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
|
+
},
|
|
100
|
+
});
|
|
89
101
|
```
|
|
90
102
|
|
|
91
|
-
|
|
103
|
+
### Top-level (connection & identity)
|
|
92
104
|
|
|
93
|
-
|
|
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). |
|
|
94
130
|
|
|
95
|
-
|
|
131
|
+
### `options.privacy`
|
|
96
132
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
appId: "web",
|
|
100
|
-
environment: "prod",
|
|
101
|
-
userAction: { type: "click", target: "checkout-button" },
|
|
102
|
-
});
|
|
103
|
-
// → log event attributes include: appId, environment, userAction.type, userAction.target
|
|
104
|
-
```
|
|
133
|
+
Drives both replay and non-replay masking. **On by default.** See
|
|
134
|
+
[Privacy & data masking](#privacy--data-masking) for the full table.
|
|
105
135
|
|
|
106
|
-
|
|
136
|
+
### `options.tracing`
|
|
107
137
|
|
|
108
|
-
|
|
138
|
+
Distributed-tracing header propagation for outgoing requests.
|
|
109
139
|
|
|
110
|
-
|
|
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. |
|
|
111
147
|
|
|
112
148
|
```ts
|
|
113
|
-
|
|
149
|
+
options: {
|
|
150
|
+
tracing: {
|
|
151
|
+
propagationUrls: ["https://api.acme.com/*"],
|
|
152
|
+
traceIdHeaderName: "x-groundcover-trace-id",
|
|
153
|
+
spanIdHeaderName: "x-groundcover-span-id",
|
|
154
|
+
},
|
|
155
|
+
}
|
|
114
156
|
```
|
|
115
157
|
|
|
116
|
-
|
|
158
|
+
### `options.transport`
|
|
117
159
|
|
|
118
|
-
|
|
119
|
-
groundcover.captureException(new Error("Payment failed"), {
|
|
120
|
-
userId: "123",
|
|
121
|
-
feature: "checkout",
|
|
122
|
-
cart: { items: 3, total: 99.99 },
|
|
123
|
-
});
|
|
124
|
-
```
|
|
125
|
-
|
|
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
|
-
}
|
|
142
|
-
```
|
|
160
|
+
Batching and delivery of outgoing events.
|
|
143
161
|
|
|
144
|
-
|
|
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). |
|
|
145
167
|
|
|
146
|
-
|
|
168
|
+
### `options.replay`
|
|
147
169
|
|
|
148
|
-
|
|
170
|
+
Session-replay recording controls. **This is noise reduction, not privacy** — to mask sensitive
|
|
171
|
+
content use [`privacy`](#privacy--data-masking).
|
|
149
172
|
|
|
150
|
-
|
|
151
|
-
|
|
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). |
|
|
152
176
|
|
|
153
|
-
|
|
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.
|
|
154
180
|
|
|
155
|
-
|
|
181
|
+
### Updating config at runtime
|
|
156
182
|
|
|
157
|
-
|
|
158
|
-
`privacy.level
|
|
159
|
-
and non-replay events (DOM / network / logs / errors).
|
|
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:
|
|
160
185
|
|
|
161
|
-
```
|
|
162
|
-
groundcover.
|
|
163
|
-
/* … */
|
|
186
|
+
```ts
|
|
187
|
+
groundcover.updateConfig({
|
|
164
188
|
options: {
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
},
|
|
189
|
+
transport: { batchSize: 20 }, // batchTimeout/compression are preserved
|
|
190
|
+
privacy: { level: "mask-all" },
|
|
171
191
|
},
|
|
172
192
|
});
|
|
173
193
|
```
|
|
174
194
|
|
|
175
|
-
|
|
176
|
-
|
|
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 |
|
|
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).
|
|
182
197
|
|
|
183
|
-
|
|
198
|
+
---
|
|
184
199
|
|
|
185
|
-
|
|
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.
|
|
200
|
+
## Privacy & data masking
|
|
192
201
|
|
|
193
|
-
**
|
|
194
|
-
|
|
195
|
-
visible:
|
|
202
|
+
Masking is **on by default** (`privacy.level: 'mask-sensitive'`). A single `level` is the master
|
|
203
|
+
switch; finer toggles and hooks refine it.
|
|
196
204
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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) |
|
|
202
210
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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.
|
|
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.
|
|
209
215
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
`
|
|
213
|
-
|
|
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). |
|
|
214
228
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
229
|
+
```ts
|
|
230
|
+
options: {
|
|
231
|
+
privacy: {
|
|
232
|
+
level: "mask-sensitive",
|
|
233
|
+
maskSelectors: [".pii", "#ssn"],
|
|
234
|
+
sensitiveKeys: ["account_no"],
|
|
235
|
+
},
|
|
236
|
+
}
|
|
237
|
+
```
|
|
218
238
|
|
|
219
|
-
|
|
239
|
+
#### Built-in sensitive patterns
|
|
220
240
|
|
|
221
241
|
These are always treated as sensitive (case-insensitive); your `sensitiveKeys` are merged in on top.
|
|
222
242
|
|
|
@@ -231,58 +251,216 @@ These are always treated as sensitive (case-insensitive); your `sensitiveKeys` a
|
|
|
231
251
|
- **Query / form param names** — matched as a whole key (not inside JSON bodies), for OAuth-style
|
|
232
252
|
callbacks: `code`, `state`, `session_state`, `id_token`, `access_token`, `refresh_token`, `token`.
|
|
233
253
|
|
|
234
|
-
To opt out
|
|
235
|
-
|
|
236
|
-
|
|
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:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
groundcover.startReplayRecording();
|
|
272
|
+
groundcover.stopReplayRecording();
|
|
273
|
+
```
|
|
274
|
+
|
|
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
|
|
302
|
+
```
|
|
237
303
|
|
|
238
|
-
|
|
304
|
+
**Event lifecycle (in order):**
|
|
239
305
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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.
|
|
246
313
|
|
|
247
|
-
|
|
248
|
-
> honored: `enableMasking: true` → `'mask-all'`, explicit `enableMasking: false`
|
|
249
|
-
> → `'allow'`, and `maskFields` entries match DOM element attributes. Prefer
|
|
250
|
-
> `privacy.level` / `privacy.maskSelectors` / `privacy.sensitiveKeys`.
|
|
314
|
+
Supporting pieces:
|
|
251
315
|
|
|
252
|
-
|
|
253
|
-
|
|
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.
|
|
321
|
+
|
|
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`.
|
|
324
|
+
|
|
325
|
+
---
|
|
254
326
|
|
|
255
327
|
## Web Worker offloading
|
|
256
328
|
|
|
257
|
-
The SDK spawns a dedicated Web Worker on init and offloads CPU-heavy work — session-replay event
|
|
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.
|
|
258
332
|
|
|
259
|
-
The worker is bundled inline (no extra files to host) and spawned from a `blob:` URL. On
|
|
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.
|
|
260
337
|
|
|
261
|
-
|
|
338
|
+
---
|
|
262
339
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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 });
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Performance considerations
|
|
376
|
+
|
|
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.
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## API reference
|
|
390
|
+
|
|
391
|
+
All methods are available on the default export and on `window.groundcover`.
|
|
392
|
+
|
|
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
|
|
276
414
|
});
|
|
277
415
|
```
|
|
278
416
|
|
|
279
|
-
|
|
280
|
-
|
|
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.
|
|
281
420
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
421
|
+
### `identifyUser`
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
groundcover.identifyUser({
|
|
425
|
+
id: "u_123",
|
|
426
|
+
email: "john@acme.com",
|
|
427
|
+
organization: "acme",
|
|
428
|
+
});
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### `captureException`
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
groundcover.captureException(new Error("Payment failed"), {
|
|
435
|
+
userId: "123",
|
|
436
|
+
feature: "checkout",
|
|
287
437
|
});
|
|
288
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.
|