@observtech/rum 0.1.31
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 +94 -0
- package/dist/index.d.ts +131 -0
- package/dist/index.js +675 -0
- package/dist/index.js.map +1 -0
- package/package.json +80 -0
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# `@observtech/rum`
|
|
2
|
+
|
|
3
|
+
Browser **RUM + Session Replay** SDK for Observ — _replay-as-context_. One call
|
|
4
|
+
captures a web session (rrweb replay + OpenTelemetry spans + semantic events +
|
|
5
|
+
JS errors) under a single `session.id`, so the whole session can be read as
|
|
6
|
+
context rather than watched as a video.
|
|
7
|
+
|
|
8
|
+
## What it does
|
|
9
|
+
|
|
10
|
+
A single `observ.init()` wires all of the following, each stamped with the same
|
|
11
|
+
`session.id` so the backend can correlate them:
|
|
12
|
+
|
|
13
|
+
- **Session replay** — `@rrweb/record` DOM capture, sent as gzip chunks.
|
|
14
|
+
- **Traces** — `http.client` spans for `fetch`/`XHR` + page load, over OTLP/HTTP,
|
|
15
|
+
with W3C `traceparent` propagation (so front and backend traces stitch).
|
|
16
|
+
- **Semantic events** — `click`, `rage_click`, `navigate` as OTel log records.
|
|
17
|
+
- **JS errors** — uncaught errors and unhandled promise rejections, observe-only.
|
|
18
|
+
- **PII masking** — input values are masked by default (safe-by-default).
|
|
19
|
+
|
|
20
|
+
`observ.init()` is **idempotent** and **never throws**: any setup failure
|
|
21
|
+
degrades to "no telemetry" instead of breaking the host page.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
yarn add @observtech/rum # or: npm install / pnpm add
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`rrweb` is pulled in as a transitive dependency and resolved by your bundler.
|
|
30
|
+
Requires an evergreen browser (uses native `CompressionStream`, `fetch`,
|
|
31
|
+
`sessionStorage`, `history`).
|
|
32
|
+
|
|
33
|
+
## Configure
|
|
34
|
+
|
|
35
|
+
Call it as early as possible, before the first `fetch`/XHR you want traced:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { observ } from '@observtech/rum'
|
|
39
|
+
|
|
40
|
+
observ.init({
|
|
41
|
+
endpoint: 'https://observ.example.com', // Observ backend base URL
|
|
42
|
+
key: '<api-key>', // sent as the x-observ-key header
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Main options:
|
|
47
|
+
|
|
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). |
|
|
55
|
+
|
|
56
|
+
### Privacy / PII masking
|
|
57
|
+
|
|
58
|
+
The replay records the live DOM, so input values are PII. Masking is **on by
|
|
59
|
+
default**: omit `privacy` entirely and every `<input>`/`<textarea>`/`<select>`
|
|
60
|
+
value is masked. Three CSS classes mark sensitive nodes declaratively:
|
|
61
|
+
|
|
62
|
+
- `observ-mask` — mask the element's text
|
|
63
|
+
- `observ-block` — drop the element from the replay
|
|
64
|
+
- `observ-ignore` — record the element but ignore its input value
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
observ.init({
|
|
68
|
+
endpoint: 'https://observ.example.com',
|
|
69
|
+
key: '<api-key>',
|
|
70
|
+
privacy: {
|
|
71
|
+
maskAllInputs: true, // default; set false only on a surface free of PII
|
|
72
|
+
maskAllText: false, // true masks ALL visible text (degrades the replay)
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Stop (optional)
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
await observ.shutdown() // stops replay (flushing the residual) + tears down OTel
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The SDK already flushes on `visibilitychange`/`pagehide`, so `shutdown()` is
|
|
84
|
+
mainly for SPA teardown or tests.
|
|
85
|
+
|
|
86
|
+
## Documentation
|
|
87
|
+
|
|
88
|
+
- **Full usage & backend (`observ-server`) setup** — endpoints, CORS, API keys,
|
|
89
|
+
session storage, troubleshooting: see the Observ docs
|
|
90
|
+
(`docs/session-replay-rum-sdk.md`).
|
|
91
|
+
- **Building, releasing & maintaining this package**:
|
|
92
|
+
`docs/observ-rum-sdk-maintainers.md`.
|
|
93
|
+
|
|
94
|
+
MIT licensed.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PII masking / privacy controls for the heavy rrweb replay flux
|
|
3
|
+
* (Epic 4 — Confidentialité, story 4.1).
|
|
4
|
+
*
|
|
5
|
+
* SAFE-BY-DEFAULT: omitting `privacy` (or any field) masks all input values in
|
|
6
|
+
* the replay. Architecture D7 / PRD Open Q5 require masking before any real
|
|
7
|
+
* data. Three conventional CSS classes let a host page mark sensitive nodes
|
|
8
|
+
* declaratively; only override a class name if it collides with the host's.
|
|
9
|
+
*/
|
|
10
|
+
interface PrivacyOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Mask the value of every `<input>`/`<textarea>`/`<select>` in the replay
|
|
13
|
+
* (rrweb `maskAllInputs`). Default: `true`. Set `false` ONLY on a surface
|
|
14
|
+
* known to be free of sensitive input.
|
|
15
|
+
*/
|
|
16
|
+
maskAllInputs?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Also mask ALL visible text content in the replay, not just inputs (rrweb
|
|
19
|
+
* `maskTextSelector: '*'`). Default: `false` — full-text masking strongly
|
|
20
|
+
* degrades replay usefulness, so it is opt-in per surface.
|
|
21
|
+
*/
|
|
22
|
+
maskAllText?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* CSS class marking elements whose TEXT must be masked in the replay
|
|
25
|
+
* (rrweb `maskTextClass`). Default: `observ-mask`.
|
|
26
|
+
*/
|
|
27
|
+
maskTextClass?: string;
|
|
28
|
+
/**
|
|
29
|
+
* CSS class marking elements to DROP entirely from the replay — recorded as
|
|
30
|
+
* an empty placeholder (rrweb `blockClass`). Default: `observ-block`.
|
|
31
|
+
*/
|
|
32
|
+
blockClass?: string;
|
|
33
|
+
/**
|
|
34
|
+
* CSS class marking inputs whose user input events are IGNORED — the element
|
|
35
|
+
* is still recorded but its value changes are not (rrweb `ignoreClass`).
|
|
36
|
+
* Default: `observ-ignore`.
|
|
37
|
+
*/
|
|
38
|
+
ignoreClass?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Public option bag for {@link observ.init}.
|
|
42
|
+
*
|
|
43
|
+
* Scaffold story (1.1): only `endpoint`, `key` and `propagateTraceHeaderCorsUrls`
|
|
44
|
+
* are part of the committed public surface. Fields consumed by later stories
|
|
45
|
+
* (session lifecycle, rrweb capture, sampling…) are intentionally NOT declared
|
|
46
|
+
* yet — each story that needs one adds it, so the surface stays honest.
|
|
47
|
+
*/
|
|
48
|
+
interface ObservInitOptions {
|
|
49
|
+
/** Base URL of the Observ backend receiving telemetry, e.g. `https://obs.example.com`. */
|
|
50
|
+
endpoint: string;
|
|
51
|
+
/** API key authenticating the browser against the Observ ingestion endpoints. */
|
|
52
|
+
key: string;
|
|
53
|
+
/**
|
|
54
|
+
* Origins for which OTel may propagate the W3C `traceparent` header on
|
|
55
|
+
* cross-origin fetch/XHR. Required for `session.id ⇄ trace_id` correlation
|
|
56
|
+
* when the backend is not same-origin (see architecture D6 / PRD NFR-5).
|
|
57
|
+
* Wired up in story 1.3; accepted (and ignored) here.
|
|
58
|
+
*/
|
|
59
|
+
propagateTraceHeaderCorsUrls?: (string | RegExp)[];
|
|
60
|
+
/**
|
|
61
|
+
* Disable rrweb session-replay capture (story 2.2). OTel RUM tracing stays on.
|
|
62
|
+
* Use for sensitive surfaces where even masked replay is unwanted; otherwise
|
|
63
|
+
* prefer `privacy` masking (story 4.1), which is on by default. Default:
|
|
64
|
+
* replay enabled.
|
|
65
|
+
*/
|
|
66
|
+
disableReplay?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* PII masking controls for the heavy replay flux (story 4.1). Safe-by-default:
|
|
69
|
+
* omit to mask all input values. See {@link PrivacyOptions}.
|
|
70
|
+
*/
|
|
71
|
+
privacy?: PrivacyOptions;
|
|
72
|
+
}
|
|
73
|
+
/** Public SDK handle returned/exposed by the package entry point. */
|
|
74
|
+
interface ObservSdk {
|
|
75
|
+
/**
|
|
76
|
+
* Initialize the Observ RUM SDK: establishes the `session.id`, wires OTel RUM
|
|
77
|
+
* tracing (story 1.3) and starts rrweb replay capture (story 2.2, unless
|
|
78
|
+
* `disableReplay`). Idempotent and never throws.
|
|
79
|
+
*/
|
|
80
|
+
init(options: ObservInitOptions): void;
|
|
81
|
+
/**
|
|
82
|
+
* Stop replay capture (flushing the residual) and tear down OTel tracing.
|
|
83
|
+
* Best-effort; never throws. Mainly for SPA teardown / tests — the SDK also
|
|
84
|
+
* flushes on `visibilitychange`/`pagehide` on its own.
|
|
85
|
+
*/
|
|
86
|
+
shutdown(): Promise<void>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Attribute key used everywhere to correlate signals of a single session.
|
|
91
|
+
* Sourced from the pinned `@opentelemetry/semantic-conventions` (incubating
|
|
92
|
+
* `ATTR_SESSION_ID`) rather than a hand-typed literal, so the key tracks the
|
|
93
|
+
* standard. Enforcement rule (architecture): the OTel session attribute is
|
|
94
|
+
* EXACTLY `session.id` — never `sessionId` nor `session_id`. Single source of
|
|
95
|
+
* truth so every signal (spans, semantic events, rrweb) stamps the identical key.
|
|
96
|
+
* A test asserts `=== 'session.id'` to guard that invariant across semconv bumps.
|
|
97
|
+
*/
|
|
98
|
+
declare const SESSION_ID_ATTRIBUTE: 'session.id';
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Initialize the Observ RUM SDK.
|
|
102
|
+
*
|
|
103
|
+
* 1. Establishes (or resumes) the visit's `session.id` — the single
|
|
104
|
+
* correlation key shared by every signal (story 1.2).
|
|
105
|
+
* 2. Wires OTel RUM tracing (story 1.3): `http.client` spans for fetch/XHR,
|
|
106
|
+
* carrying `session.id`, with W3C `traceparent` propagation and OTLP/HTTP
|
|
107
|
+
* export to `{endpoint}/v1/traces`.
|
|
108
|
+
* 3. Starts rrweb replay capture (story 2.2): buffered gzip chunks POSTed to
|
|
109
|
+
* `{endpoint}/v1/replay` (size/time triggered), unless `disableReplay`.
|
|
110
|
+
* 4. Derives semantic events (story 2.3): `click` / `rage_click` / `navigate`
|
|
111
|
+
* emitted as OTel log records to `{endpoint}/v1/logs` (independent of
|
|
112
|
+
* `disableReplay`).
|
|
113
|
+
* 5. Captures JS errors (story 2.4): `observ.session.js_error` (uncaught
|
|
114
|
+
* errors + unhandled rejections) on the same logs channel.
|
|
115
|
+
*
|
|
116
|
+
* Idempotent (a second call re-registers nothing) and never throws — any setup
|
|
117
|
+
* failure is swallowed so the SDK can never break the host page; it degrades to
|
|
118
|
+
* "no tracing / no replay / no semantic events".
|
|
119
|
+
*/
|
|
120
|
+
declare function init(options: ObservInitOptions): void;
|
|
121
|
+
/**
|
|
122
|
+
* Stop replay capture (flushing the residual chunk), the semantic-event tap, and
|
|
123
|
+
* tear down OTel tracing + logs. Best-effort and never throws (resolves the
|
|
124
|
+
* story 1.3 "no public teardown" deferral). After this, a later {@link init} can
|
|
125
|
+
* re-start the SDK.
|
|
126
|
+
*/
|
|
127
|
+
declare function shutdown(): Promise<void>;
|
|
128
|
+
/** Singleton SDK handle. Usage: `observ.init({ endpoint, key })`. */
|
|
129
|
+
declare const observ: ObservSdk;
|
|
130
|
+
|
|
131
|
+
export { type ObservInitOptions, type ObservSdk, SESSION_ID_ATTRIBUTE, observ as default, init, observ, shutdown };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
import { ATTR_EXCEPTION_MESSAGE, ATTR_EXCEPTION_TYPE, ATTR_EXCEPTION_STACKTRACE } from '@opentelemetry/semantic-conventions';
|
|
2
|
+
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
|
|
3
|
+
import { LoggerProvider, BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
|
|
4
|
+
import { ATTR_SESSION_ID } from '@opentelemetry/semantic-conventions/incubating';
|
|
5
|
+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
6
|
+
import { trace, metrics } from '@opentelemetry/api';
|
|
7
|
+
import { logs } from '@opentelemetry/api-logs';
|
|
8
|
+
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
|
|
9
|
+
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
|
|
10
|
+
import { WebTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-web';
|
|
11
|
+
import { record } from '@rrweb/record';
|
|
12
|
+
|
|
13
|
+
// src/js-errors.ts
|
|
14
|
+
var SESSION_ID_ATTRIBUTE = ATTR_SESSION_ID;
|
|
15
|
+
|
|
16
|
+
// src/session.ts
|
|
17
|
+
var SESSION_STORAGE_KEY = "observ.rum.session";
|
|
18
|
+
var SESSION_INACTIVITY_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
19
|
+
var current = null;
|
|
20
|
+
function generateSessionId() {
|
|
21
|
+
const c = globalThis.crypto;
|
|
22
|
+
if (c && typeof c.randomUUID === "function") {
|
|
23
|
+
try {
|
|
24
|
+
return c.randomUUID();
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (c && typeof c.getRandomValues === "function") {
|
|
29
|
+
const bytes = c.getRandomValues(new Uint8Array(16));
|
|
30
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | 64;
|
|
31
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
32
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
33
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
34
|
+
}
|
|
35
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
|
|
36
|
+
const r = Math.random() * 16 | 0;
|
|
37
|
+
const v = ch === "x" ? r : r & 3 | 8;
|
|
38
|
+
return v.toString(16);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function safeRead() {
|
|
42
|
+
try {
|
|
43
|
+
return globalThis.sessionStorage?.getItem(SESSION_STORAGE_KEY) ?? null;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function safeWrite(value) {
|
|
49
|
+
try {
|
|
50
|
+
globalThis.sessionStorage?.setItem(SESSION_STORAGE_KEY, value);
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function isPersistedSession(value) {
|
|
55
|
+
if (typeof value !== "object" || value === null) return false;
|
|
56
|
+
const v = value;
|
|
57
|
+
return typeof v.id === "string" && v.id.length > 0 && typeof v.startedAt === "number" && Number.isFinite(v.startedAt) && typeof v.lastActivityAt === "number" && Number.isFinite(v.lastActivityAt);
|
|
58
|
+
}
|
|
59
|
+
function load() {
|
|
60
|
+
if (current) return current;
|
|
61
|
+
const raw = safeRead();
|
|
62
|
+
if (raw === null) return null;
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(raw);
|
|
65
|
+
return isPersistedSession(parsed) ? parsed : null;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function save(session) {
|
|
71
|
+
current = session;
|
|
72
|
+
safeWrite(JSON.stringify(session));
|
|
73
|
+
}
|
|
74
|
+
function ensureSession(now = Date.now()) {
|
|
75
|
+
const existing = load();
|
|
76
|
+
if (existing) {
|
|
77
|
+
const elapsed = Math.max(0, now - existing.lastActivityAt);
|
|
78
|
+
if (elapsed <= SESSION_INACTIVITY_TIMEOUT_MS) {
|
|
79
|
+
save({ ...existing, lastActivityAt: now });
|
|
80
|
+
return existing.id;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const fresh = { id: generateSessionId(), startedAt: now, lastActivityAt: now };
|
|
84
|
+
save(fresh);
|
|
85
|
+
return fresh.id;
|
|
86
|
+
}
|
|
87
|
+
function getSessionId(now = Date.now()) {
|
|
88
|
+
return ensureSession(now);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/otel-logs.ts
|
|
92
|
+
function buildLogsUrl(endpoint) {
|
|
93
|
+
try {
|
|
94
|
+
const url = new URL(endpoint);
|
|
95
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/v1/logs`;
|
|
96
|
+
url.search = "";
|
|
97
|
+
url.hash = "";
|
|
98
|
+
return url.toString();
|
|
99
|
+
} catch {
|
|
100
|
+
return `${endpoint.replace(/\/+$/, "")}/v1/logs`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
var provider = null;
|
|
104
|
+
var activeLogger = null;
|
|
105
|
+
function setupOtelLogs(options) {
|
|
106
|
+
if (activeLogger) return activeLogger;
|
|
107
|
+
const headers = {};
|
|
108
|
+
if (options.key) headers["x-observ-key"] = options.key;
|
|
109
|
+
const exporter = new OTLPLogExporter({ url: buildLogsUrl(options.endpoint), headers });
|
|
110
|
+
const p = new LoggerProvider({ processors: [new BatchLogRecordProcessor(exporter)] });
|
|
111
|
+
try {
|
|
112
|
+
const logger = p.getLogger("@observtech/rum");
|
|
113
|
+
provider = p;
|
|
114
|
+
activeLogger = logger;
|
|
115
|
+
return logger;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
void p.shutdown();
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
var SESSION_ID_REFRESH_MS = 1e3;
|
|
122
|
+
var cachedSessionId = "";
|
|
123
|
+
var cachedSessionIdAt = 0;
|
|
124
|
+
function currentSessionId() {
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
if (!cachedSessionId || now - cachedSessionIdAt >= SESSION_ID_REFRESH_MS) {
|
|
127
|
+
cachedSessionId = getSessionId();
|
|
128
|
+
cachedSessionIdAt = now;
|
|
129
|
+
}
|
|
130
|
+
return cachedSessionId;
|
|
131
|
+
}
|
|
132
|
+
function emitSessionEvent(logger, eventName, attrs = {}) {
|
|
133
|
+
try {
|
|
134
|
+
logger.emit({
|
|
135
|
+
attributes: {
|
|
136
|
+
"event.name": eventName,
|
|
137
|
+
[SESSION_ID_ATTRIBUTE]: currentSessionId(),
|
|
138
|
+
...attrs
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
} catch (err) {
|
|
142
|
+
console.warn("[observ] semantic event emit failed", err);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function shutdownOtelLogs() {
|
|
146
|
+
const p = provider;
|
|
147
|
+
provider = null;
|
|
148
|
+
activeLogger = null;
|
|
149
|
+
await p?.shutdown();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/js-errors.ts
|
|
153
|
+
var MAX_JS_ERRORS = 50;
|
|
154
|
+
var RATE_WINDOW_MS = 6e4;
|
|
155
|
+
var MAX_MESSAGE = 1024;
|
|
156
|
+
var MAX_STACK = 4096;
|
|
157
|
+
function reasonToMessage(reason) {
|
|
158
|
+
if (typeof reason === "string") return reason;
|
|
159
|
+
if (typeof reason === "object" && reason !== null) {
|
|
160
|
+
try {
|
|
161
|
+
return JSON.stringify(reason) ?? String(reason);
|
|
162
|
+
} catch {
|
|
163
|
+
return String(reason);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return String(reason);
|
|
167
|
+
}
|
|
168
|
+
var NOOP = { stop: () => {
|
|
169
|
+
} };
|
|
170
|
+
var active = null;
|
|
171
|
+
function truncate(s, max) {
|
|
172
|
+
return s.length > max ? s.slice(0, max) : s;
|
|
173
|
+
}
|
|
174
|
+
function startJsErrorCapture(options) {
|
|
175
|
+
if (active) return active;
|
|
176
|
+
if (typeof window === "undefined") return NOOP;
|
|
177
|
+
let logger;
|
|
178
|
+
try {
|
|
179
|
+
logger = setupOtelLogs(options);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.warn("[observ] js-errors: logs setup failed", err);
|
|
182
|
+
return NOOP;
|
|
183
|
+
}
|
|
184
|
+
let windowStart = 0;
|
|
185
|
+
let inWindow = 0;
|
|
186
|
+
const emit = (type, message, stack) => {
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
if (now - windowStart >= RATE_WINDOW_MS) {
|
|
189
|
+
windowStart = now;
|
|
190
|
+
inWindow = 0;
|
|
191
|
+
}
|
|
192
|
+
if (inWindow >= MAX_JS_ERRORS) return;
|
|
193
|
+
inWindow++;
|
|
194
|
+
const attrs = {
|
|
195
|
+
[ATTR_EXCEPTION_MESSAGE]: truncate(message, MAX_MESSAGE)
|
|
196
|
+
};
|
|
197
|
+
if (type) attrs[ATTR_EXCEPTION_TYPE] = type;
|
|
198
|
+
if (stack) attrs[ATTR_EXCEPTION_STACKTRACE] = truncate(stack, MAX_STACK);
|
|
199
|
+
emitSessionEvent(logger, "observ.session.js_error", attrs);
|
|
200
|
+
};
|
|
201
|
+
const onError = (e) => {
|
|
202
|
+
try {
|
|
203
|
+
const err = e.error;
|
|
204
|
+
emit(err?.name, err?.message ?? e.message ?? "unknown error", err?.stack ?? void 0);
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const onRejection = (e) => {
|
|
209
|
+
try {
|
|
210
|
+
const reason = e.reason;
|
|
211
|
+
if (reason instanceof Error) {
|
|
212
|
+
emit(reason.name, reason.message, reason.stack ?? void 0);
|
|
213
|
+
} else {
|
|
214
|
+
emit("UnhandledRejection", reasonToMessage(reason), void 0);
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
window.addEventListener("error", onError);
|
|
220
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
221
|
+
const handle = {
|
|
222
|
+
stop() {
|
|
223
|
+
window.removeEventListener("error", onError);
|
|
224
|
+
window.removeEventListener("unhandledrejection", onRejection);
|
|
225
|
+
active = null;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
active = handle;
|
|
229
|
+
return handle;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// node_modules/@opentelemetry/instrumentation/build/esm/autoLoaderUtils.js
|
|
233
|
+
function enableInstrumentations(instrumentations, tracerProvider, meterProvider, loggerProvider) {
|
|
234
|
+
for (let i = 0, j = instrumentations.length; i < j; i++) {
|
|
235
|
+
const instrumentation = instrumentations[i];
|
|
236
|
+
if (tracerProvider) {
|
|
237
|
+
instrumentation.setTracerProvider(tracerProvider);
|
|
238
|
+
}
|
|
239
|
+
if (meterProvider) {
|
|
240
|
+
instrumentation.setMeterProvider(meterProvider);
|
|
241
|
+
}
|
|
242
|
+
if (loggerProvider && instrumentation.setLoggerProvider) {
|
|
243
|
+
instrumentation.setLoggerProvider(loggerProvider);
|
|
244
|
+
}
|
|
245
|
+
if (!instrumentation.getConfig().enabled) {
|
|
246
|
+
instrumentation.enable();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function disableInstrumentations(instrumentations) {
|
|
251
|
+
instrumentations.forEach((instrumentation) => instrumentation.disable());
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// node_modules/@opentelemetry/instrumentation/build/esm/autoLoader.js
|
|
255
|
+
function registerInstrumentations(options) {
|
|
256
|
+
const tracerProvider = options.tracerProvider || trace.getTracerProvider();
|
|
257
|
+
const meterProvider = options.meterProvider || metrics.getMeterProvider();
|
|
258
|
+
const loggerProvider = options.loggerProvider || logs.getLoggerProvider();
|
|
259
|
+
const instrumentations = options.instrumentations?.flat() ?? [];
|
|
260
|
+
enableInstrumentations(instrumentations, tracerProvider, meterProvider, loggerProvider);
|
|
261
|
+
return () => {
|
|
262
|
+
disableInstrumentations(instrumentations);
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function buildTracesUrl(endpoint) {
|
|
266
|
+
try {
|
|
267
|
+
const url = new URL(endpoint);
|
|
268
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/v1/traces`;
|
|
269
|
+
url.search = "";
|
|
270
|
+
url.hash = "";
|
|
271
|
+
return url.toString();
|
|
272
|
+
} catch {
|
|
273
|
+
return `${endpoint.replace(/\/+$/, "")}/v1/traces`;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
var SessionAttributeSpanProcessor = class {
|
|
277
|
+
onStart(span) {
|
|
278
|
+
try {
|
|
279
|
+
span.setAttribute(SESSION_ID_ATTRIBUTE, getSessionId());
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
onEnd(_span) {
|
|
284
|
+
}
|
|
285
|
+
forceFlush() {
|
|
286
|
+
return Promise.resolve();
|
|
287
|
+
}
|
|
288
|
+
shutdown() {
|
|
289
|
+
return Promise.resolve();
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
var provider2 = null;
|
|
293
|
+
var disableInstrumentations2 = null;
|
|
294
|
+
var activeConfig = null;
|
|
295
|
+
function setupOtelRum(options) {
|
|
296
|
+
if (provider2) {
|
|
297
|
+
if (activeConfig && (activeConfig.endpoint !== options.endpoint || activeConfig.key !== options.key)) {
|
|
298
|
+
console.warn(
|
|
299
|
+
"[observ] OTel RUM already initialized; ignoring divergent endpoint/key on this init() (first call wins)."
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const headers = {};
|
|
305
|
+
if (options.key) {
|
|
306
|
+
headers["x-observ-key"] = options.key;
|
|
307
|
+
} else {
|
|
308
|
+
console.warn("[observ] no API key provided; front-end spans will export unauthenticated.");
|
|
309
|
+
}
|
|
310
|
+
const exporter = new OTLPTraceExporter({
|
|
311
|
+
url: buildTracesUrl(options.endpoint),
|
|
312
|
+
headers
|
|
313
|
+
});
|
|
314
|
+
const p = new WebTracerProvider({
|
|
315
|
+
spanProcessors: [new SessionAttributeSpanProcessor(), new BatchSpanProcessor(exporter)]
|
|
316
|
+
});
|
|
317
|
+
try {
|
|
318
|
+
p.register();
|
|
319
|
+
disableInstrumentations2 = registerInstrumentations({
|
|
320
|
+
tracerProvider: p,
|
|
321
|
+
instrumentations: [
|
|
322
|
+
new FetchInstrumentation({
|
|
323
|
+
propagateTraceHeaderCorsUrls: options.propagateTraceHeaderCorsUrls
|
|
324
|
+
}),
|
|
325
|
+
new XMLHttpRequestInstrumentation({
|
|
326
|
+
propagateTraceHeaderCorsUrls: options.propagateTraceHeaderCorsUrls
|
|
327
|
+
})
|
|
328
|
+
]
|
|
329
|
+
});
|
|
330
|
+
} catch (err) {
|
|
331
|
+
disableInstrumentations2?.();
|
|
332
|
+
disableInstrumentations2 = null;
|
|
333
|
+
void p.shutdown();
|
|
334
|
+
throw err;
|
|
335
|
+
}
|
|
336
|
+
provider2 = p;
|
|
337
|
+
activeConfig = { endpoint: options.endpoint, key: options.key };
|
|
338
|
+
}
|
|
339
|
+
async function shutdownOtelRum() {
|
|
340
|
+
const p = provider2;
|
|
341
|
+
try {
|
|
342
|
+
disableInstrumentations2?.();
|
|
343
|
+
} finally {
|
|
344
|
+
disableInstrumentations2 = null;
|
|
345
|
+
provider2 = null;
|
|
346
|
+
activeConfig = null;
|
|
347
|
+
await p?.shutdown();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/privacy.ts
|
|
352
|
+
var DEFAULT_MASK_TEXT_CLASS = "observ-mask";
|
|
353
|
+
var DEFAULT_BLOCK_CLASS = "observ-block";
|
|
354
|
+
var DEFAULT_IGNORE_CLASS = "observ-ignore";
|
|
355
|
+
function resolveReplayMasking(privacy) {
|
|
356
|
+
const resolved = {
|
|
357
|
+
// Safe-by-default: inputs are masked unless the host *explicitly* opts out.
|
|
358
|
+
maskAllInputs: privacy?.maskAllInputs ?? true,
|
|
359
|
+
maskTextClass: privacy?.maskTextClass ?? DEFAULT_MASK_TEXT_CLASS,
|
|
360
|
+
blockClass: privacy?.blockClass ?? DEFAULT_BLOCK_CLASS,
|
|
361
|
+
ignoreClass: privacy?.ignoreClass ?? DEFAULT_IGNORE_CLASS
|
|
362
|
+
};
|
|
363
|
+
if (privacy?.maskAllText) resolved.maskTextSelector = "*";
|
|
364
|
+
return resolved;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/transport.ts
|
|
368
|
+
function buildReplayUrl(endpoint) {
|
|
369
|
+
try {
|
|
370
|
+
const url = new URL(endpoint);
|
|
371
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/v1/replay`;
|
|
372
|
+
url.search = "";
|
|
373
|
+
url.hash = "";
|
|
374
|
+
return url.toString();
|
|
375
|
+
} catch {
|
|
376
|
+
return `${endpoint.replace(/\/+$/, "")}/v1/replay`;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async function gzipJsonl(lines) {
|
|
380
|
+
const data = new TextEncoder().encode(lines.join("\n"));
|
|
381
|
+
const stream = new Blob([data]).stream().pipeThrough(new CompressionStream("gzip"));
|
|
382
|
+
const buf = await new Response(stream).arrayBuffer();
|
|
383
|
+
return new Uint8Array(buf);
|
|
384
|
+
}
|
|
385
|
+
async function postReplayChunk(opts) {
|
|
386
|
+
if (opts.body.length === 0) return;
|
|
387
|
+
const headers = { "content-type": "application/octet-stream" };
|
|
388
|
+
if (opts.key) headers["x-observ-key"] = opts.key;
|
|
389
|
+
const url = `${opts.url}?session_id=${encodeURIComponent(opts.sessionId)}&seq=${opts.seq}`;
|
|
390
|
+
try {
|
|
391
|
+
const res = await fetch(url, {
|
|
392
|
+
method: "POST",
|
|
393
|
+
headers,
|
|
394
|
+
body: opts.body,
|
|
395
|
+
keepalive: opts.keepalive
|
|
396
|
+
});
|
|
397
|
+
if (!res.ok) {
|
|
398
|
+
console.warn(`[observ] replay chunk rejected: HTTP ${res.status}`);
|
|
399
|
+
}
|
|
400
|
+
} catch (err) {
|
|
401
|
+
console.warn("[observ] replay chunk upload failed", err);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/rrweb-record.ts
|
|
406
|
+
var CHUNK_MAX_BYTES = 512 * 1024;
|
|
407
|
+
var CHUNK_INTERVAL_MS = 5e3;
|
|
408
|
+
var CHECKOUT_INTERVAL_MS = 5 * 60 * 1e3;
|
|
409
|
+
var NOOP_RECORDER = { stop: () => Promise.resolve() };
|
|
410
|
+
function startReplayRecording(options) {
|
|
411
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
412
|
+
return NOOP_RECORDER;
|
|
413
|
+
}
|
|
414
|
+
const url = buildReplayUrl(options.endpoint);
|
|
415
|
+
const key = options.key;
|
|
416
|
+
let buffer = [];
|
|
417
|
+
let bufferedBytes = 0;
|
|
418
|
+
let seq = 0;
|
|
419
|
+
let lastSessionId = null;
|
|
420
|
+
let stopped = false;
|
|
421
|
+
let pending = Promise.resolve();
|
|
422
|
+
async function doFlush(keepalive) {
|
|
423
|
+
if (buffer.length === 0) return;
|
|
424
|
+
const lines = buffer;
|
|
425
|
+
buffer = [];
|
|
426
|
+
bufferedBytes = 0;
|
|
427
|
+
try {
|
|
428
|
+
const body = await gzipJsonl(lines);
|
|
429
|
+
const sessionId = getSessionId();
|
|
430
|
+
if (lastSessionId !== null && lastSessionId !== sessionId) seq = 0;
|
|
431
|
+
lastSessionId = sessionId;
|
|
432
|
+
await postReplayChunk({ url, key, sessionId, seq: seq++, body, keepalive });
|
|
433
|
+
} catch (err) {
|
|
434
|
+
console.warn("[observ] replay flush failed", err);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function enqueueFlush(keepalive = false) {
|
|
438
|
+
pending = pending.then(() => doFlush(keepalive)).catch(() => {
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
const emit = (event) => {
|
|
442
|
+
if (stopped) return;
|
|
443
|
+
try {
|
|
444
|
+
const line = JSON.stringify(event);
|
|
445
|
+
buffer.push(line);
|
|
446
|
+
bufferedBytes += line.length;
|
|
447
|
+
if (bufferedBytes >= CHUNK_MAX_BYTES) enqueueFlush();
|
|
448
|
+
} catch {
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
const masking = resolveReplayMasking(options.privacy);
|
|
452
|
+
let stopFn;
|
|
453
|
+
try {
|
|
454
|
+
stopFn = record({ emit, checkoutEveryNms: CHECKOUT_INTERVAL_MS, ...masking }) ?? void 0;
|
|
455
|
+
} catch (err) {
|
|
456
|
+
console.warn("[observ] rrweb recording failed to start", err);
|
|
457
|
+
}
|
|
458
|
+
const interval = setInterval(() => enqueueFlush(), CHUNK_INTERVAL_MS);
|
|
459
|
+
const onVisibility = () => {
|
|
460
|
+
if (document.visibilityState === "hidden") enqueueFlush();
|
|
461
|
+
};
|
|
462
|
+
const onPageHide = () => {
|
|
463
|
+
enqueueFlush(true);
|
|
464
|
+
};
|
|
465
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
466
|
+
window.addEventListener("pagehide", onPageHide);
|
|
467
|
+
return {
|
|
468
|
+
async stop() {
|
|
469
|
+
if (stopped) return;
|
|
470
|
+
stopped = true;
|
|
471
|
+
clearInterval(interval);
|
|
472
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
473
|
+
window.removeEventListener("pagehide", onPageHide);
|
|
474
|
+
try {
|
|
475
|
+
stopFn?.();
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
enqueueFlush();
|
|
479
|
+
await pending;
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/selector.ts
|
|
485
|
+
var MAX_TEXT = 100;
|
|
486
|
+
var MAX_CLASSES = 3;
|
|
487
|
+
var MAX_CLASS_LEN = 30;
|
|
488
|
+
var MAX_DEPTH = 4;
|
|
489
|
+
function cssSelector(el) {
|
|
490
|
+
if (!el || el.nodeType !== 1) return "";
|
|
491
|
+
try {
|
|
492
|
+
const tag = el.localName;
|
|
493
|
+
if (el.id) return `${tag}#${el.id}`;
|
|
494
|
+
const classes = Array.from(el.classList).filter((c) => c.length > 0 && c.length <= MAX_CLASS_LEN).slice(0, MAX_CLASSES);
|
|
495
|
+
if (classes.length > 0) return `${tag}.${classes.join(".")}`;
|
|
496
|
+
const parts = [];
|
|
497
|
+
let node = el;
|
|
498
|
+
let depth = 0;
|
|
499
|
+
while (node && node.nodeType === 1 && depth < MAX_DEPTH) {
|
|
500
|
+
const t = node.localName;
|
|
501
|
+
if (node.id) {
|
|
502
|
+
parts.unshift(`${t}#${node.id}`);
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
const parent = node.parentElement;
|
|
506
|
+
if (parent) {
|
|
507
|
+
const current2 = node;
|
|
508
|
+
const sameTag = Array.from(parent.children).filter((c) => c.tagName === current2.tagName);
|
|
509
|
+
if (sameTag.length > 1) {
|
|
510
|
+
parts.unshift(`${t}:nth-of-type(${sameTag.indexOf(current2) + 1})`);
|
|
511
|
+
} else {
|
|
512
|
+
parts.unshift(t);
|
|
513
|
+
}
|
|
514
|
+
} else {
|
|
515
|
+
parts.unshift(t);
|
|
516
|
+
}
|
|
517
|
+
node = node.parentElement;
|
|
518
|
+
depth++;
|
|
519
|
+
}
|
|
520
|
+
return parts.join(" > ");
|
|
521
|
+
} catch {
|
|
522
|
+
return el.localName ?? "";
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function elementText(el) {
|
|
526
|
+
if (!el) return "";
|
|
527
|
+
try {
|
|
528
|
+
const text = (el.textContent ?? "").trim().replace(/\s+/g, " ");
|
|
529
|
+
return text.length > MAX_TEXT ? text.slice(0, MAX_TEXT) : text;
|
|
530
|
+
} catch {
|
|
531
|
+
return "";
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/semantic-events.ts
|
|
536
|
+
var RAGE_CLICK_WINDOW_MS = 1e3;
|
|
537
|
+
var RAGE_CLICK_THRESHOLD = 3;
|
|
538
|
+
var MAX_TRACKED_SELECTORS = 100;
|
|
539
|
+
var active2 = null;
|
|
540
|
+
var NOOP2 = { stop: () => Promise.resolve() };
|
|
541
|
+
function toElement(target) {
|
|
542
|
+
if (target instanceof Element) return target;
|
|
543
|
+
const node = target;
|
|
544
|
+
return node && node.parentElement ? node.parentElement : null;
|
|
545
|
+
}
|
|
546
|
+
function startSemanticEvents(options) {
|
|
547
|
+
if (active2) return active2;
|
|
548
|
+
if (typeof document === "undefined" || typeof window === "undefined") return NOOP2;
|
|
549
|
+
let logger;
|
|
550
|
+
try {
|
|
551
|
+
logger = setupOtelLogs(options);
|
|
552
|
+
} catch (err) {
|
|
553
|
+
console.warn("[observ] semantic events: logs setup failed", err);
|
|
554
|
+
return NOOP2;
|
|
555
|
+
}
|
|
556
|
+
const clickTimes = /* @__PURE__ */ new Map();
|
|
557
|
+
const onClick = (event) => {
|
|
558
|
+
try {
|
|
559
|
+
const el = toElement(event.target);
|
|
560
|
+
if (!el) return;
|
|
561
|
+
const selector = cssSelector(el);
|
|
562
|
+
emitSessionEvent(logger, "observ.session.click", {
|
|
563
|
+
"element.selector": selector,
|
|
564
|
+
"element.text": elementText(el)
|
|
565
|
+
});
|
|
566
|
+
const now = Date.now();
|
|
567
|
+
const recent = (clickTimes.get(selector) ?? []).filter((t) => now - t < RAGE_CLICK_WINDOW_MS);
|
|
568
|
+
recent.push(now);
|
|
569
|
+
if (recent.length >= RAGE_CLICK_THRESHOLD) {
|
|
570
|
+
emitSessionEvent(logger, "observ.session.rage_click", { "element.selector": selector });
|
|
571
|
+
clickTimes.delete(selector);
|
|
572
|
+
} else {
|
|
573
|
+
if (clickTimes.size > MAX_TRACKED_SELECTORS) clickTimes.clear();
|
|
574
|
+
clickTimes.set(selector, recent);
|
|
575
|
+
}
|
|
576
|
+
} catch {
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
const emitNavigate = () => {
|
|
580
|
+
emitSessionEvent(logger, "observ.session.navigate", { url: location.href });
|
|
581
|
+
};
|
|
582
|
+
const onPopState = () => emitNavigate();
|
|
583
|
+
document.addEventListener("click", onClick, { capture: true });
|
|
584
|
+
window.addEventListener("popstate", onPopState);
|
|
585
|
+
const origPushState = history.pushState;
|
|
586
|
+
const origReplaceState = history.replaceState;
|
|
587
|
+
let historyPatched = false;
|
|
588
|
+
try {
|
|
589
|
+
history.pushState = function(...args) {
|
|
590
|
+
origPushState.apply(this, args);
|
|
591
|
+
emitNavigate();
|
|
592
|
+
};
|
|
593
|
+
history.replaceState = function(...args) {
|
|
594
|
+
origReplaceState.apply(this, args);
|
|
595
|
+
emitNavigate();
|
|
596
|
+
};
|
|
597
|
+
historyPatched = true;
|
|
598
|
+
} catch {
|
|
599
|
+
}
|
|
600
|
+
let stopped = false;
|
|
601
|
+
const handle = {
|
|
602
|
+
async stop() {
|
|
603
|
+
if (stopped) return;
|
|
604
|
+
stopped = true;
|
|
605
|
+
active2 = null;
|
|
606
|
+
document.removeEventListener("click", onClick, { capture: true });
|
|
607
|
+
window.removeEventListener("popstate", onPopState);
|
|
608
|
+
if (historyPatched) {
|
|
609
|
+
try {
|
|
610
|
+
history.pushState = origPushState;
|
|
611
|
+
history.replaceState = origReplaceState;
|
|
612
|
+
} catch {
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
active2 = handle;
|
|
618
|
+
return handle;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/index.ts
|
|
622
|
+
var replayRecorder = null;
|
|
623
|
+
var semanticEvents = null;
|
|
624
|
+
var jsErrors = null;
|
|
625
|
+
function init(options) {
|
|
626
|
+
try {
|
|
627
|
+
ensureSession();
|
|
628
|
+
setupOtelRum(options);
|
|
629
|
+
if (!options.disableReplay && !replayRecorder) {
|
|
630
|
+
replayRecorder = startReplayRecording(options);
|
|
631
|
+
}
|
|
632
|
+
if (!semanticEvents) {
|
|
633
|
+
semanticEvents = startSemanticEvents(options);
|
|
634
|
+
}
|
|
635
|
+
if (!jsErrors) {
|
|
636
|
+
jsErrors = startJsErrorCapture(options);
|
|
637
|
+
}
|
|
638
|
+
} catch (err) {
|
|
639
|
+
console.warn("[observ] RUM setup failed; tracing/replay/events degraded.", err);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
async function shutdown() {
|
|
643
|
+
const recorder = replayRecorder;
|
|
644
|
+
const events = semanticEvents;
|
|
645
|
+
const errors = jsErrors;
|
|
646
|
+
replayRecorder = null;
|
|
647
|
+
semanticEvents = null;
|
|
648
|
+
jsErrors = null;
|
|
649
|
+
try {
|
|
650
|
+
await recorder?.stop();
|
|
651
|
+
} catch {
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
await events?.stop();
|
|
655
|
+
} catch {
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
errors?.stop();
|
|
659
|
+
} catch {
|
|
660
|
+
}
|
|
661
|
+
try {
|
|
662
|
+
await shutdownOtelLogs();
|
|
663
|
+
} catch {
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
await shutdownOtelRum();
|
|
667
|
+
} catch {
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
var observ = { init, shutdown };
|
|
671
|
+
var index_default = observ;
|
|
672
|
+
|
|
673
|
+
export { SESSION_ID_ATTRIBUTE, index_default as default, init, observ, shutdown };
|
|
674
|
+
//# sourceMappingURL=index.js.map
|
|
675
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/constants.ts","../src/session.ts","../src/otel-logs.ts","../src/js-errors.ts","../node_modules/@opentelemetry/instrumentation/src/autoLoaderUtils.ts","../node_modules/@opentelemetry/instrumentation/src/autoLoader.ts","../src/otel-rum.ts","../src/privacy.ts","../src/transport.ts","../src/rrweb-record.ts","../src/selector.ts","../src/semantic-events.ts","../src/index.ts"],"names":["provider","disableInstrumentations","current","active","NOOP"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Cross-cutting constants for the Observ RUM SDK.\n *\n * Kept in a leaf module (no SDK imports beyond semconv constants) so both the\n * public entry point (`index.ts`) and internal modules (`otel-rum.ts`) can share\n * them without a circular import.\n */\nimport { ATTR_SESSION_ID } from '@opentelemetry/semantic-conventions/incubating'\n\n/**\n * Attribute key used everywhere to correlate signals of a single session.\n * Sourced from the pinned `@opentelemetry/semantic-conventions` (incubating\n * `ATTR_SESSION_ID`) rather than a hand-typed literal, so the key tracks the\n * standard. Enforcement rule (architecture): the OTel session attribute is\n * EXACTLY `session.id` — never `sessionId` nor `session_id`. Single source of\n * truth so every signal (spans, semantic events, rrweb) stamps the identical key.\n * A test asserts `=== 'session.id'` to guard that invariant across semconv bumps.\n */\nexport const SESSION_ID_ATTRIBUTE: 'session.id' = ATTR_SESSION_ID\n","/**\n * Session lifecycle for the Observ RUM SDK (FR-1).\n *\n * Produces a single, stable `session.id` per visit and exposes it INTERNALLY as\n * the one source of truth that later modules (`rrweb-record`, `semantic-events`,\n * `otel-rum`) stamp onto every signal. Not part of the public `observ` surface.\n *\n * Authoritative rules (architecture D5):\n * - `session.id` is a UUID v4, generated in the browser,\n * - persisted in `sessionStorage` (per-tab — two tabs are two visits, by design),\n * - rotated after 30 min of inactivity.\n *\n * Rotation is LAZY: there is no background timer — the window is evaluated on the\n * next `ensureSession`/`getSessionId`/`touchSession` call, and a fresh id is\n * minted then if the window has elapsed.\n *\n * This module performs NO network I/O. It must never throw: if `sessionStorage`\n * or `crypto` are unavailable (private mode, disabled storage, SSR / insecure\n * context), it degrades to an in-memory session.id. NOTE: in that degraded mode\n * persistence is lost across full page loads, so each navigation starts a new\n * session (the per-visit guarantee holds only while `sessionStorage` works).\n */\n\n/** Single `sessionStorage` key holding the serialized session state. */\nexport const SESSION_STORAGE_KEY = 'observ.rum.session'\n\n/**\n * Inactivity window after which a new `session.id` is minted (architecture D5,\n * resolves PRD Open Q2). 30 minutes, in milliseconds.\n */\nexport const SESSION_INACTIVITY_TIMEOUT_MS = 30 * 60 * 1000\n\n/** Shape persisted under {@link SESSION_STORAGE_KEY}. Timestamps are epoch ms. */\ninterface PersistedSession {\n id: string\n startedAt: number\n lastActivityAt: number\n}\n\n/**\n * In-memory memoization. Within a single page it is the source of truth (one\n * `init` per SPA load); it is also the sole fallback when storage is unusable.\n */\nlet current: PersistedSession | null = null\n\n/** Build a UUID v4, preferring crypto; falls back gracefully (documented). */\nfunction generateSessionId(): string {\n const c: Crypto | undefined = globalThis.crypto\n // `crypto.randomUUID` requires a secure context (https or localhost). Detection\n // tests presence, not callability — guard the call so a throwing implementation\n // falls through instead of escaping (the module must never throw).\n if (c && typeof c.randomUUID === 'function') {\n try {\n return c.randomUUID()\n } catch {\n // fall through to getRandomValues\n }\n }\n if (c && typeof c.getRandomValues === 'function') {\n const bytes = c.getRandomValues(new Uint8Array(16))\n bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40 // version 4\n bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80 // variant 10\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`\n }\n // Last-resort, non-crypto fallback (SSR / no Web Crypto). Not cryptographically\n // strong — acceptable only because no real entropy source exists here.\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (ch) => {\n const r = (Math.random() * 16) | 0\n const v = ch === 'x' ? r : (r & 0x3) | 0x8\n return v.toString(16)\n })\n}\n\n/** Read from `sessionStorage`, swallowing any access error. */\nfunction safeRead(): string | null {\n try {\n return globalThis.sessionStorage?.getItem(SESSION_STORAGE_KEY) ?? null\n } catch {\n return null\n }\n}\n\n/** Write to `sessionStorage`, swallowing any access/quota error. */\nfunction safeWrite(value: string): void {\n try {\n globalThis.sessionStorage?.setItem(SESSION_STORAGE_KEY, value)\n } catch {\n // In-memory `current` stays authoritative; nothing else to do.\n }\n}\n\n/** Validate the parsed storage payload before trusting it. */\nfunction isPersistedSession(value: unknown): value is PersistedSession {\n if (typeof value !== 'object' || value === null) return false\n const v = value as Record<string, unknown>\n return (\n typeof v.id === 'string' &&\n v.id.length > 0 &&\n typeof v.startedAt === 'number' &&\n Number.isFinite(v.startedAt) &&\n typeof v.lastActivityAt === 'number' &&\n Number.isFinite(v.lastActivityAt)\n )\n}\n\n/** Resolve the current session, from memory first, then storage. */\nfunction load(): PersistedSession | null {\n if (current) return current\n const raw = safeRead()\n if (raw === null) return null\n try {\n const parsed: unknown = JSON.parse(raw)\n return isPersistedSession(parsed) ? parsed : null\n } catch {\n return null // corrupt JSON → treat as absent\n }\n}\n\n/** Memoize in memory and best-effort persist to storage. */\nfunction save(session: PersistedSession): void {\n current = session\n safeWrite(JSON.stringify(session))\n}\n\n/**\n * Ensure a `session.id` exists for this visit and return it.\n *\n * Reuses the stored id when within the inactivity window (refreshing\n * `lastActivityAt`); otherwise mints a fresh session. `now` is injectable for\n * deterministic tests.\n */\nexport function ensureSession(now: number = Date.now()): string {\n const existing = load()\n if (existing) {\n // Clamp to 0 so a backward clock jump (now < lastActivityAt, e.g. NTP/DST)\n // can't yield a negative \"elapsed\" that silently extends the session; the\n // stale future timestamp is then corrected to `now` below.\n const elapsed = Math.max(0, now - existing.lastActivityAt)\n if (elapsed <= SESSION_INACTIVITY_TIMEOUT_MS) {\n save({ ...existing, lastActivityAt: now })\n return existing.id\n }\n }\n const fresh: PersistedSession = { id: generateSessionId(), startedAt: now, lastActivityAt: now }\n save(fresh)\n return fresh.id\n}\n\n/**\n * Return the `session.id` for the current visit.\n *\n * Delegates to {@link ensureSession} so every read enforces the inactivity\n * timeout (rotating to a fresh id once the window has elapsed) — a memoized\n * read must not be able to hand back a session that should have rotated. As a\n * consequence a read also refreshes `lastActivityAt`, i.e. reading the id counts\n * as activity.\n */\nexport function getSessionId(now: number = Date.now()): string {\n return ensureSession(now)\n}\n\n/**\n * Push the inactivity window forward on real activity (rotating if the session\n * has already expired). Wiring to actual interactions arrives in stories 2.x;\n * this is the hook + the write.\n */\nexport function touchSession(now: number = Date.now()): void {\n ensureSession(now)\n}\n\n/**\n * @internal Test-only. Clears the in-memory memoization to simulate a fresh\n * page load / module re-import (storage is left untouched).\n */\nexport function resetSessionState(): void {\n current = null\n}\n","/**\n * OpenTelemetry Logs pipeline for the Observ RUM SDK (story 2.3).\n *\n * Semantic events (`observ.session.*`) are emitted as OTel **log records** and\n * exported over OTLP/HTTP to `{endpoint}/v1/logs` (received by the backend since\n * story 1.4; the session-assembly writer taps them in 1.6). This is the mirror\n * of the trace pipeline in `otel-rum.ts`, on a dedicated, separate provider —\n * the light \"logs\" channel, never the heavy `/v1/replay` channel.\n *\n * `session.id` is stamped once, at emit time, by {@link emitSessionEvent} — the\n * single point through which every semantic event flows (so a separate\n * `LogRecordProcessor` would be redundant; this also keeps the SDK's\n * sdk-logs API surface minimal).\n */\nimport type { Logger } from '@opentelemetry/api-logs'\nimport { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http'\nimport { BatchLogRecordProcessor, LoggerProvider } from '@opentelemetry/sdk-logs'\n\nimport { SESSION_ID_ATTRIBUTE } from './constants.js'\nimport { getSessionId } from './session.js'\nimport type { ObservInitOptions } from './types.js'\n\n/**\n * Join an endpoint base URL with the OTLP/HTTP logs path. Mirror of\n * {@link import('./otel-rum.js').buildTracesUrl}: preserves a path prefix, never\n * doubles a trailing slash, drops query/fragment; relative/empty → same-origin.\n */\nexport function buildLogsUrl(endpoint: string): string {\n try {\n const url = new URL(endpoint)\n url.pathname = `${url.pathname.replace(/\\/+$/, '')}/v1/logs`\n url.search = ''\n url.hash = ''\n return url.toString()\n } catch {\n return `${endpoint.replace(/\\/+$/, '')}/v1/logs`\n }\n}\n\nlet provider: LoggerProvider | null = null\nlet activeLogger: Logger | null = null\n\n/**\n * Wire up the OTel Logs provider and return a `Logger`. Idempotent: a second\n * call returns the already-active logger (first init wins, mirrors\n * `setupOtelRum`). The `x-observ-key` header is omitted when the key is empty\n * (the trace setup already warns about a missing key).\n */\nexport function setupOtelLogs(options: ObservInitOptions): Logger {\n if (activeLogger) return activeLogger\n\n const headers: Record<string, string> = {}\n if (options.key) headers['x-observ-key'] = options.key\n\n const exporter = new OTLPLogExporter({ url: buildLogsUrl(options.endpoint), headers })\n const p = new LoggerProvider({ processors: [new BatchLogRecordProcessor(exporter)] })\n\n try {\n const logger = p.getLogger('@observtech/rum')\n // Commit the module state ONLY once everything succeeded — otherwise a\n // partial init would leak the provider's flush timer and latch the guard.\n provider = p\n activeLogger = logger\n return logger\n } catch (err) {\n // Roll back a partial init (mirrors otel-rum.ts): shut the provider down so\n // its BatchLogRecordProcessor timer can't outlive the failure, and leave the\n // guards null so a later setup can retry.\n void p.shutdown()\n throw err\n }\n}\n\n/** Inactivity refresh throttle for the cached session id (story 1.2 — avoid a\n * synchronous sessionStorage write on every event). */\nconst SESSION_ID_REFRESH_MS = 1_000\nlet cachedSessionId = ''\nlet cachedSessionIdAt = 0\n\n/**\n * Resolve the visit `session.id`, refreshing it (and the session's inactivity\n * window) at most once per second. A click burst therefore costs ≤ 1\n * `getSessionId` (→ ≤ 1 sessionStorage write) per second instead of one per\n * event, while still counting interaction as session activity.\n */\nfunction currentSessionId(): string {\n const now = Date.now()\n if (!cachedSessionId || now - cachedSessionIdAt >= SESSION_ID_REFRESH_MS) {\n cachedSessionId = getSessionId()\n cachedSessionIdAt = now\n }\n return cachedSessionId\n}\n\n/**\n * Emit one semantic event as an OTel log record: `event.name` in the\n * `observ.session.*` namespace + the exact `session.id` correlation attribute\n * (throttled refresh, so an event read also counts as session activity, 1.2) +\n * the caller's attributes. Best-effort: never throws to the host page.\n */\nexport function emitSessionEvent(\n logger: Logger,\n eventName: string,\n attrs: Record<string, string> = {},\n): void {\n try {\n logger.emit({\n attributes: {\n 'event.name': eventName,\n [SESSION_ID_ATTRIBUTE]: currentSessionId(),\n ...attrs,\n },\n })\n } catch (err) {\n console.warn('[observ] semantic event emit failed', err)\n }\n}\n\n/**\n * @internal Flush + shut the logs provider down and reset the idempotence guard\n * so a later setup can re-init. Called by the SDK teardown.\n */\nexport async function shutdownOtelLogs(): Promise<void> {\n const p = provider\n provider = null\n activeLogger = null\n await p?.shutdown()\n}\n\n/** @internal Test-only: the active logger, or `null` when logs are not set up. */\nexport function getActiveLogger(): Logger | null {\n return activeLogger\n}\n","/**\n * JS error capture for the Observ RUM SDK (FR-9, story 2.4).\n *\n * Captures uncaught errors (`window` `error`) and unhandled promise rejections\n * (`unhandledrejection`) and emits them as OTel log records\n * (`observ.session.js_error`) on the SAME logs channel as the semantic events\n * (story 2.3): standard exception semconv attributes + `session.id`. The stack\n * trace is carried as a log/event attribute — never a metric (architecture\n * enforcement).\n *\n * Invariant (1.1/1.2/1.3): never break the host page. Handlers run when the\n * browser is already handling an error, so each is `try/catch`-guarded and we\n * NEVER `preventDefault()` — the site's own error flow (console, other handlers)\n * is left untouched; we only observe.\n */\nimport type { Logger } from '@opentelemetry/api-logs'\nimport {\n ATTR_EXCEPTION_MESSAGE,\n ATTR_EXCEPTION_STACKTRACE,\n ATTR_EXCEPTION_TYPE,\n} from '@opentelemetry/semantic-conventions'\n\nimport { emitSessionEvent, setupOtelLogs } from './otel-logs.js'\nimport type { ObservInitOptions } from './types.js'\n\n/** Max `js_error` events per rolling window — bounds an error storm (AC#5)\n * without going permanently blind: the window resets, so capture self-heals. */\nexport const MAX_JS_ERRORS = 50\n/** Rolling window (ms) for the {@link MAX_JS_ERRORS} rate limit. */\nexport const RATE_WINDOW_MS = 60_000\n/** Truncation bounds so one error can't produce a huge log record. */\nconst MAX_MESSAGE = 1_024\nconst MAX_STACK = 4_096\n\n/** Best-effort string for a rejection reason: Error→message handled by the\n * caller; objects → JSON (so `{code:500}` isn't flattened to \"[object Object]\");\n * everything else → `String`. */\nfunction reasonToMessage(reason: unknown): string {\n if (typeof reason === 'string') return reason\n if (typeof reason === 'object' && reason !== null) {\n try {\n return JSON.stringify(reason) ?? String(reason)\n } catch {\n return String(reason)\n }\n }\n return String(reason)\n}\n\n/** Public handle for the running JS-error capture. */\nexport interface JsErrorHandle {\n /** Remove the global error listeners. Idempotent. The shared logs provider is\n * shut down by `index.shutdown()`, NOT here (two taps share it). */\n stop(): void\n}\n\nconst NOOP: JsErrorHandle = { stop: () => {} }\n\n/** Module-level guard so the capture is self-idempotent. */\nlet active: JsErrorHandle | null = null\n\nfunction truncate(s: string, max: number): string {\n return s.length > max ? s.slice(0, max) : s\n}\n\n/**\n * Start capturing uncaught JS errors + unhandled rejections. Returns a handle\n * whose `stop()` removes the listeners. Never throws; no-op in a non-DOM env.\n * Not gated by `disableReplay` — errors are useful even without replay.\n */\nexport function startJsErrorCapture(options: ObservInitOptions): JsErrorHandle {\n if (active) return active\n if (typeof window === 'undefined') return NOOP\n\n let logger: Logger\n try {\n logger = setupOtelLogs(options) // shared, idempotent — same logger as 2.3\n } catch (err) {\n console.warn('[observ] js-errors: logs setup failed', err)\n return NOOP\n }\n\n // Sliding-window rate limit: at most MAX_JS_ERRORS per RATE_WINDOW_MS. Resets\n // each window so a burst at boot doesn't blind capture for the rest of a long\n // SPA session.\n let windowStart = 0\n let inWindow = 0\n\n const emit = (type: string | undefined, message: string, stack: string | undefined): void => {\n const now = Date.now()\n if (now - windowStart >= RATE_WINDOW_MS) {\n windowStart = now\n inWindow = 0\n }\n if (inWindow >= MAX_JS_ERRORS) return // anti-flood cap (AC#5)\n inWindow++\n const attrs: Record<string, string> = {\n [ATTR_EXCEPTION_MESSAGE]: truncate(message, MAX_MESSAGE),\n }\n if (type) attrs[ATTR_EXCEPTION_TYPE] = type\n if (stack) attrs[ATTR_EXCEPTION_STACKTRACE] = truncate(stack, MAX_STACK)\n emitSessionEvent(logger, 'observ.session.js_error', attrs)\n }\n\n const onError = (e: ErrorEvent): void => {\n try {\n // `e.error` is the Error in modern browsers; cross-origin \"Script error.\"\n // has `e.error === null`, so fall back to `e.message` with no stack.\n const err = e.error as Error | null | undefined\n emit(err?.name, err?.message ?? e.message ?? 'unknown error', err?.stack ?? undefined)\n } catch {\n /* never let the error handler break the host page */\n }\n }\n\n const onRejection = (e: PromiseRejectionEvent): void => {\n try {\n const reason: unknown = e.reason\n if (reason instanceof Error) {\n emit(reason.name, reason.message, reason.stack ?? undefined)\n } else {\n emit('UnhandledRejection', reasonToMessage(reason), undefined)\n }\n } catch {\n /* swallow — observe, never interfere */\n }\n }\n\n // Do NOT use `window.onerror =` (would clobber a host handler). Never call\n // preventDefault — we observe without suppressing the site's error flow.\n window.addEventListener('error', onError)\n window.addEventListener('unhandledrejection', onRejection)\n\n const handle: JsErrorHandle = {\n stop(): void {\n window.removeEventListener('error', onError)\n window.removeEventListener('unhandledrejection', onRejection)\n active = null\n },\n }\n active = handle\n return handle\n}\n","/*\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","/**\n * OpenTelemetry RUM wiring for the Observ SDK (FR-4, story 1.3).\n *\n * Sets up a {@link WebTracerProvider} that:\n * - creates `http.client` spans for browser `fetch`/XHR (auto-instrumentation),\n * - stamps every span with the visit's `session.id` (via {@link SessionAttributeSpanProcessor}),\n * - injects the W3C `traceparent` header on outgoing requests (default\n * trace-context propagator; cross-origin gated by `propagateTraceHeaderCorsUrls`),\n * - exports spans over OTLP/HTTP to `{endpoint}/v1/traces` (BatchSpanProcessor).\n *\n * Like the rest of the SDK, this must never throw out to the host page: the\n * caller ({@link import('./index.js')}) wraps {@link setupOtelRum} defensively.\n *\n * NOTE: until the backend OTLP/HTTP receiver lands (story 1.4), exports will fail\n * at the network layer (CORS / 404). That is expected — span creation, attribute\n * stamping and header propagation are fully exercised regardless.\n */\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'\nimport { registerInstrumentations } from '@opentelemetry/instrumentation'\nimport { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch'\nimport { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request'\nimport {\n BatchSpanProcessor,\n WebTracerProvider,\n type ReadableSpan,\n type Span,\n type SpanProcessor,\n} from '@opentelemetry/sdk-trace-web'\n\nimport { SESSION_ID_ATTRIBUTE } from './constants.js'\nimport { getSessionId } from './session.js'\nimport type { ObservInitOptions } from './types.js'\n\n/**\n * Join an endpoint base URL with the OTLP/HTTP traces path. Uses the `URL` API\n * for absolute endpoints so a path prefix is preserved, a trailing slash never\n * doubles, and any query/fragment (meaningless on the traces endpoint) is\n * dropped — `https://x?a=1` ⇒ `https://x/v1/traces`, not `https://x?a=1/v1/traces`.\n * A relative or empty endpoint falls back to a string join, yielding a\n * same-origin `…/v1/traces` path.\n */\nexport function buildTracesUrl(endpoint: string): string {\n try {\n const url = new URL(endpoint)\n url.pathname = `${url.pathname.replace(/\\/+$/, '')}/v1/traces`\n url.search = ''\n url.hash = ''\n return url.toString()\n } catch {\n return `${endpoint.replace(/\\/+$/, '')}/v1/traces`\n }\n}\n\n/**\n * Span processor whose only job is to stamp `session.id` on every span at start.\n *\n * Using a processor (rather than per-instrumentation `applyCustomAttributesOnSpan`)\n * guarantees ALL spans — fetch, XHR, and any future document-load — carry the\n * attribute through a single code path. It reads the id via {@link getSessionId},\n * which also refreshes the session's inactivity window (story 1.2 decision): a\n * span therefore counts as session activity.\n */\nexport class SessionAttributeSpanProcessor implements SpanProcessor {\n onStart(span: Span): void {\n // onStart runs synchronously inside the host page's own fetch/XHR. The SDK\n // must never throw out to the page, so guard here even though getSessionId()\n // is contractually no-throw: a missing attribute beats a broken host request.\n try {\n span.setAttribute(SESSION_ID_ATTRIBUTE, getSessionId())\n } catch {\n /* swallow — never let span stamping break a host request */\n }\n }\n onEnd(_span: ReadableSpan): void {}\n forceFlush(): Promise<void> {\n return Promise.resolve()\n }\n shutdown(): Promise<void> {\n return Promise.resolve()\n }\n}\n\nlet provider: WebTracerProvider | null = null\nlet disableInstrumentations: (() => void) | null = null\nlet activeConfig: { endpoint: string; key: string } | null = null\n\n/**\n * Wire up OTel RUM tracing. Idempotent: a second call is a no-op while a provider\n * is already active (mirrors the single-`init` contract; first call wins). Not\n * defensive on its own — `init` owns the try/catch so a setup failure never\n * escapes to the page — but it cleans up after itself: a partial init (register\n * or instrumentation throwing, e.g. SSR / no `fetch`/XHR) shuts the provider down\n * and resets the guard before rethrowing, so no `WebTracerProvider`\n * (BatchSpanProcessor timer + global registration) leaks and a later retry works.\n */\nexport function setupOtelRum(options: ObservInitOptions): void {\n if (provider) {\n // First init wins. Warn if a later call diverges (key rotation / env switch)\n // so the dropped config isn't silently swallowed by the idempotence guard.\n if (\n activeConfig &&\n (activeConfig.endpoint !== options.endpoint || activeConfig.key !== options.key)\n ) {\n console.warn(\n '[observ] OTel RUM already initialized; ignoring divergent endpoint/key on this init() (first call wins).',\n )\n }\n return\n }\n\n // API-key auth via the custom `x-observ-key` header — the receiver (story 1.4)\n // must decode the same name and list it in `Access-Control-Allow-Headers`. An\n // empty key would produce a malformed credential, so omit the header (and warn)\n // rather than send a blank one.\n const headers: Record<string, string> = {}\n if (options.key) {\n headers['x-observ-key'] = options.key\n } else {\n console.warn('[observ] no API key provided; front-end spans will export unauthenticated.')\n }\n\n const exporter = new OTLPTraceExporter({\n url: buildTracesUrl(options.endpoint),\n headers,\n })\n\n // SDK 2.x: span processors are constructor-only (`addSpanProcessor` was removed).\n // Order matters — stamp `session.id` before the batch processor exports.\n const p = new WebTracerProvider({\n spanProcessors: [new SessionAttributeSpanProcessor(), new BatchSpanProcessor(exporter)],\n })\n\n try {\n // Default registration: W3C trace-context propagator + web StackContextManager.\n p.register()\n disableInstrumentations = registerInstrumentations({\n tracerProvider: p,\n instrumentations: [\n new FetchInstrumentation({\n propagateTraceHeaderCorsUrls: options.propagateTraceHeaderCorsUrls,\n }),\n new XMLHttpRequestInstrumentation({\n propagateTraceHeaderCorsUrls: options.propagateTraceHeaderCorsUrls,\n }),\n ],\n })\n } catch (err) {\n // Roll back a partial init so nothing leaks and the guard doesn't latch.\n disableInstrumentations?.()\n disableInstrumentations = null\n void p.shutdown()\n throw err\n }\n\n provider = p\n activeConfig = { endpoint: options.endpoint, key: options.key }\n}\n\n/**\n * @internal Test-only. The active provider, or `null` when tracing is not set\n * up. Lets tests assert idempotence (same instance after a second `setupOtelRum`).\n */\nexport function getActiveProvider(): WebTracerProvider | null {\n return provider\n}\n\n/**\n * @internal Tear down instrumentations and the provider, resetting the\n * idempotence guard. Used by tests (and any future explicit teardown). Not on\n * the public {@link import('./types.js').ObservSdk} surface.\n */\nexport async function shutdownOtelRum(): Promise<void> {\n const p = provider\n // Atomic: even if disabling instrumentations throws, still null the guard and\n // shut the provider down so the BatchSpanProcessor timer can't outlive teardown.\n try {\n disableInstrumentations?.()\n } finally {\n disableInstrumentations = null\n provider = null\n activeConfig = null\n await p?.shutdown()\n }\n}\n","/**\n * PII masking / privacy controls for the heavy rrweb replay flux\n * (Epic 4 — Confidentialité, story 4.1).\n *\n * The heavy replay records the live DOM. By default rrweb captures the *values*\n * users type into inputs (passwords aside, which it already masks) and the\n * visible text. On any real-data surface that is PII — architecture decision D7\n * and PRD Open Q5 require masking to be enabled **before** any real data flows.\n * This module is that gate.\n *\n * SAFE-BY-DEFAULT: with no `privacy` config at all, ALL input values are masked\n * in the replay (`maskAllInputs: true`). A surface that is known to be free of\n * sensitive input can opt out with `privacy: { maskAllInputs: false }`.\n *\n * The defaults map the SDK's small `PrivacyOptions` bag onto rrweb's native\n * masking knobs, and expose three conventional CSS classes so a host page can\n * mark sensitive nodes declaratively without writing custom functions:\n * - `observ-mask` → mask the TEXT of the element (rrweb `maskTextClass`)\n * - `observ-block` → drop the element from the replay entirely (`blockClass`)\n * - `observ-ignore` → record the element but ignore its user input (`ignoreClass`)\n *\n * Channel scope: this governs the HEAVY flux only (rrweb → `/v1/replay`). Masking\n * the LIGHT semantic-event text (`element.text` on `observ.session.click`) is a\n * separate concern handled by a later story (4.2); the two channels are never\n * crossed (enforcement rule).\n */\nimport type { PrivacyOptions } from './types.js'\n\n/** Default class marking elements whose TEXT is masked in the replay (rrweb `maskTextClass`). */\nexport const DEFAULT_MASK_TEXT_CLASS = 'observ-mask'\n/** Default class marking elements dropped entirely from the replay (rrweb `blockClass`). */\nexport const DEFAULT_BLOCK_CLASS = 'observ-block'\n/** Default class marking inputs whose user input events are ignored (rrweb `ignoreClass`). */\nexport const DEFAULT_IGNORE_CLASS = 'observ-ignore'\n\n/**\n * The subset of rrweb `recordOptions` that govern masking. Intentionally a flat,\n * fully-resolved shape (no `undefined` for the always-present fields) so the\n * recorder can spread it straight into `record({ ... })` and tests can assert it.\n */\nexport interface ReplayMaskingOptions {\n /** Mask the value of every `<input>`/`<textarea>`/`<select>` in the replay. */\n maskAllInputs: boolean\n /** Mask the text of elements carrying this class. */\n maskTextClass: string\n /** Drop elements carrying this class from the replay. */\n blockClass: string\n /** Ignore user input on elements carrying this class. */\n ignoreClass: string\n /**\n * When `maskAllText` is on, a selector matching every element so rrweb masks\n * ALL text node content. Omitted otherwise (only class-scoped text masking).\n */\n maskTextSelector?: string\n}\n\n/**\n * Resolve the public {@link PrivacyOptions} into the rrweb masking options,\n * applying the safe-by-default policy. Pure (no DOM/global access) so it is\n * trivially unit-testable and reusable from `init()`.\n */\nexport function resolveReplayMasking(privacy?: PrivacyOptions): ReplayMaskingOptions {\n const resolved: ReplayMaskingOptions = {\n // Safe-by-default: inputs are masked unless the host *explicitly* opts out.\n maskAllInputs: privacy?.maskAllInputs ?? true,\n maskTextClass: privacy?.maskTextClass ?? DEFAULT_MASK_TEXT_CLASS,\n blockClass: privacy?.blockClass ?? DEFAULT_BLOCK_CLASS,\n ignoreClass: privacy?.ignoreClass ?? DEFAULT_IGNORE_CLASS,\n }\n // Full-text masking is opt-in: it heavily degrades replay usefulness, so it is\n // off by default and only class-scoped text masking applies. '*' tells rrweb\n // to treat every element as a masked-text element.\n if (privacy?.maskAllText) resolved.maskTextSelector = '*'\n return resolved\n}\n","/**\n * Replay chunk transport for the Observ RUM SDK (FR-2/FR-5, story 2.2).\n *\n * The heavy rrweb flux travels on its OWN channel — `POST /v1/replay` — never\n * through the OTLP exporter (architecture: the two channels are never crossed).\n * The body is the raw gzip bytes of a `.jsonl.gz` chunk; the backend (story 2.1)\n * stores it verbatim and rejects an empty body (400), so we never POST one.\n *\n * Contract frozen by story 2.1:\n * POST {endpoint}/v1/replay?session_id=<id>&seq=<n>\n * headers: x-observ-key: <key>, content-type: application/octet-stream\n * body: gzip bytes (≤ 16 MiB)\n */\n\n/**\n * Join an endpoint base URL with the `/v1/replay` path. Mirror of\n * {@link import('./otel-rum.js').buildTracesUrl}: the `URL` API preserves a path\n * prefix, never doubles a trailing slash, and drops any query/fragment; a\n * relative or empty endpoint falls back to a same-origin path.\n */\nexport function buildReplayUrl(endpoint: string): string {\n try {\n const url = new URL(endpoint)\n url.pathname = `${url.pathname.replace(/\\/+$/, '')}/v1/replay`\n url.search = ''\n url.hash = ''\n return url.toString()\n } catch {\n return `${endpoint.replace(/\\/+$/, '')}/v1/replay`\n }\n}\n\n/**\n * Gzip newline-delimited JSON lines via the native `CompressionStream` — no\n * dependency (resolves the story 1.1 gzip-strategy deferral). Available in every\n * evergreen browser and Node ≥ 18. Uses the `Blob.stream()` → `pipeThrough` →\n * `Response.arrayBuffer()` pipeline so there is no manual writer/reader (no\n * unhandled rejection, no backpressure deadlock). The output is the gzip stream,\n * stored as-is by the backend.\n */\nexport async function gzipJsonl(lines: string[]): Promise<Uint8Array> {\n const data = new TextEncoder().encode(lines.join('\\n'))\n const stream = new Blob([data as BlobPart]).stream().pipeThrough(new CompressionStream('gzip'))\n const buf = await new Response(stream).arrayBuffer()\n return new Uint8Array(buf)\n}\n\n/** Arguments for {@link postReplayChunk}. */\nexport interface PostReplayChunkOptions {\n /** Replay endpoint URL (from {@link buildReplayUrl}). */\n url: string\n /** API key for the `x-observ-key` header (omitted when empty). */\n key: string\n /** Current visit `session.id` (query param, keys the object-store prefix). */\n sessionId: string\n /** Monotonic chunk sequence within the session (query param). */\n seq: number\n /** Gzip bytes of the chunk. An empty body is never sent (server returns 400). */\n body: Uint8Array\n /** Use `fetch` keepalive for an unload flush (body capped ~64 KiB by the spec). */\n keepalive?: boolean\n}\n\n/**\n * POST one gzip replay chunk. **Best-effort: never throws** — a network failure\n * degrades to \"this chunk is lost\", it must not break the host page.\n */\nexport async function postReplayChunk(opts: PostReplayChunkOptions): Promise<void> {\n if (opts.body.length === 0) return // backend rejects an empty body (400)\n\n const headers: Record<string, string> = { 'content-type': 'application/octet-stream' }\n // Mirror otel-rum.ts: a blank key would be a malformed credential — omit it.\n if (opts.key) headers['x-observ-key'] = opts.key\n\n const url = `${opts.url}?session_id=${encodeURIComponent(opts.sessionId)}&seq=${opts.seq}`\n try {\n const res = await fetch(url, {\n method: 'POST',\n headers,\n body: opts.body as BodyInit,\n keepalive: opts.keepalive,\n })\n // `fetch` only rejects on network failure — surface server rejections\n // (400 empty / 401 auth / 413 over the 16 MiB cap) instead of silent success.\n if (!res.ok) {\n console.warn(`[observ] replay chunk rejected: HTTP ${res.status}`)\n }\n } catch (err) {\n console.warn('[observ] replay chunk upload failed', err)\n }\n}\n","/**\n * rrweb recording + chunked replay upload for the Observ RUM SDK (FR-2, story 2.2).\n *\n * Captures the visual session with `@rrweb/record` (initial FullSnapshot + DOM\n * mutations), buffers events in memory, and POSTs gzip chunks to `/v1/replay`\n * (story 2.1) — triggered by SIZE or TIME, whichever comes first. The heavy flux\n * uses this channel exclusively, never the OTLP exporter.\n *\n * Invariant (1.1/1.2/1.3): the SDK must NEVER break the host page. Every path —\n * capture, serialize, gzip, upload — is defensive: a failure degrades to \"no\n * replay\", never an exception that escapes to the page.\n *\n * Flushes are SERIALIZED through a promise chain (`pending`): every trigger is\n * queued — never dropped — so the buffer cannot grow unbounded behind an\n * in-flight flush, `seq` is assigned in order, and two uploads never overlap.\n *\n * Session activity (1.2 deferral): `getSessionId` is read once per flush (~5 s),\n * NOT per rrweb event — touching the session on every high-frequency mutation\n * would thrash `sessionStorage` and jank the main thread.\n */\nimport { record } from '@rrweb/record'\n\nimport { resolveReplayMasking } from './privacy.js'\nimport { getSessionId } from './session.js'\nimport { buildReplayUrl, gzipJsonl, postReplayChunk } from './transport.js'\nimport type { ObservInitOptions } from './types.js'\n\n/** Serialized buffer size (pre-gzip) that triggers a size-based flush. */\nexport const CHUNK_MAX_BYTES = 512 * 1024 // ~512 KiB — well under the 16 MiB cap\n/** Interval (ms) for the time-based flush. */\nexport const CHUNK_INTERVAL_MS = 5_000\n/** rrweb full-snapshot cadence so a reader can resync from a checkpoint. */\nexport const CHECKOUT_INTERVAL_MS = 5 * 60 * 1_000\n\n/** Public handle for the running recorder. */\nexport interface ReplayRecorder {\n /** Stop capture, remove listeners, and flush the residual. Idempotent. */\n stop(): Promise<void>\n}\n\n/** No-op recorder returned in a non-DOM environment (SSR) — nothing runs. */\nconst NOOP_RECORDER: ReplayRecorder = { stop: () => Promise.resolve() }\n\n/**\n * Start rrweb capture + chunked upload. Returns a handle whose `stop()` tears\n * everything down and flushes the tail. Never throws: a capture/start failure is\n * swallowed (the page keeps working, just without replay).\n */\nexport function startReplayRecording(options: ObservInitOptions): ReplayRecorder {\n // SSR / non-DOM guard: rrweb and the lifecycle listeners need `document` /\n // `window`. Degrade to a no-op recorder (mirrors session.ts's globalThis-\n // defensive style) rather than throwing a ReferenceError / leaking a timer.\n if (typeof document === 'undefined' || typeof window === 'undefined') {\n return NOOP_RECORDER\n }\n\n const url = buildReplayUrl(options.endpoint)\n const key = options.key\n\n let buffer: string[] = []\n let bufferedBytes = 0\n let seq = 0\n let lastSessionId: string | null = null\n let stopped = false\n // Flush chain: each trigger appends to this promise so flushes run one after\n // another (never concurrently, never dropped). Kept reject-free by the inner\n // try/catch + the trailing `.catch`.\n let pending: Promise<void> = Promise.resolve()\n\n /** Serialize → gzip → upload the current buffer. Swapped before the first await. */\n async function doFlush(keepalive: boolean): Promise<void> {\n if (buffer.length === 0) return\n const lines = buffer\n buffer = []\n bufferedBytes = 0\n try {\n const body = await gzipJsonl(lines)\n const sessionId = getSessionId()\n // The session id rotated (idle tab past the 30-min window): restart `seq`\n // so the new object-store prefix begins at 0 (its own checkpoint).\n if (lastSessionId !== null && lastSessionId !== sessionId) seq = 0\n lastSessionId = sessionId\n await postReplayChunk({ url, key, sessionId, seq: seq++, body, keepalive })\n } catch (err) {\n console.warn('[observ] replay flush failed', err)\n }\n }\n\n /** Queue a flush after any in-flight one (never dropped). */\n function enqueueFlush(keepalive = false): void {\n pending = pending.then(() => doFlush(keepalive)).catch(() => {})\n }\n\n const emit = (event: unknown): void => {\n if (stopped) return // drop trailing events after teardown\n try {\n const line = JSON.stringify(event)\n buffer.push(line)\n bufferedBytes += line.length\n if (bufferedBytes >= CHUNK_MAX_BYTES) enqueueFlush()\n } catch {\n /* never let capture break the host page */\n }\n }\n\n // PII masking (story 4.1): safe-by-default — input values are masked unless\n // the host explicitly opts out via `privacy.maskAllInputs: false`. Resolved\n // once here and spread into the recorder config so the heavy replay flux is\n // privacy-safe by construction (architecture D7 / PRD Open Q5).\n const masking = resolveReplayMasking(options.privacy)\n\n let stopFn: (() => void) | undefined\n try {\n // `record` returns the stop handler (or undefined if unsupported).\n stopFn = record({ emit, checkoutEveryNms: CHECKOUT_INTERVAL_MS, ...masking }) ?? undefined\n } catch (err) {\n console.warn('[observ] rrweb recording failed to start', err)\n }\n\n const interval = setInterval(() => enqueueFlush(), CHUNK_INTERVAL_MS)\n\n // Lifecycle flush (1.3 deferral). visibilitychange→hidden fires on tab\n // switch / minimize / most mobile navigations while the page is still alive;\n // pagehide is the best-effort last resort (keepalive — a body the browser\n // can't keepalive-send is rejected and logged, not silently pre-dropped).\n const onVisibility = (): void => {\n if (document.visibilityState === 'hidden') enqueueFlush()\n }\n const onPageHide = (): void => {\n enqueueFlush(true)\n }\n document.addEventListener('visibilitychange', onVisibility)\n window.addEventListener('pagehide', onPageHide)\n\n return {\n async stop(): Promise<void> {\n if (stopped) return\n stopped = true\n clearInterval(interval)\n document.removeEventListener('visibilitychange', onVisibility)\n window.removeEventListener('pagehide', onPageHide)\n try {\n stopFn?.()\n } catch {\n /* swallow — teardown must not throw */\n }\n enqueueFlush() // final residual\n await pending // wait for the whole chain (incl. the final flush) to settle\n },\n }\n}\n","/**\n * Minimal, defensive CSS-selector + text derivation for semantic events\n * (story 2.3). Produces a short, reasonably stable selector for a clicked\n * element and a truncated text label. Never throws (the SDK must never break\n * the host page); degrades to the tag name or `''`.\n */\n\nconst MAX_TEXT = 100\nconst MAX_CLASSES = 3\nconst MAX_CLASS_LEN = 30\nconst MAX_DEPTH = 4\n\n/**\n * Build a short CSS selector for `el`:\n * - `tag#id` when the element has an id,\n * - else `tag.class1.class2` (≤ 3 short classes),\n * - else a depth-bounded `tag:nth-of-type(n)` ancestor path (≤ 4 levels).\n * Returns `''` for a non-element / null, or the bare tag name on any failure.\n */\nexport function cssSelector(el: Element | null): string {\n if (!el || el.nodeType !== 1) return ''\n try {\n // `localName` is lowercase for HTML but case-preserved for SVG\n // (`linearGradient`, `clipPath`) — `tagName.toLowerCase()` would corrupt those.\n const tag = el.localName\n if (el.id) return `${tag}#${el.id}`\n\n const classes = Array.from(el.classList)\n .filter((c) => c.length > 0 && c.length <= MAX_CLASS_LEN)\n .slice(0, MAX_CLASSES)\n if (classes.length > 0) return `${tag}.${classes.join('.')}`\n\n // Depth-bounded structural path.\n const parts: string[] = []\n let node: Element | null = el\n let depth = 0\n while (node && node.nodeType === 1 && depth < MAX_DEPTH) {\n const t = node.localName\n if (node.id) {\n parts.unshift(`${t}#${node.id}`)\n break\n }\n const parent: Element | null = node.parentElement\n if (parent) {\n const current = node\n const sameTag = Array.from(parent.children).filter((c) => c.tagName === current.tagName)\n if (sameTag.length > 1) {\n parts.unshift(`${t}:nth-of-type(${sameTag.indexOf(current) + 1})`)\n } else {\n parts.unshift(t)\n }\n } else {\n parts.unshift(t)\n }\n node = node.parentElement\n depth++\n }\n return parts.join(' > ')\n } catch {\n return el.localName ?? ''\n }\n}\n\n/** Trimmed, whitespace-collapsed, truncated `textContent` of `el` (≤ 100 chars). */\nexport function elementText(el: Element | null): string {\n if (!el) return ''\n try {\n const text = (el.textContent ?? '').trim().replace(/\\s+/g, ' ')\n return text.length > MAX_TEXT ? text.slice(0, MAX_TEXT) : text\n } catch {\n return ''\n }\n}\n","/**\n * Semantic event derivation for the Observ RUM SDK (FR-3 / D8, story 2.3).\n *\n * Derives high-value interaction facts from native DOM listeners and emits them\n * as OTel log records (`observ.session.*`) on the logs channel (`/v1/logs`):\n * - `observ.session.click` — every click, with `element.selector` / `element.text`\n * - `observ.session.rage_click` — ≥ 3 clicks < 1 s on the SAME selector\n * - `observ.session.navigate` — SPA navigation (`pushState`/`replaceState`/`popstate`), `url`\n *\n * Variance vs architecture D8 (\"tap rrweb emit\"): we use native DOM listeners\n * rather than reverse-mapping rrweb node ids — `element.selector`/`element.text`\n * are derived directly and reliably from `event.target`. Same OTel-logs output.\n *\n * Invariant (1.1/1.2/1.3): never break the host page. Listeners run in the\n * page's own context, so every handler is `try/catch`-guarded; the patched\n * `history` methods are restored on `stop()` (no leaked global).\n */\nimport type { Logger } from '@opentelemetry/api-logs'\n\nimport { emitSessionEvent, setupOtelLogs } from './otel-logs.js'\nimport { cssSelector, elementText } from './selector.js'\nimport type { ObservInitOptions } from './types.js'\n\n/** Rage-click window (ms) and click threshold (architecture D8 / AC#2). */\nexport const RAGE_CLICK_WINDOW_MS = 1_000\nexport const RAGE_CLICK_THRESHOLD = 3\n/** Cap on tracked selectors so the rage-click map can't grow unbounded over a\n * long session (cleared past this — rage detection is best-effort). */\nconst MAX_TRACKED_SELECTORS = 100\n\n/** Module-level guard so the tap is self-idempotent (a second start without a\n * stop() between would double-patch `history` / double-add listeners). */\nlet active: SemanticEventsHandle | null = null\n\n/** Public handle for the running semantic-event tap. */\nexport interface SemanticEventsHandle {\n /** Remove listeners, restore patched `history`, shut the logs provider down. */\n stop(): Promise<void>\n}\n\nconst NOOP: SemanticEventsHandle = { stop: () => Promise.resolve() }\n\n/** Resolve an `EventTarget` to the nearest `Element` (text/SVG nodes → parent). */\nfunction toElement(target: EventTarget | null): Element | null {\n if (target instanceof Element) return target\n const node = target as Node | null\n return node && node.parentElement ? node.parentElement : null\n}\n\n/**\n * Start deriving + emitting semantic events. Returns a handle whose `stop()`\n * tears everything down. Never throws; degrades to a no-op in a non-DOM env.\n * NOTE: not gated by `disableReplay` — semantic events are useful even without\n * the heavy replay capture.\n */\nexport function startSemanticEvents(options: ObservInitOptions): SemanticEventsHandle {\n if (active) return active\n if (typeof document === 'undefined' || typeof window === 'undefined') return NOOP\n\n let logger: Logger\n try {\n logger = setupOtelLogs(options)\n } catch (err) {\n console.warn('[observ] semantic events: logs setup failed', err)\n return NOOP\n }\n\n // Recent click timestamps per selector, for rage-click clustering.\n const clickTimes = new Map<string, number[]>()\n\n const onClick = (event: Event): void => {\n try {\n const el = toElement(event.target)\n if (!el) return\n const selector = cssSelector(el)\n emitSessionEvent(logger, 'observ.session.click', {\n 'element.selector': selector,\n 'element.text': elementText(el),\n })\n\n // Rage-click: ≥ THRESHOLD clicks within WINDOW on the same selector.\n const now = Date.now()\n const recent = (clickTimes.get(selector) ?? []).filter((t) => now - t < RAGE_CLICK_WINDOW_MS)\n recent.push(now)\n if (recent.length >= RAGE_CLICK_THRESHOLD) {\n emitSessionEvent(logger, 'observ.session.rage_click', { 'element.selector': selector })\n clickTimes.delete(selector) // reset the burst (one rage_click per burst)\n } else {\n // Bound the map: a selector clicked once never reaches the threshold and\n // would otherwise linger forever. Clear past the cap (rare; best-effort).\n if (clickTimes.size > MAX_TRACKED_SELECTORS) clickTimes.clear()\n clickTimes.set(selector, recent)\n }\n } catch {\n /* never let a click handler break the host page */\n }\n }\n\n const emitNavigate = (): void => {\n emitSessionEvent(logger, 'observ.session.navigate', { url: location.href })\n }\n const onPopState = (): void => emitNavigate()\n\n document.addEventListener('click', onClick, { capture: true })\n window.addEventListener('popstate', onPopState)\n\n // No native event fires for pushState/replaceState — patch them (and restore\n // on stop, so the global is never left monkey-patched).\n const origPushState = history.pushState\n const origReplaceState = history.replaceState\n let historyPatched = false\n try {\n history.pushState = function (this: History, ...args: Parameters<History['pushState']>): void {\n origPushState.apply(this, args)\n emitNavigate()\n }\n history.replaceState = function (\n this: History,\n ...args: Parameters<History['replaceState']>\n ): void {\n origReplaceState.apply(this, args)\n emitNavigate()\n }\n historyPatched = true\n } catch {\n /* leave history unpatched — popstate still works */\n }\n\n let stopped = false\n const handle: SemanticEventsHandle = {\n async stop(): Promise<void> {\n if (stopped) return\n stopped = true\n active = null\n document.removeEventListener('click', onClick, { capture: true })\n window.removeEventListener('popstate', onPopState)\n if (historyPatched) {\n try {\n history.pushState = origPushState\n history.replaceState = origReplaceState\n } catch {\n /* swallow — teardown must not throw */\n }\n }\n // The shared logs provider is shut down once by index.shutdown() (story\n // 2.4) — not here, since js-errors shares it.\n },\n }\n active = handle\n return handle\n}\n","import { startJsErrorCapture, type JsErrorHandle } from './js-errors.js'\nimport { shutdownOtelLogs } from './otel-logs.js'\nimport { setupOtelRum, shutdownOtelRum } from './otel-rum.js'\nimport { startReplayRecording, type ReplayRecorder } from './rrweb-record.js'\nimport { startSemanticEvents, type SemanticEventsHandle } from './semantic-events.js'\nimport { ensureSession } from './session.js'\nimport type { ObservInitOptions, ObservSdk } from './types.js'\n\nexport type { ObservInitOptions, ObservSdk } from './types.js'\n\n/**\n * Attribute key used everywhere to correlate signals of a single session.\n * Re-exported from {@link ./constants.js} (the leaf module that internal modules\n * also import, avoiding an `index ↔ otel-rum` cycle). EXACTLY `session.id`.\n */\nexport { SESSION_ID_ATTRIBUTE } from './constants.js'\n\n/** Singleton replay recorder handle (one capture per page, like the OTel provider). */\nlet replayRecorder: ReplayRecorder | null = null\n/** Singleton semantic-events tap (story 2.3). */\nlet semanticEvents: SemanticEventsHandle | null = null\n/** Singleton JS-error capture (story 2.4). */\nlet jsErrors: JsErrorHandle | null = null\n\n/**\n * Initialize the Observ RUM SDK.\n *\n * 1. Establishes (or resumes) the visit's `session.id` — the single\n * correlation key shared by every signal (story 1.2).\n * 2. Wires OTel RUM tracing (story 1.3): `http.client` spans for fetch/XHR,\n * carrying `session.id`, with W3C `traceparent` propagation and OTLP/HTTP\n * export to `{endpoint}/v1/traces`.\n * 3. Starts rrweb replay capture (story 2.2): buffered gzip chunks POSTed to\n * `{endpoint}/v1/replay` (size/time triggered), unless `disableReplay`.\n * 4. Derives semantic events (story 2.3): `click` / `rage_click` / `navigate`\n * emitted as OTel log records to `{endpoint}/v1/logs` (independent of\n * `disableReplay`).\n * 5. Captures JS errors (story 2.4): `observ.session.js_error` (uncaught\n * errors + unhandled rejections) on the same logs channel.\n *\n * Idempotent (a second call re-registers nothing) and never throws — any setup\n * failure is swallowed so the SDK can never break the host page; it degrades to\n * \"no tracing / no replay / no semantic events\".\n */\nexport function init(options: ObservInitOptions): void {\n try {\n ensureSession()\n setupOtelRum(options)\n if (!options.disableReplay && !replayRecorder) {\n replayRecorder = startReplayRecording(options)\n }\n // Semantic events + JS errors are useful even without replay → not gated.\n if (!semanticEvents) {\n semanticEvents = startSemanticEvents(options)\n }\n if (!jsErrors) {\n jsErrors = startJsErrorCapture(options)\n }\n } catch (err) {\n // The SDK must never break the host page (1.1/1.2 guarantee). Degrade to\n // \"no tracing / no replay / no semantic events\" rather than propagating.\n console.warn('[observ] RUM setup failed; tracing/replay/events degraded.', err)\n }\n}\n\n/**\n * Stop replay capture (flushing the residual chunk), the semantic-event tap, and\n * tear down OTel tracing + logs. Best-effort and never throws (resolves the\n * story 1.3 \"no public teardown\" deferral). After this, a later {@link init} can\n * re-start the SDK.\n */\nexport async function shutdown(): Promise<void> {\n const recorder = replayRecorder\n const events = semanticEvents\n const errors = jsErrors\n replayRecorder = null\n semanticEvents = null\n jsErrors = null\n try {\n await recorder?.stop()\n } catch {\n /* swallow — teardown must never throw to the host page */\n }\n try {\n await events?.stop()\n } catch {\n /* swallow */\n }\n try {\n errors?.stop()\n } catch {\n /* swallow */\n }\n // Both the semantic-events tap and the JS-error capture share ONE logs\n // provider — shut it down once, here, after both taps have stopped. Each\n // provider shutdown is guarded so a flush rejection can't skip the next one\n // or escape to the host page (shutdown must never throw — 1.3 guarantee).\n try {\n await shutdownOtelLogs()\n } catch {\n /* swallow */\n }\n try {\n await shutdownOtelRum()\n } catch {\n /* swallow */\n }\n}\n\n/** Singleton SDK handle. Usage: `observ.init({ endpoint, key })`. */\nexport const observ: ObservSdk = { init, shutdown }\n\nexport default observ\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@observtech/rum",
|
|
3
|
+
"version": "0.1.31",
|
|
4
|
+
"description": "Observ browser RUM + Session Replay SDK (rrweb + OpenTelemetry, replay-as-context).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Observ",
|
|
7
|
+
"homepage": "https://github.com/wmotr/rust-backend-otel/tree/main/packages/observ-rum#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/wmotr/rust-backend-otel.git",
|
|
11
|
+
"directory": "packages/observ-rum"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/wmotr/rust-backend-otel/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"rum",
|
|
18
|
+
"session-replay",
|
|
19
|
+
"rrweb",
|
|
20
|
+
"opentelemetry",
|
|
21
|
+
"otel",
|
|
22
|
+
"observability",
|
|
23
|
+
"real-user-monitoring",
|
|
24
|
+
"browser"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"module": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.js",
|
|
35
|
+
"default": "./dist/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist"
|
|
43
|
+
],
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsup",
|
|
49
|
+
"prepack": "tsup",
|
|
50
|
+
"dev": "tsup --watch",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"test:watch": "vitest",
|
|
54
|
+
"format": "prettier --write \"src/**/*.{ts,json}\" \"*.{ts,json,md}\"",
|
|
55
|
+
"format:check": "prettier --check \"src/**/*.{ts,json}\" \"*.{ts,json,md}\""
|
|
56
|
+
},
|
|
57
|
+
"packageManager": "yarn@4.13.0",
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "25.9.2",
|
|
60
|
+
"happy-dom": "20.10.2",
|
|
61
|
+
"prettier": "3.8.1",
|
|
62
|
+
"tsup": "8.5.1",
|
|
63
|
+
"typescript": "5.9.3",
|
|
64
|
+
"vitest": "4.1.8"
|
|
65
|
+
},
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"@opentelemetry/api": "1.9.1",
|
|
68
|
+
"@opentelemetry/api-logs": "0.218.0",
|
|
69
|
+
"@opentelemetry/exporter-logs-otlp-http": "0.218.0",
|
|
70
|
+
"@opentelemetry/exporter-trace-otlp-http": "0.218.0",
|
|
71
|
+
"@opentelemetry/instrumentation-document-load": "0.63.0",
|
|
72
|
+
"@opentelemetry/instrumentation-fetch": "0.218.0",
|
|
73
|
+
"@opentelemetry/instrumentation-xml-http-request": "0.218.0",
|
|
74
|
+
"@opentelemetry/sdk-logs": "0.218.0",
|
|
75
|
+
"@opentelemetry/sdk-trace-web": "2.7.1",
|
|
76
|
+
"@opentelemetry/semantic-conventions": "1.41.1",
|
|
77
|
+
"@rrweb/record": "2.0.1",
|
|
78
|
+
"rrweb": "2.0.1"
|
|
79
|
+
}
|
|
80
|
+
}
|