@absolutejs/replay 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -113,6 +113,40 @@ export type Recorder = {
113
113
  stop: () => Promise<void>;
114
114
  };
115
115
  export declare const createRecorder: (options: RecorderOptions) => Recorder;
116
+ /** The per-chunk wire shape (the manifest carries replayId/project). */
117
+ export type WireChunk = Pick<ReplayChunk, "events" | "from" | "seq" | "to">;
118
+ export type ReplayControllerOptions = {
119
+ /** Ingest route accepting `{ chunks: WireChunk[]; manifest: ReplayManifest }`. */
120
+ endpoint: string;
121
+ project: string;
122
+ release?: string;
123
+ environment?: string;
124
+ /** Chunks of context to retain (~10 min @ 5s/chunk). Default 120. */
125
+ maxRingChunks?: number;
126
+ /** Min ms between throttled auto-error flushes. Default 30000. */
127
+ flushThrottleMs?: number;
128
+ /** Max uncompressed bytes per upload batch. Default 700000. */
129
+ maxBatchBytes?: number;
130
+ /** Max bytes for the keepalive unload tail (stay under the ~64KB cap). Default 55000. */
131
+ maxTailBytes?: number;
132
+ /** Masking / record-injection / cadence forwarded to the recorder. */
133
+ recorder?: Omit<RecorderOptions, "project" | "upload" | "replayId" | "release" | "environment">;
134
+ /** Override fetch (tests / proxies). Default global fetch. */
135
+ fetch?: typeof fetch;
136
+ };
137
+ export type ReplayController = {
138
+ /** The session id — feed to `@absolutejs/beacon`'s `getReplayId`. */
139
+ getReplayId: () => string;
140
+ /** Persist the full ring now (a bug report). Returns the replayId. */
141
+ flush: () => Promise<string | null>;
142
+ /** Persist the ring, but at most once per `flushThrottleMs` (auto-errors). */
143
+ flushThrottled: () => void;
144
+ /** Keepalive tail-flush on `pagehide` — no-op unless the session mattered. */
145
+ flushOnUnload: () => void;
146
+ /** Stop recording and flush the final chunk. */
147
+ stop: () => Promise<void>;
148
+ };
149
+ export declare const createReplayController: (options: ReplayControllerOptions) => ReplayController;
116
150
  /** Re-assemble a session's chunks into a single ordered event stream. */
117
151
  export declare const assembleReplay: (chunks: ReplayChunk[]) => ReplayEvent[];
118
152
  export type ReplayPlayerOptions = {
package/dist/index.js CHANGED
@@ -128,6 +128,142 @@ var createRecorder = (options) => {
128
128
  }
129
129
  };
130
130
  };
