@observtech/rum
v0.1.37
Published
Observ browser RUM + Session Replay SDK (rrweb + OpenTelemetry, replay-as-context).
Maintainers
Readme
@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/recordDOM capture, sent as gzip chunks. - Traces —
http.clientspans forfetch/XHR+ page load, over OTLP/HTTP, with W3Ctraceparentpropagation (so front and backend traces stitch). - Session lifecycle —
session.start/session.endevents withprevious_idchaining, a 30 min inactivity window + 4 h hard cap, and an activity sweep (per-tabsessionStorageby default; opt-in cross-tab). - End-user identity —
enduser.pseudo.id(anonymous, persistent) on every signal, plusenduser.idonce you callsetUser(). - Semantic events —
click,rage_click,navigateas 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 inzone.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
yarn add @observtech/rum # or: npm install / pnpm addrrweb 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:
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 textobserv-block— drop the element from the replayobserv-ignore— record the element but ignore its input value
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:
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)
observ.init({ endpoint, key, forwardConsole: ['warn', 'error'] }) // or `true` for all levelsOff by default — console output can be noisy and may contain PII. The original
console.* is always still called.
User-interaction spans (optional)
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):
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 downThe 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)
await observ.shutdown() // stops replay (flushing the residual) + tears down OTelThe 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.
