epok

Browser RUM & session replay

Updated Oct 21, 2026 · today

Core RUM and replay are separate artifacts. Replay includes the pinned stable rrweb recorder. Both default to denied consent: connect your consent UI before enabling capture. These SDK instructions do not enable server ingestion or change server volume caps.

Core SDK

When installing from a published package, use your reviewed package version and lockfile. The core SDK captures SPA views, fetch and XHR timing, frontend errors, and optional Web Vitals. Repeated initialization on the same page returns the existing handle.

bash
npm install epok-rum web-vitals # web-vitals is optional
ts
import { initRum, createConsentController } from "epok-rum";
import * as webVitals from "web-vitals";

const consent = createConsentController(); // denied until your consent UI grants it
const rum = initRum({
  consent,
  serviceName: "storefront-web",
  endpoint: "https://ingest.getepok.dev",
  apiKey: "epk_…", // restricted ingest-only key
  environment: "production",
  release: import.meta.env.GIT_SHA,
  tracePropagationTargets: ["https://api.example.com"], // exact origins
  webVitals,
});

// Connect your application's consent UI to this callback:
function onTelemetryConsentChanged(allowed) {
  consent.setConsent(allowed === true ? "granted" : "denied");
}
// After consent and sign-in, optionally identify the user:
// rum.setUser({ id: user.id }); // an opaque id, not an email — stored on spans

Optional replay

ts
import { startReplay } from "epok-rum/replay";

const replay = startReplay({
  consent, // the SAME controller passed to initRum above
  get sessionId() { return rum.sessionId; },
  endpoint: "https://ingest.getepok.dev",
  apiKey: "epk_…",
  sampleRate: 0.1,
  recordOnError: true,
});
// rrweb 2.0.0 is compiled into this artifact; no separate recorder import.
// Withdrawal stops both SDKs, erases queues, and does not flush the replay tail.
// consent.setConsent("denied");
// A later grant starts fresh. Identify the user again only after that grant.

Inputs are masked by default. Mark sensitive text with .epok-mask or [data-private], and omit elements with .epok-block. URLs redact query values, dynamic path segments, and fragments; console activity uses the same redaction before buffering. A throwing beforeSend hook drops its core event. Replay flushes every 30 seconds by default while consent is granted. Sampling applies only after consent.

Use the shared consent controller for withdrawal. The replay stop method is an ordinary end-of-session flush; discard stops and erases a replay without sending its tail. Withdrawal cannot recall telemetry already delivered to the server.

Serve the artifact from your origin

Build and copy our dist artifact to your own static assets. Replay loads as one module from your origin, including rrweb; no third-party script origin is needed in your CSP. The build also emits IIFE variants and an integrity.json manifest with SHA-384 hashes.

bash
# From this source checkout:
npm ci --ignore-scripts
npm ci --prefix lib/rum --ignore-scripts
node lib/rum/build.mjs
# Copy lib/rum/dist/epok-rum-replay.esm.js into your site's assets/epok/.
# Keep core separate; copy epok-rum.esm.js too only if you use core RUM.
js
// In an application module served by YOUR origin:
import { startReplay } from "/assets/epok/epok-rum-replay.esm.js";

const replay = startReplay({
  endpoint: "https://ingest.getepok.dev",
  apiKey: "epk_…",
}); // denied: no recording or uploads yet
function onTelemetryConsentChanged(allowed) {
  replay.setConsent(allowed === true ? "granted" : "denied");
}

Check the integration

Use a restricted ingest-only key; browser keys are public. Before granting consent, verify that no telemetry requests or recordings occur. After a grant, inspect the redacted payloads. Withdraw while data is buffered and confirm there is no tail upload. Re-grant and confirm a new session. Backend ingestion must be enabled separately for your environment; this page does not change that gate.

Core propagation uses exact HTTP(S) origins and preserves valid existing trace context. An optional path scope uses an object with origin and pathPrefix, such as { origin: "https://api.example.com", pathPrefix: "/v1" }. If you need per-chunk trace linkage, provide replay's getTraceId callback from your tracing integration.

See source maps, authentication, and ingestion limits for the related setup.