@absolutejs/replay 0.0.1
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/LICENSE +94 -0
- package/README.md +90 -0
- package/dist/index.d.ts +134 -0
- package/dist/index.js +153 -0
- package/dist/index.js.map +10 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
**Licensor:** Alex Kahn
|
|
4
|
+
|
|
5
|
+
**Licensed Work:** @absolutejs/replay (https://github.com/absolutejs/replay)
|
|
6
|
+
|
|
7
|
+
**Change Date:** June 24, 2030
|
|
8
|
+
|
|
9
|
+
**Change License:** Apache License, Version 2.0
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Terms
|
|
14
|
+
|
|
15
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
16
|
+
works, redistribute, and make non-production use of the Licensed Work. The
|
|
17
|
+
Licensor may make an Additional Use Grant, permitting limited production use.
|
|
18
|
+
|
|
19
|
+
### Additional Use Grant
|
|
20
|
+
|
|
21
|
+
You may use the Licensed Work in production, provided your use does not include
|
|
22
|
+
any of the following:
|
|
23
|
+
|
|
24
|
+
1. **Offering a Competing Service.** You may not offer the Licensed Work, or
|
|
25
|
+
any derivative or substantial portion of it, to third parties as a hosted or
|
|
26
|
+
managed error-tracking, exception-monitoring, crash-reporting, session-replay,
|
|
27
|
+
or real-user-monitoring (RUM) service that competes with hosted observability
|
|
28
|
+
offerings (including, but not limited to, Sentry, Datadog, LogRocket, Bugsnag,
|
|
29
|
+
Rollbar, Honeybadger, Airbrake, Raygun, TrackJS, AppSignal, New Relic Browser,
|
|
30
|
+
FullStory, Highlight, or any similar hosted offering whose primary value to its
|
|
31
|
+
users is capturing client-side errors, breadcrumbs, or sessions from end-user
|
|
32
|
+
browsers). This includes any product whose primary value to its users is the
|
|
33
|
+
functionality the Licensed Work provides.
|
|
34
|
+
|
|
35
|
+
2. **Resale or Redistribution as a Standalone Product.** You may not sell,
|
|
36
|
+
license, or distribute the Licensed Work, or any derivative or fork of it,
|
|
37
|
+
as a standalone commercial product.
|
|
38
|
+
|
|
39
|
+
3. **Removal of Attribution.** Any derivative work, fork, or redistribution of
|
|
40
|
+
the Licensed Work must prominently credit AbsoluteJS and include a link to
|
|
41
|
+
the original project repository (https://github.com/absolutejs/replay).
|
|
42
|
+
|
|
43
|
+
For clarity, the following uses are expressly permitted:
|
|
44
|
+
|
|
45
|
+
- Using the Licensed Work to capture errors from your own applications,
|
|
46
|
+
websites, internal tools, or SaaS products (whether commercial or non-
|
|
47
|
+
commercial), so long as the Licensed Work itself is not the primary product
|
|
48
|
+
you are selling.
|
|
49
|
+
- Using the Licensed Work as a dependency in commercial software you build and
|
|
50
|
+
sell, as long as the software is not itself a competing hosted observability
|
|
51
|
+
service of the kind described in clause 1.
|
|
52
|
+
- Providing consulting, development, or professional services to clients using
|
|
53
|
+
the Licensed Work.
|
|
54
|
+
- Forking and modifying the Licensed Work for your own internal use, provided
|
|
55
|
+
attribution is maintained.
|
|
56
|
+
|
|
57
|
+
### Change Date and Change License
|
|
58
|
+
|
|
59
|
+
On the Change Date specified above, or on such other date as the Licensor may
|
|
60
|
+
specify by written notice, the Licensed Work will be made available under the
|
|
61
|
+
Change License (Apache License, Version 2.0). Until the Change Date, the terms
|
|
62
|
+
of this Business Source License 1.1 apply.
|
|
63
|
+
|
|
64
|
+
### Trademark
|
|
65
|
+
|
|
66
|
+
This license does not grant you any rights to use the "AbsoluteJS" or
|
|
67
|
+
"@absolutejs" name, logo, or any related trademarks. Forks and derivative works
|
|
68
|
+
must not be named or branded in a manner that suggests endorsement by or
|
|
69
|
+
affiliation with AbsoluteJS or the Licensor.
|
|
70
|
+
|
|
71
|
+
### Notices
|
|
72
|
+
|
|
73
|
+
You must not remove or obscure any licensing, copyright, or other notices
|
|
74
|
+
included in the Licensed Work.
|
|
75
|
+
|
|
76
|
+
### No Warranty
|
|
77
|
+
|
|
78
|
+
THE LICENSED WORK IS PROVIDED "AS IS". THE LICENSOR HEREBY DISCLAIMS ALL
|
|
79
|
+
WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF
|
|
80
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO
|
|
81
|
+
EVENT SHALL THE LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY,
|
|
82
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR
|
|
83
|
+
IN CONNECTION WITH THE LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE
|
|
84
|
+
LICENSED WORK.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Contact
|
|
89
|
+
|
|
90
|
+
For commercial licensing inquiries or additional permissions, contact:
|
|
91
|
+
|
|
92
|
+
- **Alex Kahn**
|
|
93
|
+
- alexkahndev@gmail.com
|
|
94
|
+
- alexkahndev.github.io
|
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @absolutejs/replay
|
|
2
|
+
|
|
3
|
+
> Session replay for the AbsoluteJS observability stack. ~1 KB of glue;
|
|
4
|
+
> [rrweb](https://github.com/rrweb-io/rrweb) is an optional, lazy-loaded peer.
|
|
5
|
+
|
|
6
|
+
Records DOM sessions, chunks them, and uploads each chunk via a pluggable
|
|
7
|
+
transport (wire [`@absolutejs/blob`](https://www.npmjs.com/package/@absolutejs/blob)).
|
|
8
|
+
Exposes a `replayId` so [`@absolutejs/beacon`](https://www.npmjs.com/package/@absolutejs/beacon)
|
|
9
|
+
can stamp every error with the session — cross-linking an issue to the **exact**
|
|
10
|
+
DOM replay around it. Re-assembles chunks for a framework-agnostic player.
|
|
11
|
+
|
|
12
|
+
## Design
|
|
13
|
+
|
|
14
|
+
- **Zero hard dependencies.** DOM recording genuinely needs a heavy engine, so
|
|
15
|
+
the recorder wraps rrweb — but rrweb is an **optional peer**, lazy-imported
|
|
16
|
+
only when you start recording (and fully injectable). Replay is the one heavy
|
|
17
|
+
feature; its weight never lands on a page that isn't recording.
|
|
18
|
+
- **Plain TS, not Effect** — like `beacon`, it's browser-first where bytes are
|
|
19
|
+
the cost. Replay's own code is ~1 KB gz.
|
|
20
|
+
- **Private by default** — inputs are masked (`maskAllInputs: true`). Recording
|
|
21
|
+
user sessions is a real liability surface; keep masking on.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
bun add @absolutejs/replay rrweb
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Record
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { createRecorder } from "@absolutejs/replay";
|
|
33
|
+
import { initBeacon } from "@absolutejs/beacon";
|
|
34
|
+
|
|
35
|
+
const recorder = createRecorder({
|
|
36
|
+
project: "web",
|
|
37
|
+
release: import.meta.env.VITE_RELEASE,
|
|
38
|
+
upload: (chunk) =>
|
|
39
|
+
uploadToBlob(
|
|
40
|
+
`replays/${chunk.replayId}/${chunk.seq}.json`,
|
|
41
|
+
JSON.stringify(chunk),
|
|
42
|
+
),
|
|
43
|
+
// privacy defaults: maskAllInputs: true, blockClass: 'rr-block', maskTextClass: 'rr-mask'
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Cross-link errors → this session:
|
|
47
|
+
initBeacon({ project: "web", getReplayId: () => recorder.replayId });
|
|
48
|
+
|
|
49
|
+
// On error, flush the tail so the replay around it is stored:
|
|
50
|
+
window.addEventListener("error", () => void recorder.flush());
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Add `class="rr-block"` to a node to skip recording it, or `class="rr-mask"` to
|
|
54
|
+
mask its text. Use `maskAllText: true` for high-sensitivity apps.
|
|
55
|
+
|
|
56
|
+
## Play back
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { assembleReplay, createReplayPlayer } from "@absolutejs/replay";
|
|
60
|
+
|
|
61
|
+
const chunks = await loadChunksFromBlob(replayId); // your storage read
|
|
62
|
+
const player = await createReplayPlayer({
|
|
63
|
+
target: document.getElementById("replay")!,
|
|
64
|
+
events: assembleReplay(chunks), // ordered + flattened
|
|
65
|
+
});
|
|
66
|
+
player.pause();
|
|
67
|
+
player.play(0);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## API
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
createRecorder(options) => Recorder
|
|
74
|
+
// Recorder: { replayId, manifest(), flush(), stop() }
|
|
75
|
+
// options: project, upload, release?, environment?, replayId?,
|
|
76
|
+
// chunkIntervalMs? (5000), chunkMaxEvents? (200),
|
|
77
|
+
// maskAllInputs? (true), maskAllText? (false), blockClass?, maskTextClass?,
|
|
78
|
+
// recordCanvas?, record? (inject rrweb), onError?
|
|
79
|
+
|
|
80
|
+
assembleReplay(chunks) => ReplayEvent[] // sort by seq, flatten
|
|
81
|
+
createReplayPlayer({ target, events, Replayer?, autoplay?, speed? }) => Promise<ReplayPlayer>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
SSR-safe: imported without a DOM, `createRecorder` returns a no-op handle (with
|
|
85
|
+
a valid `replayId`/`manifest`).
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
BSL-1.1 with a named carveout against hosted session-replay / observability
|
|
90
|
+
SaaS (LogRocket, FullStory, Sentry Replay, Datadog). See `LICENSE`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @absolutejs/replay — session replay for the AbsoluteJS observability stack.
|
|
3
|
+
*
|
|
4
|
+
* DOM recording genuinely needs a heavy, hard-to-replicate engine, so the
|
|
5
|
+
* recorder wraps **rrweb** — but rrweb is an **optional, lazy-loaded peer**
|
|
6
|
+
* (and fully injectable), so:
|
|
7
|
+
* - this package has ZERO hard dependencies; rrweb is only pulled when you
|
|
8
|
+
* actually start recording, and only into the replay code path (opt-in
|
|
9
|
+
* weight — replay is the one heavy feature, never on a page that isn't
|
|
10
|
+
* recording).
|
|
11
|
+
* - it's plain TS, NOT Effect — like @absolutejs/beacon, it's browser-first
|
|
12
|
+
* where bytes are the cost; the server-side rigor lives in the ingest /
|
|
13
|
+
* storage layers.
|
|
14
|
+
*
|
|
15
|
+
* Pipeline: rrweb emits events → buffered → chunked (by size/interval) →
|
|
16
|
+
* uploaded via a pluggable `upload` transport (wire `@absolutejs/blob`). A
|
|
17
|
+
* `replayId` is exposed synchronously so `@absolutejs/beacon`'s `getReplayId`
|
|
18
|
+
* seam can stamp every error with the session — cross-linking an issue to its
|
|
19
|
+
* exact DOM replay. Playback re-assembles chunks and feeds rrweb's `Replayer`.
|
|
20
|
+
*
|
|
21
|
+
* PRIVACY: inputs are masked by default (`maskAllInputs: true`). Recording user
|
|
22
|
+
* sessions is a real liability surface — keep masking on, add `blockClass` /
|
|
23
|
+
* `maskTextClass` to sensitive nodes, and use `maskAllText` for high-sensitivity
|
|
24
|
+
* apps.
|
|
25
|
+
*/
|
|
26
|
+
/** An rrweb event. Opaque to us — we only buffer/transport/replay it. */
|
|
27
|
+
export type ReplayEvent = {
|
|
28
|
+
type: number;
|
|
29
|
+
timestamp: number;
|
|
30
|
+
data: unknown;
|
|
31
|
+
};
|
|
32
|
+
export type RecordConfig = {
|
|
33
|
+
emit: (event: ReplayEvent) => void;
|
|
34
|
+
maskAllInputs?: boolean;
|
|
35
|
+
maskTextSelector?: string;
|
|
36
|
+
blockClass?: string;
|
|
37
|
+
maskTextClass?: string;
|
|
38
|
+
recordCanvas?: boolean;
|
|
39
|
+
};
|
|
40
|
+
/** rrweb's `record` — returns a stop handler. */
|
|
41
|
+
export type RrwebRecord = (config: RecordConfig) => (() => void) | undefined;
|
|
42
|
+
export type RrwebReplayerInstance = {
|
|
43
|
+
play: (timeOffset?: number) => void;
|
|
44
|
+
pause: () => void;
|
|
45
|
+
destroy?: () => void;
|
|
46
|
+
};
|
|
47
|
+
/** rrweb's `Replayer` constructor. */
|
|
48
|
+
export type RrwebReplayerConstructor = new (events: ReplayEvent[], config?: {
|
|
49
|
+
root?: Element;
|
|
50
|
+
speed?: number;
|
|
51
|
+
}) => RrwebReplayerInstance;
|
|
52
|
+
/** A contiguous slice of a session's events — the unit uploaded to storage. */
|
|
53
|
+
export type ReplayChunk = {
|
|
54
|
+
replayId: string;
|
|
55
|
+
project: string;
|
|
56
|
+
/** Monotonic chunk index within the session (0-based). */
|
|
57
|
+
seq: number;
|
|
58
|
+
/** First event timestamp in this chunk. */
|
|
59
|
+
from: number;
|
|
60
|
+
/** Last event timestamp in this chunk. */
|
|
61
|
+
to: number;
|
|
62
|
+
events: ReplayEvent[];
|
|
63
|
+
};
|
|
64
|
+
/** Session-level metadata — pair with the chunk keys to locate a replay. */
|
|
65
|
+
export type ReplayManifest = {
|
|
66
|
+
replayId: string;
|
|
67
|
+
project: string;
|
|
68
|
+
startedAt: number;
|
|
69
|
+
release?: string;
|
|
70
|
+
environment?: string;
|
|
71
|
+
chunkCount: number;
|
|
72
|
+
durationMs: number;
|
|
73
|
+
};
|
|
74
|
+
/** Persist one chunk — wire `@absolutejs/blob` (or any object store) here. */
|
|
75
|
+
export type ChunkUpload = (chunk: ReplayChunk) => void | Promise<void>;
|
|
76
|
+
export type RecorderOptions = {
|
|
77
|
+
project: string;
|
|
78
|
+
/** Called for each chunk as it's flushed. */
|
|
79
|
+
upload: ChunkUpload;
|
|
80
|
+
/** Override the generated session id. */
|
|
81
|
+
replayId?: string;
|
|
82
|
+
release?: string;
|
|
83
|
+
environment?: string;
|
|
84
|
+
/** Flush a chunk at least this often (ms). Default 5000. */
|
|
85
|
+
chunkIntervalMs?: number;
|
|
86
|
+
/** Flush once this many events buffer. Default 200. */
|
|
87
|
+
chunkMaxEvents?: number;
|
|
88
|
+
/** Mask all input values (privacy). Default **true**. */
|
|
89
|
+
maskAllInputs?: boolean;
|
|
90
|
+
/** Mask all text content (high-sensitivity). Default false. */
|
|
91
|
+
maskAllText?: boolean;
|
|
92
|
+
/** CSS class whose subtrees are not recorded. Default `'rr-block'`. */
|
|
93
|
+
blockClass?: string;
|
|
94
|
+
/** CSS class whose text is masked. Default `'rr-mask'`. */
|
|
95
|
+
maskTextClass?: string;
|
|
96
|
+
/** Record `<canvas>` (heavier). Default false. */
|
|
97
|
+
recordCanvas?: boolean;
|
|
98
|
+
/** Inject rrweb's `record` (default: lazy-imported). */
|
|
99
|
+
record?: RrwebRecord;
|
|
100
|
+
/** Override `Date.now()` for tests. */
|
|
101
|
+
clock?: () => number;
|
|
102
|
+
/** Hook for upload / recorder failures (best-effort; never throws to the app). */
|
|
103
|
+
onError?: (error: unknown) => void;
|
|
104
|
+
};
|
|
105
|
+
export type Recorder = {
|
|
106
|
+
/** The session id — feed to `@absolutejs/beacon`'s `getReplayId`. */
|
|
107
|
+
replayId: string;
|
|
108
|
+
/** Current session metadata snapshot. */
|
|
109
|
+
manifest: () => ReplayManifest;
|
|
110
|
+
/** Force-flush the buffered events as a chunk now. */
|
|
111
|
+
flush: () => Promise<void>;
|
|
112
|
+
/** Stop recording and flush the final chunk. */
|
|
113
|
+
stop: () => Promise<void>;
|
|
114
|
+
};
|
|
115
|
+
export declare const createRecorder: (options: RecorderOptions) => Recorder;
|
|
116
|
+
/** Re-assemble a session's chunks into a single ordered event stream. */
|
|
117
|
+
export declare const assembleReplay: (chunks: ReplayChunk[]) => ReplayEvent[];
|
|
118
|
+
export type ReplayPlayerOptions = {
|
|
119
|
+
/** Element to mount the replay into. */
|
|
120
|
+
target: Element;
|
|
121
|
+
/** The assembled event stream (see `assembleReplay`). */
|
|
122
|
+
events: ReplayEvent[];
|
|
123
|
+
/** Inject rrweb's `Replayer` (default: lazy-imported). */
|
|
124
|
+
Replayer?: RrwebReplayerConstructor;
|
|
125
|
+
/** Start playing immediately. Default true. */
|
|
126
|
+
autoplay?: boolean;
|
|
127
|
+
speed?: number;
|
|
128
|
+
};
|
|
129
|
+
export type ReplayPlayer = {
|
|
130
|
+
play: (timeOffset?: number) => void;
|
|
131
|
+
pause: () => void;
|
|
132
|
+
destroy: () => void;
|
|
133
|
+
};
|
|
134
|
+
export declare const createReplayPlayer: (options: ReplayPlayerOptions) => Promise<ReplayPlayer>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined")
|
|
5
|
+
return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/index.ts
|
|
10
|
+
var newId = () => {
|
|
11
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
12
|
+
return crypto.randomUUID();
|
|
13
|
+
}
|
|
14
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
15
|
+
};
|
|
16
|
+
var loadRrwebRecord = async () => {
|
|
17
|
+
try {
|
|
18
|
+
const mod = await import("rrweb");
|
|
19
|
+
return mod.record;
|
|
20
|
+
} catch (cause) {
|
|
21
|
+
throw new Error("[replay] rrweb is not installed. Run `bun add rrweb`, or pass `record` to createRecorder.", { cause });
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var loadRrwebReplayer = async () => {
|
|
25
|
+
try {
|
|
26
|
+
const mod = await import("rrweb");
|
|
27
|
+
return mod.Replayer;
|
|
28
|
+
} catch (cause) {
|
|
29
|
+
throw new Error("[replay] rrweb is not installed. Run `bun add rrweb`, or pass `Replayer` to createReplayPlayer.", { cause });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var createRecorder = (options) => {
|
|
33
|
+
const replayId = options.replayId ?? newId();
|
|
34
|
+
const clock = options.clock ?? Date.now;
|
|
35
|
+
const startedAt = clock();
|
|
36
|
+
const baseManifest = (chunkCount2, durationMs) => ({
|
|
37
|
+
chunkCount: chunkCount2,
|
|
38
|
+
durationMs,
|
|
39
|
+
project: options.project,
|
|
40
|
+
replayId,
|
|
41
|
+
startedAt,
|
|
42
|
+
...options.release !== undefined ? { release: options.release } : {},
|
|
43
|
+
...options.environment !== undefined ? { environment: options.environment } : {}
|
|
44
|
+
});
|
|
45
|
+
if (typeof window === "undefined") {
|
|
46
|
+
return {
|
|
47
|
+
flush: async () => {},
|
|
48
|
+
manifest: () => baseManifest(0, 0),
|
|
49
|
+
replayId,
|
|
50
|
+
stop: async () => {}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const chunkMaxEvents = options.chunkMaxEvents ?? 200;
|
|
54
|
+
const chunkIntervalMs = options.chunkIntervalMs ?? 5000;
|
|
55
|
+
const onError = options.onError ?? (() => {});
|
|
56
|
+
let buffer = [];
|
|
57
|
+
let seq = 0;
|
|
58
|
+
let chunkCount = 0;
|
|
59
|
+
let lastTimestamp = startedAt;
|
|
60
|
+
let stopFn;
|
|
61
|
+
let stopped = false;
|
|
62
|
+
const flush = async () => {
|
|
63
|
+
if (buffer.length === 0)
|
|
64
|
+
return;
|
|
65
|
+
const events = buffer;
|
|
66
|
+
buffer = [];
|
|
67
|
+
const chunk = {
|
|
68
|
+
events,
|
|
69
|
+
from: events[0].timestamp,
|
|
70
|
+
project: options.project,
|
|
71
|
+
replayId,
|
|
72
|
+
seq: seq++,
|
|
73
|
+
to: events[events.length - 1].timestamp
|
|
74
|
+
};
|
|
75
|
+
chunkCount += 1;
|
|
76
|
+
try {
|
|
77
|
+
await options.upload(chunk);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
onError(error);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const emit = (event) => {
|
|
83
|
+
buffer.push(event);
|
|
84
|
+
lastTimestamp = event.timestamp;
|
|
85
|
+
if (buffer.length >= chunkMaxEvents)
|
|
86
|
+
flush();
|
|
87
|
+
};
|
|
88
|
+
const config = {
|
|
89
|
+
blockClass: options.blockClass ?? "rr-block",
|
|
90
|
+
emit,
|
|
91
|
+
maskAllInputs: options.maskAllInputs ?? true,
|
|
92
|
+
maskTextClass: options.maskTextClass ?? "rr-mask",
|
|
93
|
+
...options.maskAllText === true ? { maskTextSelector: "*" } : {},
|
|
94
|
+
...options.recordCanvas === true ? { recordCanvas: true } : {}
|
|
95
|
+
};
|
|
96
|
+
const start = (record) => {
|
|
97
|
+
if (stopped)
|
|
98
|
+
return;
|
|
99
|
+
try {
|
|
100
|
+
stopFn = record(config) ?? undefined;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
onError(error);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
if (options.record !== undefined)
|
|
106
|
+
start(options.record);
|
|
107
|
+
else
|
|
108
|
+
loadRrwebRecord().then(start).catch(onError);
|
|
109
|
+
const timer = setInterval(() => {
|
|
110
|
+
flush();
|
|
111
|
+
}, chunkIntervalMs);
|
|
112
|
+
timer.unref?.();
|
|
113
|
+
return {
|
|
114
|
+
flush,
|
|
115
|
+
manifest: () => baseManifest(chunkCount, Math.max(0, lastTimestamp - startedAt)),
|
|
116
|
+
replayId,
|
|
117
|
+
stop: async () => {
|
|
118
|
+
stopped = true;
|
|
119
|
+
clearInterval(timer);
|
|
120
|
+
if (stopFn !== undefined) {
|
|
121
|
+
try {
|
|
122
|
+
stopFn();
|
|
123
|
+
} catch (error) {
|
|
124
|
+
onError(error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
await flush();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
var assembleReplay = (chunks) => [...chunks].sort((a, b) => a.seq - b.seq).flatMap((chunk) => chunk.events);
|
|
132
|
+
var createReplayPlayer = async (options) => {
|
|
133
|
+
const Replayer = options.Replayer ?? await loadRrwebReplayer();
|
|
134
|
+
const replayer = new Replayer(options.events, {
|
|
135
|
+
root: options.target,
|
|
136
|
+
...options.speed !== undefined ? { speed: options.speed } : {}
|
|
137
|
+
});
|
|
138
|
+
if (options.autoplay !== false)
|
|
139
|
+
replayer.play();
|
|
140
|
+
return {
|
|
141
|
+
destroy: () => replayer.destroy?.(),
|
|
142
|
+
pause: () => replayer.pause(),
|
|
143
|
+
play: (timeOffset) => replayer.play(timeOffset)
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
export {
|
|
147
|
+
createReplayPlayer,
|
|
148
|
+
createRecorder,
|
|
149
|
+
assembleReplay
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
//# debugId=5AEB3D7140DBAB4464756E2164756E21
|
|
153
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts"],
|
|
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"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "5AEB3D7140DBAB4464756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@absolutejs/replay",
|
|
3
|
+
"version": "0.0.1",
|
|
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
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/absolutejs/replay.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/absolutejs/replay",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/absolutejs/replay/issues"
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"module": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"license": "BSL-1.1",
|
|
18
|
+
"author": "Alex Kahn",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "rm -rf dist && bun build src/index.ts --outdir dist --sourcemap --target=browser --external rrweb && tsc --project tsconfig.build.json",
|
|
35
|
+
"test": "bun test",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"format": "prettier --write \"./**/*.{ts,json,md}\"",
|
|
38
|
+
"check:package": "bun run typecheck && bun run build && bun run test",
|
|
39
|
+
"release": "bun run format && bun run check:package && bun publish"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"absolutejs",
|
|
43
|
+
"replay",
|
|
44
|
+
"session-replay",
|
|
45
|
+
"rrweb",
|
|
46
|
+
"browser",
|
|
47
|
+
"observability",
|
|
48
|
+
"rum",
|
|
49
|
+
"logrocket"
|
|
50
|
+
],
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"rrweb": "^2.0.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"rrweb": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@happy-dom/global-registrator": "^20.10.6",
|
|
61
|
+
"@types/bun": "^1.3.14",
|
|
62
|
+
"prettier": "^3.8.3",
|
|
63
|
+
"rrweb": "^2.0.1",
|
|
64
|
+
"typescript": "^5.9.0"
|
|
65
|
+
}
|
|
66
|
+
}
|