@observtech/rum 0.1.32 → 0.1.34

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
@@ -13,8 +13,18 @@ A single `observ.init()` wires all of the following, each stamped with the same
13
13
  - **Session replay** — `@rrweb/record` DOM capture, sent as gzip chunks.
14
14
  - **Traces** — `http.client` spans for `fetch`/`XHR` + page load, over OTLP/HTTP,
15
15
  with W3C `traceparent` propagation (so front and backend traces stitch).
16
+ - **Session lifecycle** — `session.start` / `session.end` events with
17
+ `previous_id` chaining, a 30 min inactivity window + 4 h hard cap, and an
18
+ activity sweep (per-tab `sessionStorage` by default; opt-in cross-tab).
19
+ - **End-user identity** — `enduser.pseudo.id` (anonymous, persistent) on every
20
+ signal, plus `enduser.id` once you call `setUser()`.
16
21
  - **Semantic events** — `click`, `rage_click`, `navigate` as OTel log records.
22
+ - **Page views** — rich `app.page_view` (referrer, time-on-page, navigation type).
17
23
  - **JS errors** — uncaught errors and unhandled promise rejections, observe-only.
24
+ - **Console forwarding** — optionally mirror `console.*` to the logs pipeline.
25
+ - **User-interaction spans** — optional click/submit/keydown spans with
26
+ `ZoneContextManager` (opt-in; pulls in `zone.js`).
27
+ - **Consent gate** — optionally hold ALL telemetry until GDPR analytics consent.
18
28
  - **PII masking** — input values are masked by default (safe-by-default).
19
29
 
20
30
  `observ.init()` is **idempotent** and **never throws**: any setup failure
