Overview
Session replay for the AbsoluteJS observability stack. 1 KB of glue around a lazy-loaded recorder, plus an optional rrweb-powered player.
@absolutejs/replayv0.3.5betaObservabilityChunked 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.
bun add @absolutejs/replay rrwebSession 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.
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
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.
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.
SSR-safe: imported without a DOM, createRecorder returns a no-op handle (with a valid replayId/manifest).
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.
Recordings are split by chunkIntervalMs / chunkMaxEvents and each chunk is handed to your upload function — point it at @absolutejs/blob or any storage.
maskAllInputs is on by default; rr-block skips a node entirely, rr-mask masks its text, and maskAllText covers high-sensitivity apps.
The recorder exposes replayId for @absolutejs/beacon getReplayId, so every captured error carries the session that produced it.
assembleReplay orders and flattens stored chunks, and createReplayPlayer plays them back into any DOM target without a framework.
Imported without a DOM, createRecorder returns a no-op handle that still has a valid replayId and manifest.
Outcomes
Session replay for the AbsoluteJS observability stack. 1 KB of glue around a lazy-loaded recorder, plus an optional rrweb-powered player.
Capability-split engine. DOM recording genuinely needs a heavy engine, so
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
Follow in order
Working example for Record.
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());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.
const replay = createReplayController({
endpoint: "/ingest/replay",
project: "web",
maxUploadConcurrency: 2,
uploadTimeoutMs: 10_000,
});Start recording, upload chunks to your storage and stamp every beacon error with the session id.
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());Re-assemble stored chunks and play them back into any DOM element.
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);
}Search the declarations exported by the current package type files. Expand a symbol to inspect its source-backed signature.
Re-assemble a session's chunks into a single ordered event stream.
const assembleReplay: (chunks: ReplayChunk[]) => ReplayEvent[];@absolutejs/replayCurrent package surface
Import surface · click to copy