# `@observtech/rum`

Browser **RUM + Session Replay** SDK for Observ — _replay-as-context_. One call
captures a web session (rrweb replay + OpenTelemetry spans + semantic events +
JS errors) under a single `session.id`, so the whole session can be read as
context rather than watched as a video.

## What it does

A single `observ.init()` wires all of the following, each stamped with the same
`session.id` so the backend can correlate them:

- **Session replay** — `@rrweb/record` DOM capture, sent as gzip chunks.
- **Traces** — `http.client` spans for `fetch`/`XHR` + page load, over OTLP/HTTP,
  with W3C `traceparent` propagation (so front and backend traces stitch).
- **Session lifecycle** — `session.start` / `session.end` events with
  `previous_id` chaining, a 30 min inactivity window + 4 h hard cap, and an
  activity sweep (per-tab `sessionStorage` by default; opt-in cross-tab).
- **End-user identity** — `enduser.pseudo.id` (anonymous, persistent) on every
  signal, plus `enduser.id` once you call `setUser()`.
- **Semantic events** — `click`, `rage_click`, `navigate` as OTel log records.
- **Page views** — rich `app.page_view` (referrer, time-on-page, navigation type).
- **JS errors** — uncaught errors and unhandled promise rejections, observe-only.
- **Console forwarding** — optionally mirror `console.*` to the logs pipeline.
- **User-interaction spans** — optional click/submit/keydown spans with
  `ZoneContextManager` (opt-in; pulls in `zone.js`).
- **Consent gate** — optionally hold ALL telemetry until GDPR analytics consent.
- **PII masking** — input values are masked by default (safe-by-default).

`observ.init()` is **idempotent** and **never throws**: any setup failure
degrades to "no telemetry" instead of breaking the host page.

## Install

```bash
yarn add @observtech/rum   # or: npm install / pnpm add
```

`rrweb` is pulled in as a transitive dependency and resolved by your bundler.
Requires an evergreen browser (uses native `CompressionStream`, `fetch`,
`sessionStorage`, `history`).

## Configure

Call it as early as possible, before the first `fetch`/XHR you want traced:

```ts
import { observ } from '@observtech/rum'

observ.init({
  endpoint: 'https://observ.example.com', // Observ backend base URL
  key: '<api-key>', // sent as the x-observ-key header
})
```

Main options:

| Option                                           | Type                        | Default                                                 | Role                                                                   |
| ------------------------------------------------ | --------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------- |
| `endpoint`                                       | `string`                    | —                                                       | Base URL of the Observ backend (`/v1/...` paths are appended).         |
| `key`                                            | `string`                    | —                                                       | API key sent as `x-observ-key` (empty ⇒ header omitted).               |
| `propagateTraceHeaderCorsUrls`                   | `(string \| RegExp)[]`      | `[]`                                                    | Cross-origin backends allowed to receive the W3C `traceparent` header. |
| `disableReplay`                                  | `boolean`                   | `false`                                                 | Keep tracing/events but turn off the heavy rrweb replay.               |
| `privacy`                                        | `PrivacyOptions`            | masked                                                  | PII masking of the replay stream (see below).                          |
| `serviceName` / `serviceVersion` / `environment` | `string`                    | —                                                       | Resource attributes (`service.name`, …) stamped on every signal.       |
| `sampleRate`                                     | `number`                    | `1`                                                     | Head-based trace sampling ratio in `[0,1)` (ParentBased).              |
| `propagateBaggage`                               | `boolean`                   | `false`                                                 | Send `session.id` to allowed backends via the W3C `baggage` header.    |
| `disableMetrics`                                 | `boolean`                   | `false`                                                 | Turn off Core Web Vitals + OTLP metrics.                               |
| `sessionInactivityMs`                            | `number`                    | 30 min                                                  | Inactivity window before the `session.id` rotates.                     |
| `sessionMaxDurationMs`                           | `number`                    | 4 h                                                     | Hard cap on a single session's duration (rotates even while active).   |
| `crossTabSessions`                               | `boolean`                   | `false`                                                 | Share one session across tabs (`localStorage`) instead of per-tab.     |
| `disablePseudoUser`                              | `boolean`                   | `false`                                                 | Disable the persistent pseudonymous `enduser.pseudo.id`.               |
| `forwardConsole`                                 | `boolean \| ConsoleLevel[]` | `false`                                                 | Mirror `console.*` to the logs pipeline (all levels, or a subset).     |
| `disablePageViews`                               | `boolean`                   | `false`                                                 | Turn off rich `app.page_view` events.                                  |
| `userInteraction`                                | `boolean`                   | `false`                                                 | Interaction spans + `ZoneContextManager` (opt-in; pulls in `zone.js`). |
| `requireConsent`                                 | `boolean`                   | `false`                                                 | Hold ALL telemetry until analytics consent is present (see below).     |
| `consentKey` / `consentGrantedValues`            | `string` / `string[]`       | `observ.consent` / `['granted','true','1','yes','all']` | Where/what consent is read from.                                       |