@@ -45,13 +55,26 @@ observ.init({
45
55
 
46
56
  Main options:
47
57
 
48
- | Option | Type | Default | Role |
49
- | ------------------------------ | ---------------------- | ------- | ---------------------------------------------------------------------- |
50
- | `endpoint` | `string` | — | Base URL of the Observ backend (`/v1/...` paths are appended). |
51
- | `key` | `string` | — | API key sent as `x-observ-key` (empty ⇒ header omitted). |
52
- | `propagateTraceHeaderCorsUrls` | `(string \| RegExp)[]` | `[]` | Cross-origin backends allowed to receive the W3C `traceparent` header. |
53
- | `disableReplay` | `boolean` | `false` | Keep tracing/events but turn off the heavy rrweb replay. |
54
- | `privacy` | `PrivacyOptions` | masked | PII masking of the replay stream (see below). |
58
+ | Option | Type | Default | Role |
59
+ | ------------------------------------------------ | --------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------- |
60
+ | `endpoint` | `string` | — | Base URL of the Observ backend (`/v1/...` paths are appended). |
61
+ | `key` | `string` | — | API key sent as `x-observ-key` (empty ⇒ header omitted). |
62
+ | `propagateTraceHeaderCorsUrls` | `(string \| RegExp)[]` | `[]` | Cross-origin backends allowed to receive the W3C `traceparent` header. |
63
+ | `disableReplay` | `boolean` | `false` | Keep tracing/events but turn off the heavy rrweb replay. |
64
+ | `privacy` | `PrivacyOptions` | masked | PII masking of the replay stream (see below). |
65
+ | `serviceName` / `serviceVersion` / `environment` | `string` | — | Resource attributes (`service.name`, …) stamped on every signal. |
66
+ | `sampleRate` | `number` | `1` | Head-based trace sampling ratio in `[0,1)` (ParentBased). |
67
+ | `propagateBaggage` | `boolean` | `false` | Send `session.id` to allowed backends via the W3C `baggage` header. |
68
+ | `disableMetrics` | `boolean` | `false` | Turn off Core Web Vitals + OTLP metrics. |
69
+ | `sessionInactivityMs` | `number` | 30 min | Inactivity window before the `session.id` rotates. |
70
+ | `sessionMaxDurationMs` | `number` | 4 h | Hard cap on a single session's duration (rotates even while active). |
71
+ | `crossTabSessions` | `boolean` | `false` | Share one session across tabs (`localStorage`) instead of per-tab. |
72
+ | `disablePseudoUser` | `boolean` | `false` | Disable the persistent pseudonymous `enduser.pseudo.id`. |
73
+ | `forwardConsole` | `boolean \| ConsoleLevel[]` | `false` | Mirror `console.*` to the logs pipeline (all levels, or a subset). |
74
+ | `disablePageViews` | `boolean` | `false` | Turn off rich `app.page_view` events. |
75
+ | `userInteraction` | `boolean` | `false` | Interaction spans + `ZoneContextManager` (opt-in; pulls in `zone.js`). |
76
+ | `requireConsent` | `boolean` | `false` | Hold ALL telemetry until analytics consent is present (see below). |
77
+ | `consentKey` / `consentGrantedValues` | `string` / `string[]` | `observ.consent` / `['granted','true','1','yes','all']` | Where/what consent is read from. |
55
78
 
56
79
  ### Privacy / PII masking
57
80
 
@@ -74,6 +97,61 @@ observ.init({
74
97
  })
75
98
  ```
76
99
 
100
+ ### End-user identity
101
+
102
+ Every signal carries a pseudonymous `enduser.pseudo.id` (persisted in
103
+ `localStorage`, anonymous). After sign-in, attach the authenticated identity; clear
104
+ it on sign-out:
105
+
106
+ ```ts
107
+ observ.setUser({ id: user.id, role: user.role, name: user.name }) // → enduser.id/role/name
108
+ observ.clearUser() // on logout (the pseudo id persists)
109
+ ```
110
+
111
+ ### Sessions
112
+
113
+ A `session.id` is shared by every signal. It rotates after 30 min of inactivity or
114
+ a 4 h hard cap, emitting `session.start` (with `session.previous_id` on a
115
+ continuation) and a best-effort `session.end` (with `session.duration_ms`). Sessions
116
+ are **per-tab** (`sessionStorage`) by default — two tabs are two visits, by design.
117
+ Set `crossTabSessions: true` to share one session across a visitor's tabs.
118
+
119
+ ### Console forwarding (optional)
120
+
121
+ ```ts
122
+ observ.init({ endpoint, key, forwardConsole: ['warn', 'error'] }) // or `true` for all levels
123
+ ```
124
+
125
+ Off by default — console output can be noisy and may contain PII. The original
126
+ `console.*` is always still called.
127
+
128
+ ### User-interaction spans (optional)
129
+
130
+ ```ts
131
+ observ.init({ endpoint, key, userInteraction: true })
132
+ ```
133
+
134
+ Turns clicks/submits/keydowns into spans with a `ZoneContextManager` so trace
135
+ context survives the async work they trigger. **Opt-in**: it pulls in `zone.js`,
136
+ which monkey-patches the host's global timers/`Promise` on import — so it is
137
+ dynamically loaded only when enabled (the default bundle and host globals stay
138
+ untouched).
139
+
140
+ ### Consent (GDPR)
141
+
142
+ Hold ALL telemetry until analytics consent is present (no session, no persistent
143
+ id, no network):
144
+
145
+ ```ts
146
+ observ.init({ endpoint, key, requireConsent: true }) // reads cookie/localStorage `observ.consent`
147
+ // …later, from your consent banner:
148
+ observ.grantConsent() // persists consent + starts the waiting SDK
149
+ observ.revokeConsent() // clears consent + shuts the SDK down
150
+ ```
151
+
152
+ The SDK also auto-starts if another tab grants consent (via the `storage` event) or
153
+ the banner writes the key in this tab (a light poll picks it up).
154
+
77
155
  ### Stop (optional)
78
156
 
79
157
  ```ts
@@ -0,0 +1,42 @@
1
+ import { trace, metrics } from '@opentelemetry/api';
2
+ import { logs } from '@opentelemetry/api-logs';
3
+
4
+ // node_modules/@opentelemetry/instrumentation/build/esm/autoLoader.js
5
+
6
+ // node_modules/@opentelemetry/instrumentation/build/esm/autoLoaderUtils.js
7
+ function enableInstrumentations(instrumentations, tracerProvider, meterProvider, loggerProvider) {
8
+ for (let i = 0, j = instrumentations.length; i < j; i++) {
9
+ const instrumentation = instrumentations[i];
10
+ if (tracerProvider) {
11
+ instrumentation.setTracerProvider(tracerProvider);
12
+ }
13
+ if (meterProvider) {
14
+ instrumentation.setMeterProvider(meterProvider);
15
+ }
16
+ if (loggerProvider && instrumentation.setLoggerProvider) {
17
+ instrumentation.setLoggerProvider(loggerProvider);
18
+ }
19
+ if (!instrumentation.getConfig().enabled) {
20
+ instrumentation.enable();
21
+ }
22
+ }
23
+ }
24
+ function disableInstrumentations(instrumentations) {
25
+ instrumentations.forEach((instrumentation) => instrumentation.disable());
26
+ }
27
+
28
+ // node_modules/@opentelemetry/instrumentation/build/esm/autoLoader.js
29
+ function registerInstrumentations(options) {
30
+ const tracerProvider = options.tracerProvider || trace.getTracerProvider();
31
+ const meterProvider = options.meterProvider || metrics.getMeterProvider();
32
+ const loggerProvider = options.loggerProvider || logs.getLoggerProvider();
33
+ const instrumentations = options.instrumentations?.flat() ?? [];
34
+ enableInstrumentations(instrumentations, tracerProvider, meterProvider, loggerProvider);
35
+ return () => {
36
+ disableInstrumentations(instrumentations);
37
+ };
38
+ }
39
+
40
+ export { registerInstrumentations };
41
+ //# sourceMappingURL=chunk-P6VK4P6W.js.map
42
+ //# sourceMappingURL=chunk-P6VK4P6W.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../node_modules/@opentelemetry/instrumentation/src/autoLoaderUtils.ts","../node_modules/@opentelemetry/instrumentation/src/autoLoader.ts"],"names":[],"mappings":";;;;;;AAeM,SAAU,sBAAA,CACd,gBAAA,EACA,cAAA,EACA,aAAA,EACA,cAAA,EAA+B;AAE/B,EAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,iBAAiB,MAAA,EAAQ,CAAA,GAAI,GAAG,CAAA,EAAA,EAAK;AACvD,IAAA,MAAM,eAAA,GAAkB,iBAAiB,CAAC,CAAA;AAC1C,IAAA,IAAI,cAAA,EAAgB;AAClB,MAAA,eAAA,CAAgB,kBAAkB,cAAc,CAAA;;AAElD,IAAA,IAAI,aAAA,EAAe;AACjB,MAAA,eAAA,CAAgB,iBAAiB,aAAa,CAAA;;AAEhD,IAAA,IAAI,cAAA,IAAkB,gBAAgB,iBAAA,EAAmB;AACvD,MAAA,eAAA,CAAgB,kBAAkB,cAAc,CAAA;;AAMlD,IAAA,IAAI,CAAC,eAAA,CAAgB,SAAA,EAAS,CAAG,OAAA,EAAS;AACxC,MAAA,eAAA,CAAgB,MAAA,EAAM;;;AAG5B;AAMM,SAAU,wBACd,gBAAA,EAAmC;AAEnC,EAAA,gBAAA,CAAiB,OAAA,CAAQ,CAAA,eAAA,KAAmB,eAAA,CAAgB,OAAA,EAAS,CAAA;AACvE;;;AC/BM,SAAU,yBACd,OAAA,EAA0B;AAE1B,EAAA,MAAM,cAAA,GAAiB,OAAA,CAAQ,cAAA,IAAkB,KAAA,CAAM,iBAAA,EAAiB;AACxE,EAAA,MAAM,aAAA,GAAgB,OAAA,CAAQ,aAAA,IAAiB,OAAA,CAAQ,gBAAA,EAAgB;AACvE,EAAA,MAAM,cAAA,GAAiB,OAAA,CAAQ,cAAA,IAAkB,IAAA,CAAK,iBAAA,EAAiB;AACvE,EAAA,MAAM,gBAAA,GAAmB,OAAA,CAAQ,gBAAA,EAAkB,IAAA,MAAU,EAAA;AAE7D,EAAA,sBAAA,CACE,gBAAA,EACA,cAAA,EACA,aAAA,EACA,cAAc,CAAA;AAGhB,EAAA,OAAO,MAAK;AACV,IAAA,uBAAA,CAAwB,gBAAgB,CAAA;AAC1C,EAAA,CAAA;AACF","file":"chunk-P6VK4P6W.js","sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { TracerProvider, MeterProvider } from '@opentelemetry/api';\nimport type { Instrumentation } from './types';\nimport type { LoggerProvider } from '@opentelemetry/api-logs';\n\n/**\n * Enable instrumentations\n * @param instrumentations\n * @param tracerProvider\n * @param meterProvider\n */\nexport function enableInstrumentations(\n instrumentations: Instrumentation[],\n tracerProvider?: TracerProvider,\n meterProvider?: MeterProvider,\n loggerProvider?: LoggerProvider\n): void {\n for (let i = 0, j = instrumentations.length; i < j; i++) {\n const instrumentation = instrumentations[i];\n if (tracerProvider) {\n instrumentation.setTracerProvider(tracerProvider);\n }\n if (meterProvider) {\n instrumentation.setMeterProvider(meterProvider);\n }\n if (loggerProvider && instrumentation.setLoggerProvider) {\n instrumentation.setLoggerProvider(loggerProvider);\n }\n // instrumentations have been already enabled during creation\n // so enable only if user prevented that by setting enabled to false\n // this is to prevent double enabling but when calling register all\n // instrumentations should be now enabled\n if (!instrumentation.getConfig().enabled) {\n instrumentation.enable();\n }\n }\n}\n\n/**\n * Disable instrumentations\n * @param instrumentations\n */\nexport function disableInstrumentations(\n instrumentations: Instrumentation[]\n): void {\n instrumentations.forEach(instrumentation => instrumentation.disable());\n}\n","/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { trace, metrics } from '@opentelemetry/api';\nimport { logs } from '@opentelemetry/api-logs';\nimport {\n disableInstrumentations,\n enableInstrumentations,\n} from './autoLoaderUtils';\nimport type { AutoLoaderOptions } from './types_internal';\n\n/**\n * It will register instrumentations and plugins\n * @param options\n * @return returns function to unload instrumentation and plugins that were\n * registered\n */\nexport function registerInstrumentations(\n options: AutoLoaderOptions\n): () => void {\n const tracerProvider = options.tracerProvider || trace.getTracerProvider();\n const meterProvider = options.meterProvider || metrics.getMeterProvider();\n const loggerProvider = options.loggerProvider || logs.getLoggerProvider();\n const instrumentations = options.instrumentations?.flat() ?? [];\n\n enableInstrumentations(\n instrumentations,\n tracerProvider,\n meterProvider,\n loggerProvider\n );\n\n return () => {\n disableInstrumentations(instrumentations);\n };\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,23 @@
1
+ /** Console methods we can forward, in increasing severity. */
2
+ type ConsoleLevel = 'debug' | 'log' | 'info' | 'warn' | 'error';
3
+
4
+ /**
5
+ * Authenticated end-user identity passed to {@link setUser}. `id` is required; the
6
+ * label/role are optional. Numbers are accepted for `id` and stringified.
7
+ */
8
+ interface ObservUser {
9
+ /** Stable application user id → `enduser.id`. */
10
+ id: string | number;
11
+ /** Optional role/permission tag → `enduser.role`. */
12
+ role?: string;
13
+ /** Optional display name → `enduser.name` (avoid storing raw PII if sensitive). */
14
+ name?: string;
15
+ }
16
+ /** Attach the authenticated user to all subsequent telemetry. */
17
+ declare function setUser(user: ObservUser): void;
18
+ /** Clear the authenticated identity (call on sign-out). The pseudo id persists. */
19
+ declare function clearUser(): void;
20
+
1
21
  /**
2
22
  * PII masking / privacy controls for the heavy rrweb replay flux
3
23
  * (Epic 4 — Confidentialité, story 4.1).
@@ -37,6 +57,7 @@ interface PrivacyOptions {
37
57
  */
38
58
  ignoreClass?: string;
39
59
  }
60
+
40
61
  /**
41
62
  * Public option bag for {@link observ.init}.
42
63
  *
@@ -96,6 +117,72 @@ interface ObservInitOptions {
96
117
  * (AlwaysOn). Use under heavy traffic to bound RUM volume.
97
118
  */
98
119
  sampleRate?: number;
120
+ /**
121
+ * Inactivity window (ms) after which the `session.id` rotates. A fresh session
122
+ * is chained to the prior one via `session.previous_id`. Default: 30 min.
123
+ */
124
+ sessionInactivityMs?: number;
125
+ /**
126
+ * Hard cap (ms) on a single session's total duration, regardless of activity —
127
+ * a session older than this rotates even under continuous use. Default: 4 h.
128
+ */
129
+ sessionMaxDurationMs?: number;
130
+ /**
131
+ * Share one session across the visitor's tabs by backing it with `localStorage`
132
+ * + the `storage` event, instead of the default per-tab `sessionStorage`
133
+ * (where two tabs are deliberately two visits). Opt-in: the historical per-tab
134
+ * model is preserved unless this is `true`. Default: `false`.
135
+ */
136
+ crossTabSessions?: boolean;
137
+ /**
138
+ * Disable the pseudonymous, persistent `enduser.pseudo.id` (a random id kept in
139
+ * `localStorage` that ties a visitor's sessions together without authenticated
140
+ * PII). Default: enabled. The authenticated `enduser.id` (via {@link ObservSdk.setUser})
141
+ * is unaffected by this flag.
142
+ */
143
+ disablePseudoUser?: boolean;
144
+ /**
145
+ * Mirror `console.*` calls to the OTLP logs pipeline (each record carries
146
+ * `session.id` + end-user identity, severity-mapped). The original console is
147
+ * always still called. `true` forwards `debug`/`log`/`info`/`warn`/`error`; an
148
+ * array restricts to specific levels (e.g. `['warn', 'error']`). Opt-in
149
+ * (default off) — console output can be noisy and may contain PII.
150
+ */
151
+ forwardConsole?: boolean | ConsoleLevel[];
152
+ /**
153
+ * Disable rich `app.page_view` events (initial load + SPA route changes, with
154
+ * referrer, time-on-previous-page and navigation type). The lightweight
155
+ * `observ.session.navigate` marker is unaffected. Default: page views ON.
156
+ */
157
+ disablePageViews?: boolean;
158
+ /**
159
+ * Turn DOM interactions (click / submit / keydown / change / dblclick) into
160
+ * spans, with a `ZoneContextManager` so trace context survives the async work
161
+ * an interaction triggers (e.g. a fetch fired from a click becomes its child).
162
+ *
163
+ * **Opt-in (default off).** It pulls in `zone.js`, which monkey-patches the
164
+ * host's global timers / `Promise` on import; the SDK won't impose that unless
165
+ * asked. The dependency is dynamically imported, so leaving this off keeps both
166
+ * the bundle and the host globals untouched. Enabling it attaches a microtask
167
+ * after `init()` (the zone.js chunk loads asynchronously).
168
+ */
169
+ userInteraction?: boolean;
170
+ /**
171
+ * Gate ALL telemetry behind analytics consent. When `true`, `init()` does
172
+ * nothing — no session, no persistent id, no providers, no network — until
173
+ * consent is present (a cookie or `localStorage` value written by the site's
174
+ * banner, or {@link ObservSdk.grantConsent}). Default: `false` (telemetry driven
175
+ * solely by operator config, the historical behaviour).
176
+ */
177
+ requireConsent?: boolean;
178
+ /** Cookie / `localStorage` key the consent banner writes. Default `observ.consent`. */
179
+ consentKey?: string;
180
+ /**
181
+ * Stored values that count as "granted" (case-insensitive). Default:
182
+ * `['granted', 'true', '1', 'yes', 'all']`. The first entry is what
183
+ * {@link ObservSdk.grantConsent} persists.
184
+ */
185
+ consentGrantedValues?: string[];
99
186
  }
