Overview
Session replay for the AbsoluteJS observability stack. 1 KB of glue; rrweb is an optional, lazy-loaded peer.
@absolutejs/replayv0.3.1betaObservabilityChunked 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; rrweb is an optional, lazy-loaded peer.
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.
Zero hard dependencies. DOM recording genuinely needs a heavy engine, so
the recorder wraps rrweb — but rrweb is an optional peer, lazy-imported only when you start recording (and fully injectable). Replay is the one heavy feature; its weight never lands on a page that isn't recording.
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 maskAllText: true for high-sensitivity apps.
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; rrweb is an optional, lazy-loaded peer.
Zero hard dependencies. 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 maskAllText: true for high-sensitivity apps.
Hardening checklist
Follow in order
Working example for Record.
import { createRecorder } from "@absolutejs/replay";
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'
});
// 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());Working example for Play back.
import { assembleReplay, createReplayPlayer } from "@absolutejs/replay";
const chunks = await loadChunksFromBlob(replayId); // your storage read
const player = await createReplayPlayer({
target: document.getElementById("replay")!,
events: assembleReplay(chunks), // ordered + flattened
});
player.pause();
player.play(0);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.
@absolutejs/replay — session replay for the AbsoluteJS observability stack. DOM recording genuinely needs a heavy, hard-to-replicate engine, so the recorder wraps rrweb — but rrweb is an optional, lazy-loaded peer (and fully injectable), so: - this package has ZERO hard dependencies; rrweb is only pulled when you actually start recording, and only into the replay code path (opt-in weight — replay is the one heavy feature, never on a page that isn't recording). - it's plain TS, NOT Effect — like
type ReplayEvent = {
type: number;
timestamp: number;
data: unknown;
};@absolutejs/replayCurrent package surface
Import surface · click to copy