### Privacy / PII masking

The replay records the live DOM, so input values are PII. Masking is **on by
default**: omit `privacy` entirely and every `<input>`/`<textarea>`/`<select>`
value is masked. Three CSS classes mark sensitive nodes declaratively:

- `observ-mask` — mask the element's text
- `observ-block` — drop the element from the replay
- `observ-ignore` — record the element but ignore its input value

```ts
observ.init({
  endpoint: 'https://observ.example.com',
  key: '<api-key>',
  privacy: {
    maskAllInputs: true, // default; set false only on a surface free of PII
    maskAllText: false, // true masks ALL visible text (degrades the replay)
  },
})
```

### End-user identity

Every signal carries a pseudonymous `enduser.pseudo.id` (persisted in
`localStorage`, anonymous). After sign-in, attach the authenticated identity; clear
it on sign-out:

```ts
observ.setUser({ id: user.id, role: user.role, name: user.name }) // → enduser.id/role/name
observ.clearUser() // on logout (the pseudo id persists)
```

### Sessions

A `session.id` is shared by every signal. It rotates after 30 min of inactivity or
a 4 h hard cap, emitting `session.start` (with `session.previous_id` on a
continuation) and a best-effort `session.end` (with `session.duration_ms`). Sessions
are **per-tab** (`sessionStorage`) by default — two tabs are two visits, by design.
Set `crossTabSessions: true` to share one session across a visitor's tabs.

### Console forwarding (optional)

```ts
observ.init({ endpoint, key, forwardConsole: ['warn', 'error'] }) // or `true` for all levels
```

Off by default — console output can be noisy and may contain PII. The original
`console.*` is always still called.

### User-interaction spans (optional)

```ts
observ.init({ endpoint, key, userInteraction: true })
```

Turns clicks/submits/keydowns into spans with a `ZoneContextManager` so trace
context survives the async work they trigger. **Opt-in**: it pulls in `zone.js`,
which monkey-patches the host's global timers/`Promise` on import — so it is
dynamically loaded only when enabled (the default bundle and host globals stay
untouched).

### Consent (GDPR)

Hold ALL telemetry until analytics consent is present (no session, no persistent
id, no network):

```ts
observ.init({ endpoint, key, requireConsent: true }) // reads cookie/localStorage `observ.consent`
// …later, from your consent banner:
observ.grantConsent() // persists consent + starts the waiting SDK
observ.revokeConsent() // clears consent + shuts the SDK down
```

The SDK also auto-starts if another tab grants consent (via the `storage` event) or
the banner writes the key in this tab (a light poll picks it up).

### Stop (optional)

```ts
await observ.shutdown() // stops replay (flushing the residual) + tears down OTel
```

The SDK already flushes on `visibilitychange`/`pagehide`, so `shutdown()` is
mainly for SPA teardown or tests.

## Documentation

- **Full usage & backend (`observ-server`) setup** — endpoints, CORS, API keys,
  session storage, troubleshooting: see the Observ docs
  (`docs/session-replay-rum-sdk.md`).
- **Building, releasing & maintaining this package**:
  `docs/observ-rum-sdk-maintainers.md`.

MIT licensed.