131
+ var RING_CHUNKS_DEFAULT = 120;
132
+ var FLUSH_THROTTLE_DEFAULT_MS = 30000;
133
+ var BATCH_BYTES_DEFAULT = 700000;
134
+ var TAIL_BYTES_DEFAULT = 55000;
135
+ var encodeBody = async (json) => {
136
+ const plain = { body: json, gzip: false };
137
+ if (typeof CompressionStream === "undefined")
138
+ return plain;
139
+ const stream = new Response(json).body;
140
+ if (stream === null)
141
+ return plain;
142
+ try {
143
+ const compressed = await new Response(stream.pipeThrough(new CompressionStream("gzip"))).arrayBuffer();
144
+ return { body: compressed, gzip: true };
145
+ } catch {
146
+ return plain;
147
+ }
148
+ };
149
+ var toWire = (chunk) => ({
150
+ events: chunk.events,
151
+ from: chunk.from,
152
+ seq: chunk.seq,
153
+ to: chunk.to
154
+ });
155
+ var batchByBytes = (chunks, maxBytes) => {
156
+ const batches = [];
157
+ let current = [];
158
+ let bytes = 0;
159
+ const flush = () => {
160
+ if (current.length > 0)
161
+ batches.push(current);
162
+ current = [];
163
+ bytes = 0;
164
+ };
165
+ for (const chunk of chunks) {
166
+ const size = JSON.stringify(chunk).length;
167
+ if (current.length > 0 && bytes + size > maxBytes)
168
+ flush();
169
+ current.push(chunk);
170
+ bytes += size;
171
+ }
172
+ flush();
173
+ return batches;
174
+ };
175
+ var createReplayController = (options) => {
176
+ const maxRing = options.maxRingChunks ?? RING_CHUNKS_DEFAULT;
177
+ const throttleMs = options.flushThrottleMs ?? FLUSH_THROTTLE_DEFAULT_MS;
178
+ const maxBatchBytes = options.maxBatchBytes ?? BATCH_BYTES_DEFAULT;
179
+ const maxTailBytes = options.maxTailBytes ?? TAIL_BYTES_DEFAULT;
180
+ const doFetch = options.fetch ?? globalThis.fetch;
181
+ const ring = [];
182
+ let lastFlush = 0;
183
+ let sessionMatters = false;
184
+ const recorder = createRecorder({
185
+ project: options.project,
186
+ upload: (chunk) => {
187
+ ring.push(chunk);
188
+ while (ring.length > maxRing)
189
+ ring.shift();
190
+ },
191
+ ...options.release !== undefined ? { release: options.release } : {},
192
+ ...options.environment !== undefined ? { environment: options.environment } : {},
193
+ ...options.recorder ?? {}
194
+ });
195
+ const postBatch = async (batch, manifest) => {
196
+ const { body, gzip } = await encodeBody(JSON.stringify({ chunks: batch, manifest }));
197
+ const headers = {
198
+ "content-type": "application/json"
199
+ };
200
+ if (gzip)
201
+ headers["content-encoding"] = "gzip";
202
+ await doFetch(options.endpoint, {
203
+ body,
204
+ credentials: "include",
205
+ headers,
206
+ method: "POST"
207
+ });
208
+ };
209
+ const flush = async () => {
210
+ if (typeof window === "undefined")
211
+ return null;
212
+ sessionMatters = true;
213
+ await recorder.flush();
214
+ if (ring.length === 0)
215
+ return recorder.replayId;
216
+ const manifest = recorder.manifest();
217
+ const wire = ring.map(toWire);
218
+ try {
219
+ await Promise.all(batchByBytes(wire, maxBatchBytes).map((batch) => postBatch(batch, manifest)));
220
+ lastFlush = Date.now();
221
+ } catch {}
222
+ return recorder.replayId;
223
+ };
224
+ const flushThrottled = () => {
225
+ if (Date.now() - lastFlush < throttleMs)
226
+ return;
227
+ lastFlush = Date.now();
228
+ flush();
229
+ };
230
+ const flushOnUnload = () => {
231
+ if (!sessionMatters || typeof window === "undefined")
232
+ return;
233
+ const tail = [];
234
+ let bytes = 0;
235
+ for (let index = ring.length - 1;index >= 0; index -= 1) {
236
+ const chunk = ring[index];
237
+ if (chunk === undefined)
238
+ continue;
239
+ const size = JSON.stringify(chunk).length;
240
+ if (tail.length > 0 && bytes + size > maxTailBytes)
241
+ break;
242
+ tail.unshift(chunk);
243
+ bytes += size;
244
+ }
245
+ if (tail.length === 0)
246
+ return;
247
+ doFetch(options.endpoint, {
248
+ body: JSON.stringify({
249
+ chunks: tail.map(toWire),
250
+ manifest: recorder.manifest()
251
+ }),
252
+ headers: { "content-type": "application/json" },
253
+ keepalive: true,
254
+ method: "POST"
255
+ }).catch(() => {
256
+ return;
257
+ });
258
+ };
259
+ return {
260
+ flush,
261
+ flushOnUnload,
262
+ flushThrottled,
263
+ getReplayId: () => recorder.replayId,
264
+ stop: () => recorder.stop()
265
+ };
266
+ };
131
267
  var assembleReplay = (chunks) => [...chunks].sort((a, b) => a.seq - b.seq).flatMap((chunk) => chunk.events);
