AbsoluteJS

Replay

@absolutejs/replayv0.3.1betaObservability

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; 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.

Design

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

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 maskAllText: true for high-sensitivity apps.

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; rrweb is an optional, lazy-loaded peer.

Design

Zero hard dependencies. 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 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";
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());

#Play back

Partial snippet

Working example for Play back.

TS
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);

#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.

20 symbols
ReplayEventtypePermalink

@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

TS
type ReplayEvent = {
    type: number;
    timestamp: number;
    data: unknown;
};
Exported from @absolutejs/replay

Current package surface

What ships today

@absolutejs/replayv0.3.1 · betaObservabilitynpmSource
3entry points21symbols

Import surface · click to copy