@groundcover/browser 1.0.9 → 2.0.0-rc.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
@@ -32,6 +32,7 @@ for the full product guide.
32
32
  - [Session lifecycle](#session-lifecycle)
33
33
  - [Performance considerations](#performance-considerations)
34
34
  - [API reference](#api-reference)
35
+ - [Migrating from 1.x to 2.0.0](#migrating-from-1x-to-200)
35
36
  - [Migrating to 1.0.0](#migrating-to-100)
36
37
 
37
38
  ---
@@ -80,6 +81,11 @@ groundcover.sendCustomEvent({ event: "plan_upgraded", attributes: { plan: "pro"
80
81
  `init()` takes connection/identity fields at the top level and all behavioral knobs under
81
82
  `options`, grouped by concern.
82
83
 
84
+ The first successful `init()` initializes the SDK. Every later call is silently ignored, even with
85
+ a different `appId` or config. Calls made during initialization are also ignored; if initialization
86
+ fails, a later call can retry. Use [`updateConfig()`](#updating-config-at-runtime) or the other runtime
87
+ APIs for supported changes.
88
+
83
89
  ```ts
84
90
  groundcover.init({
85
91
  // ── Connection & identity ──
@@ -89,7 +95,7 @@ groundcover.init({
89
95
 
90
96
  options: {
91
97
  debug,
92
- sessionSampleRate, eventSampleRate,
98
+ sessionSampleRate,
93
99
  enabledEvents, excludedUrls,
94
100
  sessionMaxDuration,
95
101
  beforeSend, enrichEvent,
@@ -108,7 +114,7 @@ groundcover.init({
108
114
  | `apiKey` | `string` | ✅ | Your groundcover RUM API key. |
109
115
  | `dsn` | `string` | ✅ | RUM intake endpoint. A bare host is upgraded to `https://`. |
110
116
  | `cluster` | `string` | ✅ | Target groundcover cluster. |
111
- | `appId` | `string` | ✅ | Application identifier; reported as `service.name`. |
117
+ | `appId` | `string` | ✅ | Required application identifier; cannot be empty or whitespace-only. Reported unchanged as `service.name`. |
112
118
  | `environment` | `string` | — | Deployment environment (e.g. `production`, `staging`). |
113
119
  | `namespace` | `string` | — | Logical grouping/namespace for the app. |
114
120
  | `releaseId` | `string` | — | Release/version identifier (e.g. a semver or commit hash). |
@@ -122,7 +128,6 @@ groundcover.init({
122
128
  |---|---|---|---|
123
129
  | `debug` | `boolean` | `false` | Verbose SDK console logging. |
124
130
  | `sessionSampleRate` | `number` | `1` | Fraction of sessions captured (0–1). |
125
- | `eventSampleRate` | `number` | `1` | Fraction of events captured (0–1). |
126
131
  | `enabledEvents` | `Array<…>` | `[]` | Instrumentations to enable. **Empty = all.** Members: `dom`, `network`, `exceptions`, `logs`, `pageload`, `navigation`, `performance`, `replay`. Note: `replay` still requires an explicit [`startReplayRecording()`](#session-replay) call — it never auto-starts. |
127
132
  | `excludedUrls` | `Array<string \| RegExp>` | — | Network requests matching these are not captured. |
128
133
  | `sessionMaxDuration` | `number` | `4h` | Target max session length in ms (1 min – 8 h). See [session lifecycle](#session-lifecycle). |
@@ -181,7 +186,9 @@ content use [`privacy`](#privacy--data-masking).
181
186
 
182
187
  ### Updating config at runtime
183
188
 
184
- `updateConfig` mirrors the `init` shape; nested groups merge one level deep (and `tracing.origin` /
189
+ `updateConfig` accepts these optional fields: `cluster`, `environment`, `namespace`, `releaseId`,
190
+ `dsn`, `apiKey`, `options`, and `user`. `appId` cannot be updated. `sessionId` is not accepted;
191
+ use the separate `setSessionId()` API. Nested groups merge one level deep (and `tracing.origin` /
185
192
  `privacy.replay` one level deeper), so a partial update preserves the other keys in the group:
186
193
 
187
194
  ```ts
@@ -423,14 +430,20 @@ every other guarantee here is stated for that case.
423
430
 
424
431
  ### Micro-frontend session synchronization
425
432
 
426
- Pass a shared `sessionId` so multiple frontends report under one session:
433
+ Initialize the shared SDK once in the shell with a shared `sessionId` so the micro-frontends report
434
+ under one session. Each micro-frontend uses that same SDK to capture events:
427
435
 
428
436
  ```ts
437
+ // Shell
429
438
  const sharedSessionId = "session-12345";
430
- groundcover.init({ /* …app… */ apiKey, dsn, cluster, appId: "shell", sessionId: sharedSessionId });
431
- groundcover.init({ /* …mfe… */ apiKey, dsn, cluster, appId: "micro-frontend", sessionId: sharedSessionId });
439
+ groundcover.init({ apiKey, dsn, cluster, appId: "your-app-id", sessionId: sharedSessionId });
440
+
441
+ // Micro-frontend using the shared SDK
442
+ groundcover.sendCustomEvent({ event: "checkout_opened", attributes: { cartId: "cart-123" } });
432
443
  ```
433
444
 
445
+ If you need to explicitly rebind the shared SDK to another session, use `setSessionId()`.
446
+
434
447
  ---
435
448
 
436
449
  ## Performance considerations
@@ -438,7 +451,8 @@ groundcover.init({ /* …mfe… */ apiKey, dsn, cluster, appId: "micro-frontend"
438
451
  - **Batching & compression** — events are buffered (`transport.batchSize` / `transport.batchTimeout`) and
439
452
  gzipped (`transport.compression`) to minimize request count and payload size.
440
453
  - **Web Worker offload** — replay packing and gzip run off the main thread (see above).
441
- - **Sampling** — cap volume with `sessionSampleRate` / `eventSampleRate`.
454
+ - **Sampling** — use `sessionSampleRate` to select whole sessions. Events within selected
455
+ sessions are not randomly sampled; enabled-event settings, filtering, and privacy controls still apply.
442
456
  - **Scope what you capture** — drop noisy endpoints with `excludedUrls`, and disable unused
443
457
  instrumentations via `enabledEvents`.
444
458
  - **Lazy session rotation** — no background timers; rotation work happens only on real activity.
@@ -453,7 +467,7 @@ All methods are available on the default export and on `window.groundcover`.
453
467
 
454
468
  | Method | Description |
455
469
  |---|---|
456
- | `init(config)` | Initialize the SDK and install instrumentation. |
470
+ | `init(config)` | Initialize the SDK and install instrumentation once; calls after successful initialization are ignored. |
457
471
  | `identifyUser(user)` | Attach user identity to subsequent events. |
458
472
  | `sendCustomEvent({ event, attributes })` | Emit a custom business event. |
459
473
  | `captureException(error, metadata?)` | Capture a handled error with optional context. |
@@ -500,6 +514,68 @@ groundcover.captureException(new Error("Payment failed"), {
500
514
 
501
515
  ---
502
516
 
517
+ ## Migrating from 1.x to 2.0.0
518
+
519
+ `2.0.0` removes event-level sampling and makes browser application identity fixed after the first
520
+ successful initialization. These are breaking changes; review your initialization and runtime
521
+ updates before upgrading:
522
+
523
+ ```bash
524
+ npm install @groundcover/browser@2.0.0-rc.0
525
+ ```
526
+
527
+ | Change | Migration |
528
+ |---|---|
529
+ | `options.eventSampleRate` is removed | Delete it from initialization and runtime updates. Choose `options.sessionSampleRate` explicitly if you need to limit capture volume. |
530
+ | `appId` is required and cannot be blank | Supply a non-empty, non-whitespace string at initialization. Its value is preserved exactly. |
531
+ | Each SDK instance initializes once | Initialize in your application's bootstrap code. Use runtime APIs instead of calling `init()` again. |
532
+ | `appId` cannot be updated | Exclude it from `updateConfig()`, including when spreading an initialization config. Its presence rejects the entire update, even if its value is unchanged or `undefined`. |
533
+
534
+ ### Why event-level sampling was removed
535
+
536
+ Randomly dropping individual events leaves gaps in user journeys and can distort funnel analysis.
537
+ The SDK now uses `sessionSampleRate` to select sessions without randomly dropping individual events
538
+ within them. Enabled-event settings, filtering, and privacy controls still apply; session replay
539
+ still requires an explicit start.
540
+
541
+ > **Volume warning:** If you previously set `eventSampleRate` below `1`, upgrading can increase event
542
+ > volume. The removed option has no effect: passing it warns and strips it from stored configuration.
543
+ > `sessionSampleRate` is not a rename or an equivalent numeric replacement: `0.1` selects roughly 10%
544
+ > of sessions, rather than sampling 10% of events within sessions. Rates are never automatically
545
+ > converted. If event sampling was left at its default of `1`, this removal does not increase capture
546
+ > through sampling alone.
547
+
548
+ For example, if you deliberately choose to capture 10% of sessions:
549
+
550
+ ```ts
551
+ groundcover.init({
552
+ apiKey, dsn, cluster,
553
+ appId: "checkout",
554
+ options: { sessionSampleRate: 0.1 },
555
+ });
556
+ ```
557
+
558
+ ### Initialize once; update supported fields explicitly
559
+
560
+ Missing, non-string, or blank `appId` values produce a warning and leave the SDK uninitialized.
561
+ After correcting the input, call `init()` again. A failed initialization also permits an explicit
562
+ retry; calls during initialization or after success are silently ignored, regardless of `appId`.
563
+
564
+ Do not spread your initialization config into a runtime update:
565
+
566
+ ```ts
567
+ // Pass only the fields you intend to change, without appId.
568
+ groundcover.updateConfig({ dsn: newDsn, apiKey: newApiKey, cluster: newCluster });
569
+ groundcover.identifyUser({ id: "user-123" });
570
+ await groundcover.setSessionId("shared-session-id");
571
+ ```
572
+
573
+ An update containing `appId` warns and applies **none** of its fields. Use `identifyUser()` for user
574
+ identity and `setSessionId()` for session identity; neither changes the application identity.
575
+ For micro-frontends, initialize the shared SDK once in the shell and reuse it in each frontend.
576
+
577
+ ---
578
+
503
579
  ## Migrating to 1.0.0
504
580
 
505
581
  `1.0.0` restructures `options` by concern (a clean break from `0.x`) and removes the deprecated
package/dist/index.d.mts CHANGED
@@ -4,14 +4,9 @@
4
4
  */
5
5
  type KnownEventType = "dom.event" | "exception" | "network" | "log" | "pageload" | "navigation" | "custom" | "performance" | "replay";
6
6
  /**
7
- * `TType` is what lets a platform package name its own event kinds without
8
- * editing core: React Native can pass `KnownEventType | "tap" | "screen_view"`
9
- * and every core class below flows it through. It defaults to
10
- * `KnownEventType`, so nothing that only deals in the shipped browser kinds
11
- * has to spell it out.
12
- *
13
- * Shared shapes are still shared — the point is that the set of names is open,
14
- * not that each platform reinvents the envelope.
7
+ * `TType` lets a platform name its own event kinds without editing core, and every core class
8
+ * flows it through. Defaults to `KnownEventType`, so callers dealing only in browser kinds need
9
+ * not spell it out. Shared shapes stay shared — the set of NAMES is open, not the envelope.
15
10
  */
16
11
  type BaseEvent<TType extends string = KnownEventType> = {
17
12
  type: TType;
@@ -280,7 +275,6 @@ interface SDKOptions$1<TEvent extends BaseEvent<string> = EventTypes$1[keyof Eve
280
275
  * package's `config/types.ts`).
281
276
  */
282
277
  enabledEvents: string[];
283
- eventSampleRate: number;
284
278
  sessionSampleRate: number;
285
279
  debug: boolean;
286
280
  excludedUrls?: Array<string | RegExp>;
@@ -302,41 +296,12 @@ interface SDKOptions$1<TEvent extends BaseEvent<string> = EventTypes$1[keyof Eve
302
296
  */
303
297
  replay?: unknown;
304
298
  /**
305
- * Target maximum wall-clock duration of a session, in milliseconds.
299
+ * Target maximum wall-clock duration of a session, in ms. Enforced LAZILY on activity, so NOT
300
+ * a hard bound — an idle tab keeps its id past the cap, which deliberately avoids minting
301
+ * phantom sessions. Also bounded by a 30-minute inactivity gap, counting raw input as
302
+ * presence so passive reading does not end a session.
306
303
  *
307
- * Enforced LAZILY, on activity — not by a background timer. Once the cap has
308
- * elapsed, the next user/business event (click, navigation, log, custom,
309
- * network, exception, …) flushes buffered events under the current session
310
- * id, then mints a fresh session id and restarts replay recording if it was
311
- * active.
312
- *
313
- * Because it is activity-gated, this is NOT a hard upper bound: a session
314
- * only rotates when the next qualifying event arrives. An idle or
315
- * backgrounded tab that produces no such events — including one that is only
316
- * emitting session-replay heartbeats, which never trigger rotation on their
317
- * own — keeps the same session id past the cap until activity resumes. This
318
- * is deliberate: it avoids minting "phantom" sessions for dormant tabs. (An
319
- * idle tab with replay on is ultimately bounded by the 30-minute user-idle
320
- * pause — recording stops when no user presence is detected — not by this
321
- * cap.)
322
- *
323
- * Sessions are also bounded by a 30-minute inactivity gap, enforced the same
324
- * lazy way: when a user-interaction event (click, navigation, pageload,
325
- * custom — not background network/log/performance traffic) arrives after 30+
326
- * minutes without one, the session rotates first and replay recording
327
- * restarts for the returning user. Raw user input (scroll, pointer, key,
328
- * touch) also counts as presence even when it produces no SDK event, so
329
- * passive engagement — reading, watching — doesn't end the session. The
330
- * same gap check runs at init, so a reload after a long break also starts a
331
- * fresh session.
332
- *
333
- * The flush is best-effort and the rotation always proceeds — it does not
334
- * wait for or depend on successful delivery, so if that network request
335
- * fails the batch may be lost while the new session id still takes effect.
336
- *
337
- * Defaults to 4 hours when omitted; must be between 1 minute and 8 hours.
338
- * Values outside that range or otherwise invalid fall back to the 4-hour
339
- * default with a `console.warn`.
304
+ * Defaults to 4 hours; must be 1 minute to 8 hours, else falls back with a warning.
340
305
  */
341
306
  sessionMaxDuration?: number;
342
307
  }
@@ -442,7 +407,7 @@ interface SDKOptions extends Omit<SDKOptions$1, "privacy" | "replay" | "enabledE
442
407
  interface SDKConfigInput extends Omit<SDKConfigInput$1, "options"> {
443
408
  options?: Partial<SDKOptions>;
444
409
  }
445
- type SDKConfigUpdate = Partial<Omit<SDKConfigInput, "sessionId" | "user">> & {
410
+ type SDKConfigUpdate = Partial<Omit<SDKConfigInput, "appId" | "sessionId" | "user">> & {
446
411
  user?: SDKConfigInput["user"] | null;
447
412
  };
448
413
 
@@ -470,7 +435,6 @@ declare class SessionManager {
470
435
  setSessionId(newSessionId?: string): Promise<void>;
471
436
  startReplayRecording(): void;
472
437
  stopReplayRecording(): void;
473
- handoffForReinit(): void;
474
438
  destroy(): void;
475
439
  }
476
440
 
@@ -491,11 +455,11 @@ declare global {
491
455
  groundcover?: typeof groundcover;
492
456
  }
493
457
  }
494
- declare function init(params?: {
458
+ declare function init(params: {
495
459
  cluster?: string;
496
460
  environment?: string;
497
461
  namespace?: string;
498
- appId?: string;
462
+ appId: string;
499
463
  releaseId?: string;
500
464
  user?: Partial<UserIdentifiers>;
501
465
  dsn?: string;
package/dist/index.d.ts CHANGED
@@ -4,14 +4,9 @@
4
4
  */
5
5
  type KnownEventType = "dom.event" | "exception" | "network" | "log" | "pageload" | "navigation" | "custom" | "performance" | "replay";
6
6
  /**
7
- * `TType` is what lets a platform package name its own event kinds without
8
- * editing core: React Native can pass `KnownEventType | "tap" | "screen_view"`
9
- * and every core class below flows it through. It defaults to
10
- * `KnownEventType`, so nothing that only deals in the shipped browser kinds
11
- * has to spell it out.
12
- *
13
- * Shared shapes are still shared — the point is that the set of names is open,
14
- * not that each platform reinvents the envelope.
7
+ * `TType` lets a platform name its own event kinds without editing core, and every core class
8
+ * flows it through. Defaults to `KnownEventType`, so callers dealing only in browser kinds need
9
+ * not spell it out. Shared shapes stay shared — the set of NAMES is open, not the envelope.
15
10
  */
16
11
  type BaseEvent<TType extends string = KnownEventType> = {
17
12
  type: TType;
@@ -280,7 +275,6 @@ interface SDKOptions$1<TEvent extends BaseEvent<string> = EventTypes$1[keyof Eve
280
275
  * package's `config/types.ts`).
281
276
  */
282
277
  enabledEvents: string[];
283
- eventSampleRate: number;
284
278
  sessionSampleRate: number;
285
279
  debug: boolean;
286
280
  excludedUrls?: Array<string | RegExp>;
@@ -302,41 +296,12 @@ interface SDKOptions$1<TEvent extends BaseEvent<string> = EventTypes$1[keyof Eve
302
296
  */
303
297
  replay?: unknown;
304
298
  /**
305
- * Target maximum wall-clock duration of a session, in milliseconds.
299
+ * Target maximum wall-clock duration of a session, in ms. Enforced LAZILY on activity, so NOT
300
+ * a hard bound — an idle tab keeps its id past the cap, which deliberately avoids minting
301
+ * phantom sessions. Also bounded by a 30-minute inactivity gap, counting raw input as
302
+ * presence so passive reading does not end a session.
306
303
  *
307
- * Enforced LAZILY, on activity — not by a background timer. Once the cap has
308
- * elapsed, the next user/business event (click, navigation, log, custom,
309
- * network, exception, …) flushes buffered events under the current session
310
- * id, then mints a fresh session id and restarts replay recording if it was
311
- * active.
312
- *
313
- * Because it is activity-gated, this is NOT a hard upper bound: a session
314
- * only rotates when the next qualifying event arrives. An idle or
315
- * backgrounded tab that produces no such events — including one that is only
316
- * emitting session-replay heartbeats, which never trigger rotation on their
317
- * own — keeps the same session id past the cap until activity resumes. This
318
- * is deliberate: it avoids minting "phantom" sessions for dormant tabs. (An
319
- * idle tab with replay on is ultimately bounded by the 30-minute user-idle
320
- * pause — recording stops when no user presence is detected — not by this
321
- * cap.)
322
- *
323
- * Sessions are also bounded by a 30-minute inactivity gap, enforced the same
324
- * lazy way: when a user-interaction event (click, navigation, pageload,
325
- * custom — not background network/log/performance traffic) arrives after 30+
326
- * minutes without one, the session rotates first and replay recording
327
- * restarts for the returning user. Raw user input (scroll, pointer, key,
328
- * touch) also counts as presence even when it produces no SDK event, so
329
- * passive engagement — reading, watching — doesn't end the session. The
330
- * same gap check runs at init, so a reload after a long break also starts a
331
- * fresh session.
332
- *
333
- * The flush is best-effort and the rotation always proceeds — it does not
334
- * wait for or depend on successful delivery, so if that network request
335
- * fails the batch may be lost while the new session id still takes effect.
336
- *
337
- * Defaults to 4 hours when omitted; must be between 1 minute and 8 hours.
338
- * Values outside that range or otherwise invalid fall back to the 4-hour
339
- * default with a `console.warn`.
304
+ * Defaults to 4 hours; must be 1 minute to 8 hours, else falls back with a warning.
340
305
  */
341
306
  sessionMaxDuration?: number;
342
307
  }
@@ -442,7 +407,7 @@ interface SDKOptions extends Omit<SDKOptions$1, "privacy" | "replay" | "enabledE
442
407
  interface SDKConfigInput extends Omit<SDKConfigInput$1, "options"> {
443
408
  options?: Partial<SDKOptions>;
444
409
  }
445
- type SDKConfigUpdate = Partial<Omit<SDKConfigInput, "sessionId" | "user">> & {
410
+ type SDKConfigUpdate = Partial<Omit<SDKConfigInput, "appId" | "sessionId" | "user">> & {
446
411
  user?: SDKConfigInput["user"] | null;
447
412
  };
448
413
 
@@ -470,7 +435,6 @@ declare class SessionManager {
470
435
  setSessionId(newSessionId?: string): Promise<void>;
471
436
  startReplayRecording(): void;
472
437
  stopReplayRecording(): void;
473
- handoffForReinit(): void;
474
438
  destroy(): void;
475
439
  }
476
440
 
@@ -491,11 +455,11 @@ declare global {
491
455
  groundcover?: typeof groundcover;
492
456
  }
493
457
  }
494
- declare function init(params?: {
458
+ declare function init(params: {
495
459
  cluster?: string;
496
460
  environment?: string;
497
461
  namespace?: string;
498
- appId?: string;
462
+ appId: string;
499
463
  releaseId?: string;
500
464
  user?: Partial<UserIdentifiers>;
501
465
  dsn?: string;