132
268
  var createReplayPlayer = async (options) => {
133
269
  const Replayer = options.Replayer ?? await loadRrwebReplayer();
@@ -145,9 +281,10 @@ var createReplayPlayer = async (options) => {
145
281
  };
146
282
  export {
147
283
  createReplayPlayer,
284
+ createReplayController,
148
285
  createRecorder,
149
286
  assembleReplay
150
287
  };
151
288
 
152
- //# debugId=5AEB3D7140DBAB4464756E2164756E21
289
+ //# debugId=828F915630AC5DEC64756E2164756E21
153
290
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * @absolutejs/replay — session replay for the AbsoluteJS observability stack.\n *\n * DOM recording genuinely needs a heavy, hard-to-replicate engine, so the\n * recorder wraps **rrweb** — but rrweb is an **optional, lazy-loaded peer**\n * (and fully injectable), so:\n * - this package has ZERO hard dependencies; rrweb is only pulled when you\n * actually start recording, and only into the replay code path (opt-in\n * weight — replay is the one heavy feature, never on a page that isn't\n * recording).\n * - it's plain TS, NOT Effect — like @absolutejs/beacon, it's browser-first\n * where bytes are the cost; the server-side rigor lives in the ingest /\n * storage layers.\n *\n * Pipeline: rrweb emits events → buffered → chunked (by size/interval) →\n * uploaded via a pluggable `upload` transport (wire `@absolutejs/blob`). A\n * `replayId` is exposed synchronously so `@absolutejs/beacon`'s `getReplayId`\n * seam can stamp every error with the session — cross-linking an issue to its\n * exact DOM replay. Playback re-assembles chunks and feeds rrweb's `Replayer`.\n *\n * PRIVACY: inputs are masked by default (`maskAllInputs: true`). Recording user\n * sessions is a real liability surface — keep masking on, add `blockClass` /\n * `maskTextClass` to sensitive nodes, and use `maskAllText` for high-sensitivity\n * apps.\n */\n\n// =============================================================================\n// rrweb structural types — declared locally so rrweb stays an optional peer\n// (no hard type dependency on the public surface).\n// =============================================================================\n\n/** An rrweb event. Opaque to us — we only buffer/transport/replay it. */\nexport type ReplayEvent = {\n type: number;\n timestamp: number;\n data: unknown;\n};\n\nexport type RecordConfig = {\n emit: (event: ReplayEvent) => void;\n maskAllInputs?: boolean;\n maskTextSelector?: string;\n blockClass?: string;\n maskTextClass?: string;\n recordCanvas?: boolean;\n};\n\n/** rrweb's `record` — returns a stop handler. */\nexport type RrwebRecord = (config: RecordConfig) => (() => void) | undefined;\n\nexport type RrwebReplayerInstance = {\n play: (timeOffset?: number) => void;\n pause: () => void;\n destroy?: () => void;\n};\n\n/** rrweb's `Replayer` constructor. */\nexport type RrwebReplayerConstructor = new (\n events: ReplayEvent[],\n config?: { root?: Element; speed?: number },\n) => RrwebReplayerInstance;\n\n// =============================================================================\n// Replay format\n// =============================================================================\n\n/** A contiguous slice of a session's events — the unit uploaded to storage. */\nexport type ReplayChunk = {\n replayId: string;\n project: string;\n /** Monotonic chunk index within the session (0-based). */\n seq: number;\n /** First event timestamp in this chunk. */\n from: number;\n /** Last event timestamp in this chunk. */\n to: number;\n events: ReplayEvent[];\n};\n\n/** Session-level metadata — pair with the chunk keys to locate a replay. */\nexport type ReplayManifest = {\n replayId: string;\n project: string;\n startedAt: number;\n release?: string;\n environment?: string;\n chunkCount: number;\n durationMs: number;\n};\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nconst newId = (): string => {\n if (\n typeof crypto !== \"undefined\" &&\n typeof crypto.randomUUID === \"function\"\n ) {\n return crypto.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n};\n\nconst loadRrwebRecord = async (): Promise<RrwebRecord> => {\n try {\n const mod = (await import(\"rrweb\")) as unknown as { record: RrwebRecord };\n return mod.record;\n } catch (cause) {\n throw new Error(\n \"[replay] rrweb is not installed. Run `bun add rrweb`, or pass `record` to createRecorder.\",\n { cause },\n );\n }\n};\n\nconst loadRrwebReplayer = async (): Promise<RrwebReplayerConstructor> => {\n try {\n const mod = (await import(\"rrweb\")) as unknown as {\n Replayer: RrwebReplayerConstructor;\n };\n return mod.Replayer;\n } catch (cause) {\n throw new Error(\n \"[replay] rrweb is not installed. Run `bun add rrweb`, or pass `Replayer` to createReplayPlayer.\",\n { cause },\n );\n }\n};\n\n// =============================================================================\n// Recorder\n// =============================================================================\n\n/** Persist one chunk — wire `@absolutejs/blob` (or any object store) here. */\nexport type ChunkUpload = (chunk: ReplayChunk) => void | Promise<void>;\n\nexport type RecorderOptions = {\n project: string;\n /** Called for each chunk as it's flushed. */\n upload: ChunkUpload;\n /** Override the generated session id. */\n replayId?: string;\n release?: string;\n environment?: string;\n /** Flush a chunk at least this often (ms). Default 5000. */\n chunkIntervalMs?: number;\n /** Flush once this many events buffer. Default 200. */\n chunkMaxEvents?: number;\n /** Mask all input values (privacy). Default **true**. */\n maskAllInputs?: boolean;\n /** Mask all text content (high-sensitivity). Default false. */\n maskAllText?: boolean;\n /** CSS class whose subtrees are not recorded. Default `'rr-block'`. */\n blockClass?: string;\n /** CSS class whose text is masked. Default `'rr-mask'`. */\n maskTextClass?: string;\n /** Record `<canvas>` (heavier). Default false. */\n recordCanvas?: boolean;\n /** Inject rrweb's `record` (default: lazy-imported). */\n record?: RrwebRecord;\n /** Override `Date.now()` for tests. */\n clock?: () => number;\n /** Hook for upload / recorder failures (best-effort; never throws to the app). */\n onError?: (error: unknown) => void;\n};\n\nexport type Recorder = {\n /** The session id — feed to `@absolutejs/beacon`'s `getReplayId`. */\n replayId: string;\n /** Current session metadata snapshot. */\n manifest: () => ReplayManifest;\n /** Force-flush the buffered events as a chunk now. */\n flush: () => Promise<void>;\n /** Stop recording and flush the final chunk. */\n stop: () => Promise<void>;\n};\n\nexport const createRecorder = (options: RecorderOptions): Recorder => {\n const replayId = options.replayId ?? newId();\n const clock = options.clock ?? Date.now;\n const startedAt = clock();\n\n const baseManifest = (\n chunkCount: number,\n durationMs: number,\n ): ReplayManifest => ({\n chunkCount,\n durationMs,\n project: options.project,\n replayId,\n startedAt,\n ...(options.release !== undefined ? { release: options.release } : {}),\n ...(options.environment !== undefined\n ? { environment: options.environment }\n : {}),\n });\n\n // SSR / non-DOM: a valid recorder handle that records nothing.\n if (typeof window === \"undefined\") {\n return {\n flush: async () => {},\n manifest: () => baseManifest(0, 0),\n replayId,\n stop: async () => {},\n };\n }\n\n const chunkMaxEvents = options.chunkMaxEvents ?? 200;\n const chunkIntervalMs = options.chunkIntervalMs ?? 5000;\n const onError = options.onError ?? (() => {});\n\n let buffer: ReplayEvent[] = [];\n let seq = 0;\n let chunkCount = 0;\n let lastTimestamp = startedAt;\n let stopFn: (() => void) | undefined;\n let stopped = false;\n\n const flush = async (): Promise<void> => {\n if (buffer.length === 0) return;\n const events = buffer;\n buffer = [];\n const chunk: ReplayChunk = {\n events,\n from: events[0]!.timestamp,\n project: options.project,\n replayId,\n seq: seq++,\n to: events[events.length - 1]!.timestamp,\n };\n chunkCount += 1;\n try {\n await options.upload(chunk);\n } catch (error) {\n onError(error);\n }\n };\n\n const emit = (event: ReplayEvent): void => {\n buffer.push(event);\n lastTimestamp = event.timestamp;\n if (buffer.length >= chunkMaxEvents) void flush();\n };\n\n const config: RecordConfig = {\n blockClass: options.blockClass ?? \"rr-block\",\n emit,\n maskAllInputs: options.maskAllInputs ?? true,\n maskTextClass: options.maskTextClass ?? \"rr-mask\",\n ...(options.maskAllText === true ? { maskTextSelector: \"*\" } : {}),\n ...(options.recordCanvas === true ? { recordCanvas: true } : {}),\n };\n\n const start = (record: RrwebRecord): void => {\n if (stopped) return;\n try {\n stopFn = record(config) ?? undefined;\n } catch (error) {\n onError(error);\n }\n };\n\n if (options.record !== undefined) start(options.record);\n else loadRrwebRecord().then(start).catch(onError);\n\n const timer = setInterval(() => {\n void flush();\n }, chunkIntervalMs);\n (timer as { unref?: () => void }).unref?.();\n\n return {\n flush,\n manifest: () =>\n baseManifest(chunkCount, Math.max(0, lastTimestamp - startedAt)),\n replayId,\n stop: async () => {\n stopped = true;\n clearInterval(timer);\n if (stopFn !== undefined) {\n try {\n stopFn();\n } catch (error) {\n onError(error);\n }\n }\n await flush();\n },\n };\n};\n\n// =============================================================================\n// Playback\n// =============================================================================\n\n/** Re-assemble a session's chunks into a single ordered event stream. */\nexport const assembleReplay = (chunks: ReplayChunk[]): ReplayEvent[] =>\n [...chunks].sort((a, b) => a.seq - b.seq).flatMap((chunk) => chunk.events);\n\nexport type ReplayPlayerOptions = {\n /** Element to mount the replay into. */\n target: Element;\n /** The assembled event stream (see `assembleReplay`). */\n events: ReplayEvent[];\n /** Inject rrweb's `Replayer` (default: lazy-imported). */\n Replayer?: RrwebReplayerConstructor;\n /** Start playing immediately. Default true. */\n autoplay?: boolean;\n speed?: number;\n};\n\nexport type ReplayPlayer = {\n play: (timeOffset?: number) => void;\n pause: () => void;\n destroy: () => void;\n};\n\nexport const createReplayPlayer = async (\n options: ReplayPlayerOptions,\n): Promise<ReplayPlayer> => {\n const Replayer = options.Replayer ?? (await loadRrwebReplayer());\n const replayer = new Replayer(options.events, {\n root: options.target,\n ...(options.speed !== undefined ? { speed: options.speed } : {}),\n });\n if (options.autoplay !== false) replayer.play();\n return {\n destroy: () => replayer.destroy?.(),\n pause: () => replayer.pause(),\n play: (timeOffset) => replayer.play(timeOffset),\n };\n};\n"
5
+ "/**\n * @absolutejs/replay — session replay for the AbsoluteJS observability stack.\n *\n * DOM recording genuinely needs a heavy, hard-to-replicate engine, so the\n * recorder wraps **rrweb** — but rrweb is an **optional, lazy-loaded peer**\n * (and fully injectable), so:\n * - this package has ZERO hard dependencies; rrweb is only pulled when you\n * actually start recording, and only into the replay code path (opt-in\n * weight — replay is the one heavy feature, never on a page that isn't\n * recording).\n * - it's plain TS, NOT Effect — like @absolutejs/beacon, it's browser-first\n * where bytes are the cost; the server-side rigor lives in the ingest /\n * storage layers.\n *\n * Pipeline: rrweb emits events → buffered → chunked (by size/interval) →\n * uploaded via a pluggable `upload` transport (wire `@absolutejs/blob`). A\n * `replayId` is exposed synchronously so `@absolutejs/beacon`'s `getReplayId`\n * seam can stamp every error with the session — cross-linking an issue to its\n * exact DOM replay. Playback re-assembles chunks and feeds rrweb's `Replayer`.\n *\n * PRIVACY: inputs are masked by default (`maskAllInputs: true`). Recording user\n * sessions is a real liability surface — keep masking on, add `blockClass` /\n * `maskTextClass` to sensitive nodes, and use `maskAllText` for high-sensitivity\n * apps.\n */\n\n// =============================================================================\n// rrweb structural types — declared locally so rrweb stays an optional peer\n// (no hard type dependency on the public surface).\n// =============================================================================\n\n/** An rrweb event. Opaque to us — we only buffer/transport/replay it. */\nexport type ReplayEvent = {\n type: number;\n timestamp: number;\n data: unknown;\n};\n\nexport type RecordConfig = {\n emit: (event: ReplayEvent) => void;\n maskAllInputs?: boolean;\n maskTextSelector?: string;\n blockClass?: string;\n maskTextClass?: string;\n recordCanvas?: boolean;\n};\n\n/** rrweb's `record` — returns a stop handler. */\nexport type RrwebRecord = (config: RecordConfig) => (() => void) | undefined;\n\nexport type RrwebReplayerInstance = {\n play: (timeOffset?: number) => void;\n pause: () => void;\n destroy?: () => void;\n};\n\n/** rrweb's `Replayer` constructor. */\nexport type RrwebReplayerConstructor = new (\n events: ReplayEvent[],\n config?: { root?: Element; speed?: number },\n) => RrwebReplayerInstance;\n\n// =============================================================================\n// Replay format\n// =============================================================================\n\n/** A contiguous slice of a session's events — the unit uploaded to storage. */\nexport type ReplayChunk = {\n replayId: string;\n project: string;\n /** Monotonic chunk index within the session (0-based). */\n seq: number;\n /** First event timestamp in this chunk. */\n from: number;\n /** Last event timestamp in this chunk. */\n to: number;\n events: ReplayEvent[];\n};\n\n/** Session-level metadata — pair with the chunk keys to locate a replay. */\nexport type ReplayManifest = {\n replayId: string;\n project: string;\n startedAt: number;\n release?: string;\n environment?: string;\n chunkCount: number;\n durationMs: number;\n};\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nconst newId = (): string => {\n if (\n typeof crypto !== \"undefined\" &&\n typeof crypto.randomUUID === \"function\"\n ) {\n return crypto.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n};\n\nconst loadRrwebRecord = async (): Promise<RrwebRecord> => {\n try {\n const mod = (await import(\"rrweb\")) as unknown as { record: RrwebRecord };\n return mod.record;\n } catch (cause) {\n throw new Error(\n \"[replay] rrweb is not installed. Run `bun add rrweb`, or pass `record` to createRecorder.\",\n { cause },\n );\n }\n};\n\nconst loadRrwebReplayer = async (): Promise<RrwebReplayerConstructor> => {\n try {\n const mod = (await import(\"rrweb\")) as unknown as {\n Replayer: RrwebReplayerConstructor;\n };\n return mod.Replayer;\n } catch (cause) {\n throw new Error(\n \"[replay] rrweb is not installed. Run `bun add rrweb`, or pass `Replayer` to createReplayPlayer.\",\n { cause },\n );\n }\n};\n\n// =============================================================================\n// Recorder\n// =============================================================================\n\n/** Persist one chunk — wire `@absolutejs/blob` (or any object store) here. */\nexport type ChunkUpload = (chunk: ReplayChunk) => void | Promise<void>;\n\nexport type RecorderOptions = {\n project: string;\n /** Called for each chunk as it's flushed. */\n upload: ChunkUpload;\n /** Override the generated session id. */\n replayId?: string;\n release?: string;\n environment?: string;\n /** Flush a chunk at least this often (ms). Default 5000. */\n chunkIntervalMs?: number;\n /** Flush once this many events buffer. Default 200. */\n chunkMaxEvents?: number;\n /** Mask all input values (privacy). Default **true**. */\n maskAllInputs?: boolean;\n /** Mask all text content (high-sensitivity). Default false. */\n maskAllText?: boolean;\n /** CSS class whose subtrees are not recorded. Default `'rr-block'`. */\n blockClass?: string;\n /** CSS class whose text is masked. Default `'rr-mask'`. */\n maskTextClass?: string;\n /** Record `<canvas>` (heavier). Default false. */\n recordCanvas?: boolean;\n /** Inject rrweb's `record` (default: lazy-imported). */\n record?: RrwebRecord;\n /** Override `Date.now()` for tests. */\n clock?: () => number;\n /** Hook for upload / recorder failures (best-effort; never throws to the app). */\n onError?: (error: unknown) => void;\n};\n\nexport type Recorder = {\n /** The session id — feed to `@absolutejs/beacon`'s `getReplayId`. */\n replayId: string;\n /** Current session metadata snapshot. */\n manifest: () => ReplayManifest;\n /** Force-flush the buffered events as a chunk now. */\n flush: () => Promise<void>;\n /** Stop recording and flush the final chunk. */\n stop: () => Promise<void>;\n};\n\nexport const createRecorder = (options: RecorderOptions): Recorder => {\n const replayId = options.replayId ?? newId();\n const clock = options.clock ?? Date.now;\n const startedAt = clock();\n\n const baseManifest = (\n chunkCount: number,\n durationMs: number,\n ): ReplayManifest => ({\n chunkCount,\n durationMs,\n project: options.project,\n replayId,\n startedAt,\n ...(options.release !== undefined ? { release: options.release } : {}),\n ...(options.environment !== undefined\n ? { environment: options.environment }\n : {}),\n });\n\n // SSR / non-DOM: a valid recorder handle that records nothing.\n if (typeof window === \"undefined\") {\n return {\n flush: async () => {},\n manifest: () => baseManifest(0, 0),\n replayId,\n stop: async () => {},\n };\n }\n\n const chunkMaxEvents = options.chunkMaxEvents ?? 200;\n const chunkIntervalMs = options.chunkIntervalMs ?? 5000;\n const onError = options.onError ?? (() => {});\n\n let buffer: ReplayEvent[] = [];\n let seq = 0;\n let chunkCount = 0;\n let lastTimestamp = startedAt;\n let stopFn: (() => void) | undefined;\n let stopped = false;\n\n const flush = async (): Promise<void> => {\n if (buffer.length === 0) return;\n const events = buffer;\n buffer = [];\n const chunk: ReplayChunk = {\n events,\n from: events[0]!.timestamp,\n project: options.project,\n replayId,\n seq: seq++,\n to: events[events.length - 1]!.timestamp,\n };\n chunkCount += 1;\n try {\n await options.upload(chunk);\n } catch (error) {\n onError(error);\n }\n };\n\n const emit = (event: ReplayEvent): void => {\n buffer.push(event);\n lastTimestamp = event.timestamp;\n if (buffer.length >= chunkMaxEvents) void flush();\n };\n\n const config: RecordConfig = {\n blockClass: options.blockClass ?? \"rr-block\",\n emit,\n maskAllInputs: options.maskAllInputs ?? true,\n maskTextClass: options.maskTextClass ?? \"rr-mask\",\n ...(options.maskAllText === true ? { maskTextSelector: \"*\" } : {}),\n ...(options.recordCanvas === true ? { recordCanvas: true } : {}),\n };\n\n const start = (record: RrwebRecord): void => {\n if (stopped) return;\n try {\n stopFn = record(config) ?? undefined;\n } catch (error) {\n onError(error);\n }\n };\n\n if (options.record !== undefined) start(options.record);\n else loadRrwebRecord().then(start).catch(onError);\n\n const timer = setInterval(() => {\n void flush();\n }, chunkIntervalMs);\n (timer as { unref?: () => void }).unref?.();\n\n return {\n flush,\n manifest: () =>\n baseManifest(chunkCount, Math.max(0, lastTimestamp - startedAt)),\n replayId,\n stop: async () => {\n stopped = true;\n clearInterval(timer);\n if (stopFn !== undefined) {\n try {\n stopFn();\n } catch (error) {\n onError(error);\n }\n }\n await flush();\n },\n };\n};\n\n// =============================================================================\n// Controller — production \"batteries\" over createRecorder.\n//\n// createRecorder gives you raw chunks; deciding WHEN and HOW to ship them is the\n// part every app otherwise re-derives (and gets wrong). The controller keeps a\n// bounded ring of recent chunks (nothing stored server-side by default) and\n// uploads the tail only when it matters — a bug report (`flush`), an auto-error\n// (`flushThrottled`), or the page unloading (`flushOnUnload`). Uploads are\n// gzipped and size-batched so no POST can exceed a gateway body cap no matter\n// how large the session (rrweb JSON compresses ~10:1). The unload path uses\n// `keepalive` so the final moments survive the tab closing, and only fires for\n// sessions that already mattered — so nothing orphaned is ever stored.\n//\n// Wire contract: POST `{ chunks: WireChunk[]; manifest: ReplayManifest }` to\n// your `endpoint`; the gzip path sets `content-encoding: gzip` (decompress\n// before validating). Pairs with @absolutejs/errors' ingest + the typed\n// ReplayChunk/ReplayManifest shapes above.\n// =============================================================================\n\n/** The per-chunk wire shape (the manifest carries replayId/project). */\nexport type WireChunk = Pick<ReplayChunk, \"events\" | \"from\" | \"seq\" | \"to\">;\n\ntype EncodedBody = { body: BodyInit; gzip: boolean };\n\nexport type ReplayControllerOptions = {\n /** Ingest route accepting `{ chunks: WireChunk[]; manifest: ReplayManifest }`. */\n endpoint: string;\n project: string;\n release?: string;\n environment?: string;\n /** Chunks of context to retain (~10 min @ 5s/chunk). Default 120. */\n maxRingChunks?: number;\n /** Min ms between throttled auto-error flushes. Default 30000. */\n flushThrottleMs?: number;\n /** Max uncompressed bytes per upload batch. Default 700000. */\n maxBatchBytes?: number;\n /** Max bytes for the keepalive unload tail (stay under the ~64KB cap). Default 55000. */\n maxTailBytes?: number;\n /** Masking / record-injection / cadence forwarded to the recorder. */\n recorder?: Omit<\n RecorderOptions,\n \"project\" | \"upload\" | \"replayId\" | \"release\" | \"environment\"\n >;\n /** Override fetch (tests / proxies). Default global fetch. */\n fetch?: typeof fetch;\n};\n\nexport type ReplayController = {\n /** The session id — feed to `@absolutejs/beacon`'s `getReplayId`. */\n getReplayId: () => string;\n /** Persist the full ring now (a bug report). Returns the replayId. */\n flush: () => Promise<string | null>;\n /** Persist the ring, but at most once per `flushThrottleMs` (auto-errors). */\n flushThrottled: () => void;\n /** Keepalive tail-flush on `pagehide` — no-op unless the session mattered. */\n flushOnUnload: () => void;\n /** Stop recording and flush the final chunk. */\n stop: () => Promise<void>;\n};\n\nconst RING_CHUNKS_DEFAULT = 120;\nconst FLUSH_THROTTLE_DEFAULT_MS = 30_000;\nconst BATCH_BYTES_DEFAULT = 700_000;\nconst TAIL_BYTES_DEFAULT = 55_000;\n\n// gzip a JSON string when supported (Response(json).body avoids needing a Blob);\n// fall back to plain text where CompressionStream / a body stream is absent.\nconst encodeBody = async (json: string): Promise<EncodedBody> => {\n const plain: EncodedBody = { body: json, gzip: false };\n if (typeof CompressionStream === \"undefined\") return plain;\n const stream = new Response(json).body;\n if (stream === null) return plain;\n try {\n const compressed = await new Response(\n stream.pipeThrough(new CompressionStream(\"gzip\")),\n ).arrayBuffer();\n return { body: compressed, gzip: true };\n } catch {\n return plain;\n }\n};\n\nconst toWire = (chunk: ReplayChunk): WireChunk => ({\n events: chunk.events,\n from: chunk.from,\n seq: chunk.seq,\n to: chunk.to,\n});\n\n// Group chunks into size-bounded batches so no single POST exceeds a gateway\n// cap (ingest must be idempotent on (replayId, seq), which storeReplay is).\nconst batchByBytes = (chunks: WireChunk[], maxBytes: number): WireChunk[][] => {\n const batches: WireChunk[][] = [];\n let current: WireChunk[] = [];\n let bytes = 0;\n const flush = (): void => {\n if (current.length > 0) batches.push(current);\n current = [];\n bytes = 0;\n };\n for (const chunk of chunks) {\n const size = JSON.stringify(chunk).length;\n if (current.length > 0 && bytes + size > maxBytes) flush();\n current.push(chunk);\n bytes += size;\n }\n flush();\n return batches;\n};\n\nexport const createReplayController = (\n options: ReplayControllerOptions,\n): ReplayController => {\n const maxRing = options.maxRingChunks ?? RING_CHUNKS_DEFAULT;\n const throttleMs = options.flushThrottleMs ?? FLUSH_THROTTLE_DEFAULT_MS;\n const maxBatchBytes = options.maxBatchBytes ?? BATCH_BYTES_DEFAULT;\n const maxTailBytes = options.maxTailBytes ?? TAIL_BYTES_DEFAULT;\n const doFetch = options.fetch ?? globalThis.fetch;\n\n const ring: ReplayChunk[] = [];\n let lastFlush = 0;\n // True once a report/error/unload made this session worth keeping.\n let sessionMatters = false;\n\n const recorder = createRecorder({\n project: options.project,\n upload: (chunk) => {\n ring.push(chunk);\n while (ring.length > maxRing) ring.shift();\n },\n ...(options.release !== undefined ? { release: options.release } : {}),\n ...(options.environment !== undefined\n ? { environment: options.environment }\n : {}),\n ...(options.recorder ?? {}),\n });\n\n const postBatch = async (\n batch: WireChunk[],\n manifest: ReplayManifest,\n ): Promise<void> => {\n const { body, gzip } = await encodeBody(\n JSON.stringify({ chunks: batch, manifest }),\n );\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n };\n if (gzip) headers[\"content-encoding\"] = \"gzip\";\n await doFetch(options.endpoint, {\n body,\n credentials: \"include\",\n headers,\n method: \"POST\",\n });\n };\n\n const flush = async (): Promise<string | null> => {\n if (typeof window === \"undefined\") return null;\n sessionMatters = true;\n await recorder.flush();\n if (ring.length === 0) return recorder.replayId;\n const manifest = recorder.manifest();\n const wire = ring.map(toWire);\n try {\n await Promise.all(\n batchByBytes(wire, maxBatchBytes).map((batch) =>\n postBatch(batch, manifest),\n ),\n );\n lastFlush = Date.now();\n } catch {\n // Best-effort upload — still return the replayId so the report can link\n // the session (a later chunk / error flush can still populate it).\n }\n return recorder.replayId;\n };\n\n const flushThrottled = (): void => {\n if (Date.now() - lastFlush < throttleMs) return;\n lastFlush = Date.now();\n void flush();\n };\n\n const flushOnUnload = (): void => {\n if (!sessionMatters || typeof window === \"undefined\") return;\n const tail: ReplayChunk[] = [];\n let bytes = 0;\n for (let index = ring.length - 1; index >= 0; index -= 1) {\n const chunk = ring[index];\n if (chunk === undefined) continue;\n const size = JSON.stringify(chunk).length;\n if (tail.length > 0 && bytes + size > maxTailBytes) break;\n tail.unshift(chunk);\n bytes += size;\n }\n if (tail.length === 0) return;\n void doFetch(options.endpoint, {\n body: JSON.stringify({\n chunks: tail.map(toWire),\n manifest: recorder.manifest(),\n }),\n headers: { \"content-type\": \"application/json\" },\n keepalive: true,\n method: \"POST\",\n }).catch(() => undefined);\n };\n\n return {\n flush,\n flushOnUnload,\n flushThrottled,\n getReplayId: () => recorder.replayId,\n stop: () => recorder.stop(),\n };\n};\n\n// =============================================================================\n// Playback\n// =============================================================================\n\n/** Re-assemble a session's chunks into a single ordered event stream. */\nexport const assembleReplay = (chunks: ReplayChunk[]): ReplayEvent[] =>\n [...chunks].sort((a, b) => a.seq - b.seq).flatMap((chunk) => chunk.events);\n\nexport type ReplayPlayerOptions = {\n /** Element to mount the replay into. */\n target: Element;\n /** The assembled event stream (see `assembleReplay`). */\n events: ReplayEvent[];\n /** Inject rrweb's `Replayer` (default: lazy-imported). */\n Replayer?: RrwebReplayerConstructor;\n /** Start playing immediately. Default true. */\n autoplay?: boolean;\n speed?: number;\n};\n\nexport type ReplayPlayer = {\n play: (timeOffset?: number) => void;\n pause: () => void;\n destroy: () => void;\n};\n\nexport const createReplayPlayer = async (\n options: ReplayPlayerOptions,\n): Promise<ReplayPlayer> => {\n const Replayer = options.Replayer ?? (await loadRrwebReplayer());\n const replayer = new Replayer(options.events, {\n root: options.target,\n ...(options.speed !== undefined ? { speed: options.speed } : {}),\n });\n if (options.autoplay !== false) replayer.play();\n return {\n destroy: () => replayer.destroy?.(),\n pause: () => replayer.pause(),\n play: (timeOffset) => replayer.play(timeOffset),\n };\n};\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "5AEB3D7140DBAB4464756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "828F915630AC5DEC64756E2164756E21",
9
9
  "names": []
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/replay",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "Session replay for the AbsoluteJS observability stack. A tiny zero-hard-dependency recorder (rrweb is an optional, lazy-loaded peer) that chunks DOM recordings and uploads them via a pluggable transport (@absolutejs/blob), privacy-masking by default. Plus chunk assembly + a framework-agnostic player. Stamps a replayId for @absolutejs/beacon to cross-link errors to the exact session.",
5
5
  "repository": {
6
6
  "type": "git",