AbsoluteJS

Replay

@absolutejs/replayv0.3.5betaObservability

Chunked DOM session recording over your own storage, with a player and a replayId seam that links errors to the exact session.

Self-hosted session replay: a recorder that chunks DOM recordings and uploads each chunk through a pluggable transport (wire in @absolutejs/blob), plus chunk assembly and a framework-agnostic player. The recorder itself is about 1 KB of glue — rrweb is an optional, lazy-loaded peer imported only when recording starts — and it exposes a replayId that @absolutejs/beacon stamps onto every error, cross-linking an issue to the exact DOM replay around it.

#Installation

BASH
bun add @absolutejs/replay rrweb

#Capabilities

Overview

Session replay for the AbsoluteJS observability stack. 1 KB of glue around a lazy-loaded recorder, plus an optional rrweb-powered player.

Records DOM sessions, chunks them, and uploads each chunk via a pluggable transport (wire @absolutejs/blob). Exposes a replayId so @absolutejs/beacon can stamp every error with the session — cross-linking an issue to the exact DOM replay around it. Re-assembles chunks for a framework-agnostic player.

Design

Capability-split engine. DOM recording genuinely needs a heavy engine, so

the recorder lazy-loads the separately compiled @rrweb/record package only when recording starts (and remains fully injectable). Playback uses the optional rrweb peer through a separate /player entry. Apps that contain both capabilities do not merge the recorder into rrweb's full player bundle.

Plain TS, not Effect — like beacon, it's browser-first where bytes are

Show 3 more

the cost. Replay's own code is 1 KB gz.

Private by default — inputs are masked (maskAllInputs: true). Recording

user sessions is a real liability surface; keep masking on.

Record

Add class="rr-block" to a node to skip recording it, or class="rr-mask" to mask its text. Use blockSelector for browser-extension or embedded third-party DOM that the application does not own. Use maskAllText: true for high-sensitivity apps.

For application-level error/report capture, use createReplayController. It keeps an in-memory ring until the session matters, coalesces racing flushes, uploads at most two batches concurrently, applies a 10-second deadline to each attempt, removes acknowledged chunks, and retains failed chunks for retry.

API

SSR-safe: imported without a DOM, createRecorder returns a no-op handle (with a valid replayId/manifest).

Zero hard dependencies

rrweb is an optional peer, lazy-imported only when recording starts (and fully injectable), so replay weight never lands on a page that is not recording.

Chunked pluggable upload

Recordings are split by chunkIntervalMs / chunkMaxEvents and each chunk is handed to your upload function — point it at @absolutejs/blob or any storage.

Privacy masking by default

maskAllInputs is on by default; rr-block skips a node entirely, rr-mask masks its text, and maskAllText covers high-sensitivity apps.

Error cross-linking

The recorder exposes replayId for @absolutejs/beacon getReplayId, so every captured error carries the session that produced it.

Framework-agnostic playback

assembleReplay orders and flattens stored chunks, and createReplayPlayer plays them back into any DOM target without a framework.

SSR safe

Imported without a DOM, createRecorder returns a no-op handle that still has a valid replayId and manifest.

Outcomes

What you can build

Overview

Session replay for the AbsoluteJS observability stack. 1 KB of glue around a lazy-loaded recorder, plus an optional rrweb-powered player.

Design

Capability-split engine. DOM recording genuinely needs a heavy engine, so

Record

Add class="rr-block" to a node to skip recording it, or class="rr-mask" to mask its text. Use blockSelector for browser-extension or embedded third-party DOM that the application does not own. Use maskAllText: true for high-sensitivity apps.

Hardening checklist

Production guidance

Make every external boundary explicitPin the deployed @absolutejs/replay version, replace example or memory-backed dependencies with durable implementations, bound external calls, protect credentials, and emit enough evidence to retry or recover safely.

Follow in order

Troubleshooting path

1
Trace from the first failed boundary
Reproduce the smallest canonical @absolutejs/replay example, confirm the supported entry point and version in the API explorer, then inspect the first boundary that did not produce its documented result.

#Record

Partial snippet

Working example for Record.

TS
import { createRecorder } from "@absolutejs/replay/recorder";
import { initBeacon } from "@absolutejs/beacon";

const recorder = createRecorder({
  project: "web",
  release: import.meta.env.VITE_RELEASE,
  upload: (chunk) =>
    uploadToBlob(
      `replays/${chunk.replayId}/${chunk.seq}.json`,
      JSON.stringify(chunk),
    ),
  // privacy defaults: maskAllInputs: true, blockClass: 'rr-block', maskTextClass: 'rr-mask'
  blockSelector: "[data-extension-owned]", // optional third-party DOM exclusion
});

// Cross-link errors → this session:
initBeacon({ project: "web", getReplayId: () => recorder.replayId });

// On error, flush the tail so the replay around it is stored:
window.addEventListener("error", () => void recorder.flush());

#Record 2

Partial snippet

Add class="rr-block" to a node to skip recording it, or class="rr-mask" to mask its text. Use blockSelector for browser-extension or embedded third-party DOM that the application does not own. Use maskAllText: true for high-sensitivity apps.

TS
const replay = createReplayController({
  endpoint: "/ingest/replay",
  project: "web",
  maxUploadConcurrency: 2,
  uploadTimeoutMs: 10_000,
});

#Record a Session

Partial snippet

Start recording, upload chunks to your storage and stamp every beacon error with the session id.

TS
import { initBeacon } from '@absolutejs/beacon';
import { createRecorder } from '@absolutejs/replay';

const recorder = createRecorder({
	project: 'web',
	release: import.meta.env.VITE_RELEASE,
	upload: (chunk) =>
		uploadToBlob(
			`replays/${chunk.replayId}/${chunk.seq}.json`,
			JSON.stringify(chunk)
		)
	// privacy defaults: maskAllInputs: true,
	// blockClass: 'rr-block', maskTextClass: 'rr-mask'
});

// Cross-link errors → this session:
initBeacon({ getReplayId: () => recorder.replayId, project: 'web' });

// On error, flush the tail so the replay around it is stored:
window.addEventListener('error', () => void recorder.flush());

#Play Back

Partial snippet

Re-assemble stored chunks and play them back into any DOM element.

TS
import { assembleReplay, createReplayPlayer } from '@absolutejs/replay';

const chunks = await loadChunksFromBlob(replayId); // your storage read
const target = document.getElementById('replay');

if (target !== null) {
	const player = await createReplayPlayer({
		events: assembleReplay(chunks), // ordered + flattened
		target
	});
	player.pause();
	player.play(0);
}
Privacy first
Recording user sessions is a real liability surface — keep the default input masking on and add rr-block / rr-mask classes to sensitive UI.
Your storage, your data
Chunks are plain JSON handed to your own transport and storage, so replays live in your infrastructure — a self-hosted alternative to LogRocket or FullStory.

#API reference

Search the declarations exported by the current package type files. Expand a symbol to inspect its source-backed signature.

21 symbols
assembleReplayvaluePermalink

Re-assemble a session's chunks into a single ordered event stream.

TS
const assembleReplay: (chunks: ReplayChunk[]) => ReplayEvent[];
Exported from @absolutejs/replay

Current package surface

What ships today

@absolutejs/replayv0.3.5 · betaObservabilitynpmSource
5entry points43symbols

Import surface · click to copy