100
187
  /** Public SDK handle returned/exposed by the package entry point. */
101
188
  interface ObservSdk {
@@ -111,6 +198,25 @@ interface ObservSdk {
111
198
  * flushes on `visibilitychange`/`pagehide` on its own.
112
199
  */
113
200
  shutdown(): Promise<void>;
201
+ /**
202
+ * Attach the authenticated end-user identity to every subsequent span and log
203
+ * (`enduser.id`, + optional `enduser.role`/`enduser.name`). Call after sign-in.
204
+ */
205
+ setUser(user: ObservUser): void;
206
+ /** Clear the authenticated identity (call on sign-out). The pseudo id persists. */
207
+ clearUser(): void;
208
+ /**
209
+ * Grant analytics consent at runtime (e.g. from a consent-banner callback):
210
+ * persists the consent value and immediately starts a `requireConsent` init that
211
+ * was waiting. No-op when nothing is gated. See {@link ObservInitOptions.requireConsent}.
212
+ */
213
+ grantConsent(): void;
214
+ /**
215
+ * Revoke analytics consent: clears the stored consent and shuts the SDK down so
216
+ * it stops emitting (already-sent data cannot be recalled). A later `init()` /
217
+ * {@link ObservSdk.grantConsent} re-arms it.
218
+ */
219
+ revokeConsent(): void;
114
220
  }
115
221
 
116
222
  /**
@@ -143,8 +249,24 @@ declare const SESSION_ID_ATTRIBUTE: 'session.id';
143
249
  * Idempotent (a second call re-registers nothing) and never throws — any setup
144
250
  * failure is swallowed so the SDK can never break the host page; it degrades to
145
251
  * "no tracing / no replay / no semantic events".
252
+ *
253
+ * When `requireConsent` is set, init becomes a GDPR consent barrier: it does
254
+ * nothing (no session, no persistent id, no providers, no network) until consent
255
+ * is present, then auto-starts — watching for a banner that grants consent later
256
+ * (cross-tab via `storage`, same-tab via a light poll) or {@link grantConsent}.
146
257
  */
147
258
  declare function init(options: ObservInitOptions): void;
259
+ /**
260
+ * Grant analytics consent at runtime: persist the consent value and immediately
261
+ * start a `requireConsent` init that was waiting. No-op when nothing is gated.
262
+ */
263
+ declare function grantConsent(): void;
264
+ /**
265
+ * Revoke analytics consent: clear the stored consent (so future inits gate again)
266
+ * and shut the SDK down. Already-sent data cannot be recalled. A later `init()` /
267
+ * {@link grantConsent} re-arms it.
268
+ */
269
+ declare function revokeConsent(): void;
148
270
  /**
149
271
  * Stop replay capture (flushing the residual chunk), the semantic-event tap, and
150
272
  * tear down OTel tracing + logs. Best-effort and never throws (resolves the
@@ -155,4 +277,4 @@ declare function shutdown(): Promise<void>;
155
277
  /** Singleton SDK handle. Usage: `observ.init({ endpoint, key })`. */
156
278
  declare const observ: ObservSdk;
157
279
 
158
- export { type ObservInitOptions, type ObservSdk, SESSION_ID_ATTRIBUTE, observ as default, init, observ, shutdown };
280
+ export { type ObservInitOptions, type ObservSdk, type ObservUser, SESSION_ID_ATTRIBUTE, clearUser, observ as default, grantConsent, init, observ, revokeConsent, setUser, shutdown };