@codmir/sdk 0.1.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/README.md +206 -0
- package/dist/browser/index.cjs +409 -0
- package/dist/browser/index.d.cts +47 -0
- package/dist/browser/index.d.ts +47 -0
- package/dist/browser/index.js +186 -0
- package/dist/chunk-233XBWQD.js +43 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-T7OAAOG2.js +235 -0
- package/dist/chunk-X6Y5XEK5.js +255 -0
- package/dist/client.cjs +315 -0
- package/dist/client.d.cts +52 -0
- package/dist/client.d.ts +52 -0
- package/dist/client.js +10 -0
- package/dist/index-BlgYnCLd.d.cts +171 -0
- package/dist/index-BlgYnCLd.d.ts +171 -0
- package/dist/index.cjs +540 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +21 -0
- package/dist/nextjs/index.cjs +371 -0
- package/dist/nextjs/index.d.cts +77 -0
- package/dist/nextjs/index.d.ts +77 -0
- package/dist/nextjs/index.js +142 -0
- package/dist/overseer/index.cjs +250 -0
- package/dist/overseer/index.d.cts +1 -0
- package/dist/overseer/index.d.ts +1 -0
- package/dist/overseer/index.js +27 -0
- package/dist/react-native/index.cjs +356 -0
- package/dist/react-native/index.d.cts +95 -0
- package/dist/react-native/index.d.ts +95 -0
- package/dist/react-native/index.js +128 -0
- package/dist/replay/index.cjs +284 -0
- package/dist/replay/index.d.cts +206 -0
- package/dist/replay/index.d.ts +206 -0
- package/dist/replay/index.js +258 -0
- package/dist/types.cjs +69 -0
- package/dist/types.d.cts +281 -0
- package/dist/types.d.ts +281 -0
- package/dist/types.js +11 -0
- package/package.json +116 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @codmir/sdk/replay - Session Replay
|
|
3
|
+
*
|
|
4
|
+
* Record and replay user sessions for debugging.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { ReplayRecorder, ReplayPlayer } from '@codmir/sdk/replay';
|
|
9
|
+
*
|
|
10
|
+
* // Recording
|
|
11
|
+
* const recorder = new ReplayRecorder({ sessionSampleRate: 0.1 });
|
|
12
|
+
* recorder.start();
|
|
13
|
+
*
|
|
14
|
+
* // Later: stop and get session
|
|
15
|
+
* const session = recorder.stop();
|
|
16
|
+
*
|
|
17
|
+
* // Playback
|
|
18
|
+
* const player = new ReplayPlayer({ container: document.getElementById('replay') });
|
|
19
|
+
* player.load(session);
|
|
20
|
+
* player.play();
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
interface ReplaySession {
|
|
24
|
+
id: string;
|
|
25
|
+
userId?: string;
|
|
26
|
+
startedAt: Date;
|
|
27
|
+
endedAt?: Date;
|
|
28
|
+
duration: number;
|
|
29
|
+
url: string;
|
|
30
|
+
userAgent: string;
|
|
31
|
+
viewport: {
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
};
|
|
35
|
+
events: ReplayEvent[];
|
|
36
|
+
errorIds: string[];
|
|
37
|
+
segments: ReplaySegment[];
|
|
38
|
+
tags: Record<string, string>;
|
|
39
|
+
}
|
|
40
|
+
interface ReplaySegment {
|
|
41
|
+
id: string;
|
|
42
|
+
sessionId: string;
|
|
43
|
+
sequenceNumber: number;
|
|
44
|
+
startTime: number;
|
|
45
|
+
endTime: number;
|
|
46
|
+
events: ReplayEvent[];
|
|
47
|
+
compressed?: boolean;
|
|
48
|
+
}
|
|
49
|
+
type ReplayEvent = DOMSnapshotEvent | DOMIncrementalEvent | MouseEvent | ScrollEvent | InputEvent | ViewportEvent | ConsoleEvent | NetworkEvent | CustomEvent;
|
|
50
|
+
interface BaseReplayEvent {
|
|
51
|
+
timestamp: number;
|
|
52
|
+
type: string;
|
|
53
|
+
}
|
|
54
|
+
interface DOMSnapshotEvent extends BaseReplayEvent {
|
|
55
|
+
type: "dom_snapshot";
|
|
56
|
+
data: {
|
|
57
|
+
html: string;
|
|
58
|
+
baseUrl: string;
|
|
59
|
+
doctype?: string;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
interface DOMIncrementalEvent extends BaseReplayEvent {
|
|
63
|
+
type: "dom_incremental";
|
|
64
|
+
data: {
|
|
65
|
+
mutations: DOMMutation[];
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
interface DOMMutation {
|
|
69
|
+
type: "add" | "remove" | "attribute" | "text";
|
|
70
|
+
targetId: number;
|
|
71
|
+
parentId?: number;
|
|
72
|
+
data?: unknown;
|
|
73
|
+
}
|
|
74
|
+
interface MouseEvent extends BaseReplayEvent {
|
|
75
|
+
type: "mouse_move" | "mouse_click" | "mouse_down" | "mouse_up";
|
|
76
|
+
data: {
|
|
77
|
+
x: number;
|
|
78
|
+
y: number;
|
|
79
|
+
targetId?: number;
|
|
80
|
+
button?: number;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
interface ScrollEvent extends BaseReplayEvent {
|
|
84
|
+
type: "scroll";
|
|
85
|
+
data: {
|
|
86
|
+
targetId: number | "window";
|
|
87
|
+
x: number;
|
|
88
|
+
y: number;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
interface InputEvent extends BaseReplayEvent {
|
|
92
|
+
type: "input";
|
|
93
|
+
data: {
|
|
94
|
+
targetId: number;
|
|
95
|
+
value: string;
|
|
96
|
+
masked?: boolean;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
interface ViewportEvent extends BaseReplayEvent {
|
|
100
|
+
type: "viewport";
|
|
101
|
+
data: {
|
|
102
|
+
width: number;
|
|
103
|
+
height: number;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
interface ConsoleEvent extends BaseReplayEvent {
|
|
107
|
+
type: "console";
|
|
108
|
+
data: {
|
|
109
|
+
level: "log" | "info" | "warn" | "error" | "debug";
|
|
110
|
+
args: unknown[];
|
|
111
|
+
stack?: string;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
interface NetworkEvent extends BaseReplayEvent {
|
|
115
|
+
type: "network";
|
|
116
|
+
data: {
|
|
117
|
+
requestId: string;
|
|
118
|
+
method: string;
|
|
119
|
+
url: string;
|
|
120
|
+
status?: number;
|
|
121
|
+
duration?: number;
|
|
122
|
+
error?: string;
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
interface CustomEvent extends BaseReplayEvent {
|
|
126
|
+
type: "custom";
|
|
127
|
+
data: {
|
|
128
|
+
name: string;
|
|
129
|
+
payload: unknown;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
interface ReplayPrivacyConfig {
|
|
133
|
+
maskAllInputs: boolean;
|
|
134
|
+
maskInputTypes: string[];
|
|
135
|
+
maskSelectors: string[];
|
|
136
|
+
blockSelectors: string[];
|
|
137
|
+
ignoreSelectors: string[];
|
|
138
|
+
}
|
|
139
|
+
interface ReplayConfig {
|
|
140
|
+
sessionSampleRate: number;
|
|
141
|
+
errorSampleRate: number;
|
|
142
|
+
maxSessionDuration: number;
|
|
143
|
+
segmentDuration: number;
|
|
144
|
+
privacy: ReplayPrivacyConfig;
|
|
145
|
+
beforeSend?: (session: ReplaySession) => ReplaySession | null;
|
|
146
|
+
}
|
|
147
|
+
type PlaybackState = "idle" | "playing" | "paused" | "ended";
|
|
148
|
+
type PlaybackSpeed = 0.5 | 1 | 2 | 4 | 8;
|
|
149
|
+
interface PlayerConfig {
|
|
150
|
+
container: HTMLElement;
|
|
151
|
+
speed?: PlaybackSpeed;
|
|
152
|
+
skipInactiveThreshold?: number;
|
|
153
|
+
showCursor?: boolean;
|
|
154
|
+
showClickRipple?: boolean;
|
|
155
|
+
highlightErrors?: boolean;
|
|
156
|
+
}
|
|
157
|
+
declare class ReplayRecorder {
|
|
158
|
+
private config;
|
|
159
|
+
private session;
|
|
160
|
+
private currentSegment;
|
|
161
|
+
private isRecording;
|
|
162
|
+
private sessionStartTime;
|
|
163
|
+
private cleanupFns;
|
|
164
|
+
private onSegmentReady?;
|
|
165
|
+
private onSessionEnd?;
|
|
166
|
+
constructor(config?: Partial<ReplayConfig>);
|
|
167
|
+
start(options?: {
|
|
168
|
+
userId?: string;
|
|
169
|
+
tags?: Record<string, string>;
|
|
170
|
+
}): string | null;
|
|
171
|
+
stop(): ReplaySession | null;
|
|
172
|
+
markError(errorId: string): void;
|
|
173
|
+
addCustomEvent(name: string, payload: unknown): void;
|
|
174
|
+
getSessionId(): string | null;
|
|
175
|
+
isActive(): boolean;
|
|
176
|
+
onSegment(handler: (segment: ReplaySegment) => void): void;
|
|
177
|
+
onSession(handler: (session: ReplaySession) => void): void;
|
|
178
|
+
private getRelativeTime;
|
|
179
|
+
private addEvent;
|
|
180
|
+
private startNewSegment;
|
|
181
|
+
private finalizeSegment;
|
|
182
|
+
private captureSnapshot;
|
|
183
|
+
private setupEventListeners;
|
|
184
|
+
}
|
|
185
|
+
declare class ReplayPlayer {
|
|
186
|
+
private config;
|
|
187
|
+
private session;
|
|
188
|
+
private state;
|
|
189
|
+
private currentTime;
|
|
190
|
+
private animationFrameId;
|
|
191
|
+
constructor(config: PlayerConfig);
|
|
192
|
+
load(session: ReplaySession): void;
|
|
193
|
+
play(): void;
|
|
194
|
+
pause(): void;
|
|
195
|
+
stop(): void;
|
|
196
|
+
seek(time: number): void;
|
|
197
|
+
setSpeed(speed: PlaybackSpeed): void;
|
|
198
|
+
getState(): PlaybackState;
|
|
199
|
+
getCurrentTime(): number;
|
|
200
|
+
getDuration(): number;
|
|
201
|
+
destroy(): void;
|
|
202
|
+
}
|
|
203
|
+
declare function createReplayRecorder(config?: Partial<ReplayConfig>): ReplayRecorder;
|
|
204
|
+
declare function createReplayPlayer(config: PlayerConfig): ReplayPlayer;
|
|
205
|
+
|
|
206
|
+
export { type ConsoleEvent, type CustomEvent, type DOMIncrementalEvent, type DOMMutation, type DOMSnapshotEvent, type InputEvent, type MouseEvent, type NetworkEvent, type PlaybackSpeed, type PlaybackState, type PlayerConfig, type ReplayConfig, type ReplayEvent, ReplayPlayer, type ReplayPrivacyConfig, ReplayRecorder, type ReplaySegment, type ReplaySession, type ScrollEvent, type ViewportEvent, createReplayPlayer, createReplayRecorder };
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @codmir/sdk/replay - Session Replay
|
|
3
|
+
*
|
|
4
|
+
* Record and replay user sessions for debugging.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { ReplayRecorder, ReplayPlayer } from '@codmir/sdk/replay';
|
|
9
|
+
*
|
|
10
|
+
* // Recording
|
|
11
|
+
* const recorder = new ReplayRecorder({ sessionSampleRate: 0.1 });
|
|
12
|
+
* recorder.start();
|
|
13
|
+
*
|
|
14
|
+
* // Later: stop and get session
|
|
15
|
+
* const session = recorder.stop();
|
|
16
|
+
*
|
|
17
|
+
* // Playback
|
|
18
|
+
* const player = new ReplayPlayer({ container: document.getElementById('replay') });
|
|
19
|
+
* player.load(session);
|
|
20
|
+
* player.play();
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
interface ReplaySession {
|
|
24
|
+
id: string;
|
|
25
|
+
userId?: string;
|
|
26
|
+
startedAt: Date;
|
|
27
|
+
endedAt?: Date;
|
|
28
|
+
duration: number;
|
|
29
|
+
url: string;
|
|
30
|
+
userAgent: string;
|
|
31
|
+
viewport: {
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
};
|
|
35
|
+
events: ReplayEvent[];
|
|
36
|
+
errorIds: string[];
|
|
37
|
+
segments: ReplaySegment[];
|
|
38
|
+
tags: Record<string, string>;
|
|
39
|
+
}
|
|
40
|
+
interface ReplaySegment {
|
|
41
|
+
id: string;
|
|
42
|
+
sessionId: string;
|
|
43
|
+
sequenceNumber: number;
|
|
44
|
+
startTime: number;
|
|
45
|
+
endTime: number;
|
|
46
|
+
events: ReplayEvent[];
|
|
47
|
+
compressed?: boolean;
|
|
48
|
+
}
|
|
49
|
+
type ReplayEvent = DOMSnapshotEvent | DOMIncrementalEvent | MouseEvent | ScrollEvent | InputEvent | ViewportEvent | ConsoleEvent | NetworkEvent | CustomEvent;
|
|
50
|
+
interface BaseReplayEvent {
|
|
51
|
+
timestamp: number;
|
|
52
|
+
type: string;
|
|
53
|
+
}
|
|
54
|
+
interface DOMSnapshotEvent extends BaseReplayEvent {
|
|
55
|
+
type: "dom_snapshot";
|
|
56
|
+
data: {
|
|
57
|
+
html: string;
|
|
58
|
+
baseUrl: string;
|
|
59
|
+
doctype?: string;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
interface DOMIncrementalEvent extends BaseReplayEvent {
|
|
63
|
+
type: "dom_incremental";
|
|
64
|
+
data: {
|
|
65
|
+
mutations: DOMMutation[];
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
interface DOMMutation {
|
|
69
|
+
type: "add" | "remove" | "attribute" | "text";
|
|
70
|
+
targetId: number;
|
|
71
|
+
parentId?: number;
|
|
72
|
+
data?: unknown;
|
|
73
|
+
}
|
|
74
|
+
interface MouseEvent extends BaseReplayEvent {
|
|
75
|
+
type: "mouse_move" | "mouse_click" | "mouse_down" | "mouse_up";
|
|
76
|
+
data: {
|
|
77
|
+
x: number;
|
|
78
|
+
y: number;
|
|
79
|
+
targetId?: number;
|
|
80
|
+
button?: number;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
interface ScrollEvent extends BaseReplayEvent {
|
|
84
|
+
type: "scroll";
|
|
85
|
+
data: {
|
|
86
|
+
targetId: number | "window";
|
|
87
|
+
x: number;
|
|
88
|
+
y: number;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
interface InputEvent extends BaseReplayEvent {
|
|
92
|
+
type: "input";
|
|
93
|
+
data: {
|
|
94
|
+
targetId: number;
|
|
95
|
+
value: string;
|
|
96
|
+
masked?: boolean;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
interface ViewportEvent extends BaseReplayEvent {
|
|
100
|
+
type: "viewport";
|
|
101
|
+
data: {
|
|
102
|
+
width: number;
|
|
103
|
+
height: number;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
interface ConsoleEvent extends BaseReplayEvent {
|
|
107
|
+
type: "console";
|
|
108
|
+
data: {
|
|
109
|
+
level: "log" | "info" | "warn" | "error" | "debug";
|
|
110
|
+
args: unknown[];
|
|
111
|
+
stack?: string;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
interface NetworkEvent extends BaseReplayEvent {
|
|
115
|
+
type: "network";
|
|
116
|
+
data: {
|
|
117
|
+
requestId: string;
|
|
118
|
+
method: string;
|
|
119
|
+
url: string;
|
|
120
|
+
status?: number;
|
|
121
|
+
duration?: number;
|
|
122
|
+
error?: string;
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
interface CustomEvent extends BaseReplayEvent {
|
|
126
|
+
type: "custom";
|
|
127
|
+
data: {
|
|
128
|
+
name: string;
|
|
129
|
+
payload: unknown;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
interface ReplayPrivacyConfig {
|
|
133
|
+
maskAllInputs: boolean;
|
|
134
|
+
maskInputTypes: string[];
|
|
135
|
+
maskSelectors: string[];
|
|
136
|
+
blockSelectors: string[];
|
|
137
|
+
ignoreSelectors: string[];
|
|
138
|
+
}
|
|
139
|
+
interface ReplayConfig {
|
|
140
|
+
sessionSampleRate: number;
|
|
141
|
+
errorSampleRate: number;
|
|
142
|
+
maxSessionDuration: number;
|
|
143
|
+
segmentDuration: number;
|
|
144
|
+
privacy: ReplayPrivacyConfig;
|
|
145
|
+
beforeSend?: (session: ReplaySession) => ReplaySession | null;
|
|
146
|
+
}
|
|
147
|
+
type PlaybackState = "idle" | "playing" | "paused" | "ended";
|
|
148
|
+
type PlaybackSpeed = 0.5 | 1 | 2 | 4 | 8;
|
|
149
|
+
interface PlayerConfig {
|
|
150
|
+
container: HTMLElement;
|
|
151
|
+
speed?: PlaybackSpeed;
|
|
152
|
+
skipInactiveThreshold?: number;
|
|
153
|
+
showCursor?: boolean;
|
|
154
|
+
showClickRipple?: boolean;
|
|
155
|
+
highlightErrors?: boolean;
|
|
156
|
+
}
|
|
157
|
+
declare class ReplayRecorder {
|
|
158
|
+
private config;
|
|
159
|
+
private session;
|
|
160
|
+
private currentSegment;
|
|
161
|
+
private isRecording;
|
|
162
|
+
private sessionStartTime;
|
|
163
|
+
private cleanupFns;
|
|
164
|
+
private onSegmentReady?;
|
|
165
|
+
private onSessionEnd?;
|
|
166
|
+
constructor(config?: Partial<ReplayConfig>);
|
|
167
|
+
start(options?: {
|
|
168
|
+
userId?: string;
|
|
169
|
+
tags?: Record<string, string>;
|
|
170
|
+
}): string | null;
|
|
171
|
+
stop(): ReplaySession | null;
|
|
172
|
+
markError(errorId: string): void;
|
|
173
|
+
addCustomEvent(name: string, payload: unknown): void;
|
|
174
|
+
getSessionId(): string | null;
|
|
175
|
+
isActive(): boolean;
|
|
176
|
+
onSegment(handler: (segment: ReplaySegment) => void): void;
|
|
177
|
+
onSession(handler: (session: ReplaySession) => void): void;
|
|
178
|
+
private getRelativeTime;
|
|
179
|
+
private addEvent;
|
|
180
|
+
private startNewSegment;
|
|
181
|
+
private finalizeSegment;
|
|
182
|
+
private captureSnapshot;
|
|
183
|
+
private setupEventListeners;
|
|
184
|
+
}
|
|
185
|
+
declare class ReplayPlayer {
|
|
186
|
+
private config;
|
|
187
|
+
private session;
|
|
188
|
+
private state;
|
|
189
|
+
private currentTime;
|
|
190
|
+
private animationFrameId;
|
|
191
|
+
constructor(config: PlayerConfig);
|
|
192
|
+
load(session: ReplaySession): void;
|
|
193
|
+
play(): void;
|
|
194
|
+
pause(): void;
|
|
195
|
+
stop(): void;
|
|
196
|
+
seek(time: number): void;
|
|
197
|
+
setSpeed(speed: PlaybackSpeed): void;
|
|
198
|
+
getState(): PlaybackState;
|
|
199
|
+
getCurrentTime(): number;
|
|
200
|
+
getDuration(): number;
|
|
201
|
+
destroy(): void;
|
|
202
|
+
}
|
|
203
|
+
declare function createReplayRecorder(config?: Partial<ReplayConfig>): ReplayRecorder;
|
|
204
|
+
declare function createReplayPlayer(config: PlayerConfig): ReplayPlayer;
|
|
205
|
+
|
|
206
|
+
export { type ConsoleEvent, type CustomEvent, type DOMIncrementalEvent, type DOMMutation, type DOMSnapshotEvent, type InputEvent, type MouseEvent, type NetworkEvent, type PlaybackSpeed, type PlaybackState, type PlayerConfig, type ReplayConfig, type ReplayEvent, ReplayPlayer, type ReplayPrivacyConfig, ReplayRecorder, type ReplaySegment, type ReplaySession, type ScrollEvent, type ViewportEvent, createReplayPlayer, createReplayRecorder };
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import "../chunk-MLKGABMK.js";
|
|
2
|
+
|
|
3
|
+
// src/replay/index.ts
|
|
4
|
+
import { nanoid } from "nanoid";
|
|
5
|
+
var DEFAULT_PRIVACY = {
|
|
6
|
+
maskAllInputs: false,
|
|
7
|
+
maskInputTypes: ["password", "credit-card"],
|
|
8
|
+
maskSelectors: ["[data-private]", ".private"],
|
|
9
|
+
blockSelectors: ["[data-block-replay]"],
|
|
10
|
+
ignoreSelectors: []
|
|
11
|
+
};
|
|
12
|
+
var DEFAULT_CONFIG = {
|
|
13
|
+
sessionSampleRate: 0.1,
|
|
14
|
+
errorSampleRate: 1,
|
|
15
|
+
maxSessionDuration: 60 * 60 * 1e3,
|
|
16
|
+
segmentDuration: 5 * 60 * 1e3,
|
|
17
|
+
privacy: DEFAULT_PRIVACY
|
|
18
|
+
};
|
|
19
|
+
var ReplayRecorder = class {
|
|
20
|
+
config;
|
|
21
|
+
session = null;
|
|
22
|
+
currentSegment = null;
|
|
23
|
+
isRecording = false;
|
|
24
|
+
sessionStartTime = 0;
|
|
25
|
+
cleanupFns = [];
|
|
26
|
+
onSegmentReady;
|
|
27
|
+
onSessionEnd;
|
|
28
|
+
constructor(config = {}) {
|
|
29
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
30
|
+
}
|
|
31
|
+
start(options) {
|
|
32
|
+
if (this.isRecording) return this.session?.id || null;
|
|
33
|
+
if (typeof window === "undefined") return null;
|
|
34
|
+
if (Math.random() > this.config.sessionSampleRate) return null;
|
|
35
|
+
this.sessionStartTime = Date.now();
|
|
36
|
+
this.session = {
|
|
37
|
+
id: nanoid(),
|
|
38
|
+
userId: options?.userId,
|
|
39
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
40
|
+
duration: 0,
|
|
41
|
+
url: window.location.href,
|
|
42
|
+
userAgent: navigator.userAgent,
|
|
43
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
44
|
+
events: [],
|
|
45
|
+
errorIds: [],
|
|
46
|
+
segments: [],
|
|
47
|
+
tags: options?.tags || {}
|
|
48
|
+
};
|
|
49
|
+
this.startNewSegment();
|
|
50
|
+
this.isRecording = true;
|
|
51
|
+
this.setupEventListeners();
|
|
52
|
+
this.captureSnapshot();
|
|
53
|
+
return this.session.id;
|
|
54
|
+
}
|
|
55
|
+
stop() {
|
|
56
|
+
if (!this.isRecording || !this.session) return null;
|
|
57
|
+
this.isRecording = false;
|
|
58
|
+
this.session.endedAt = /* @__PURE__ */ new Date();
|
|
59
|
+
this.session.duration = Date.now() - this.sessionStartTime;
|
|
60
|
+
if (this.currentSegment) {
|
|
61
|
+
this.currentSegment.endTime = this.session.duration;
|
|
62
|
+
this.session.segments.push(this.currentSegment);
|
|
63
|
+
this.onSegmentReady?.(this.currentSegment);
|
|
64
|
+
}
|
|
65
|
+
this.cleanupFns.forEach((fn) => fn());
|
|
66
|
+
this.cleanupFns = [];
|
|
67
|
+
let finalSession = this.session;
|
|
68
|
+
if (this.config.beforeSend) {
|
|
69
|
+
const processed = this.config.beforeSend(this.session);
|
|
70
|
+
if (!processed) {
|
|
71
|
+
this.session = null;
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
finalSession = processed;
|
|
75
|
+
}
|
|
76
|
+
this.onSessionEnd?.(finalSession);
|
|
77
|
+
const result = finalSession;
|
|
78
|
+
this.session = null;
|
|
79
|
+
this.currentSegment = null;
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
markError(errorId) {
|
|
83
|
+
this.session?.errorIds.push(errorId);
|
|
84
|
+
}
|
|
85
|
+
addCustomEvent(name, payload) {
|
|
86
|
+
this.addEvent({
|
|
87
|
+
type: "custom",
|
|
88
|
+
timestamp: this.getRelativeTime(),
|
|
89
|
+
data: { name, payload }
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
getSessionId() {
|
|
93
|
+
return this.session?.id || null;
|
|
94
|
+
}
|
|
95
|
+
isActive() {
|
|
96
|
+
return this.isRecording;
|
|
97
|
+
}
|
|
98
|
+
onSegment(handler) {
|
|
99
|
+
this.onSegmentReady = handler;
|
|
100
|
+
}
|
|
101
|
+
onSession(handler) {
|
|
102
|
+
this.onSessionEnd = handler;
|
|
103
|
+
}
|
|
104
|
+
getRelativeTime() {
|
|
105
|
+
return Date.now() - this.sessionStartTime;
|
|
106
|
+
}
|
|
107
|
+
addEvent(event) {
|
|
108
|
+
if (!this.isRecording || !this.currentSegment) return;
|
|
109
|
+
this.currentSegment.events.push(event);
|
|
110
|
+
if (event.timestamp - this.currentSegment.startTime >= this.config.segmentDuration) {
|
|
111
|
+
this.finalizeSegment();
|
|
112
|
+
this.startNewSegment();
|
|
113
|
+
}
|
|
114
|
+
if (event.timestamp >= this.config.maxSessionDuration) {
|
|
115
|
+
this.stop();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
startNewSegment() {
|
|
119
|
+
this.currentSegment = {
|
|
120
|
+
id: nanoid(),
|
|
121
|
+
sessionId: this.session.id,
|
|
122
|
+
sequenceNumber: this.session.segments.length,
|
|
123
|
+
startTime: this.getRelativeTime(),
|
|
124
|
+
endTime: 0,
|
|
125
|
+
events: []
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
finalizeSegment() {
|
|
129
|
+
if (!this.currentSegment || !this.session) return;
|
|
130
|
+
this.currentSegment.endTime = this.getRelativeTime();
|
|
131
|
+
this.session.segments.push(this.currentSegment);
|
|
132
|
+
this.onSegmentReady?.(this.currentSegment);
|
|
133
|
+
}
|
|
134
|
+
captureSnapshot() {
|
|
135
|
+
if (typeof document === "undefined") return;
|
|
136
|
+
this.addEvent({
|
|
137
|
+
type: "dom_snapshot",
|
|
138
|
+
timestamp: this.getRelativeTime(),
|
|
139
|
+
data: {
|
|
140
|
+
html: document.documentElement.outerHTML,
|
|
141
|
+
baseUrl: document.baseURI,
|
|
142
|
+
doctype: document.doctype ? `<!DOCTYPE ${document.doctype.name}>` : void 0
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
setupEventListeners() {
|
|
147
|
+
let lastMouseMove = 0;
|
|
148
|
+
const onMouseMove = (e) => {
|
|
149
|
+
if (Date.now() - lastMouseMove < 50) return;
|
|
150
|
+
lastMouseMove = Date.now();
|
|
151
|
+
this.addEvent({
|
|
152
|
+
type: "mouse_move",
|
|
153
|
+
timestamp: this.getRelativeTime(),
|
|
154
|
+
data: { x: e.clientX, y: e.clientY }
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
window.addEventListener("mousemove", onMouseMove);
|
|
158
|
+
this.cleanupFns.push(() => window.removeEventListener("mousemove", onMouseMove));
|
|
159
|
+
const onClick = (e) => {
|
|
160
|
+
this.addEvent({
|
|
161
|
+
type: "mouse_click",
|
|
162
|
+
timestamp: this.getRelativeTime(),
|
|
163
|
+
data: { x: e.clientX, y: e.clientY, button: e.button }
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
window.addEventListener("click", onClick);
|
|
167
|
+
this.cleanupFns.push(() => window.removeEventListener("click", onClick));
|
|
168
|
+
let lastScroll = 0;
|
|
169
|
+
const onScroll = () => {
|
|
170
|
+
if (Date.now() - lastScroll < 100) return;
|
|
171
|
+
lastScroll = Date.now();
|
|
172
|
+
this.addEvent({
|
|
173
|
+
type: "scroll",
|
|
174
|
+
timestamp: this.getRelativeTime(),
|
|
175
|
+
data: { targetId: "window", x: window.scrollX, y: window.scrollY }
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
window.addEventListener("scroll", onScroll);
|
|
179
|
+
this.cleanupFns.push(() => window.removeEventListener("scroll", onScroll));
|
|
180
|
+
const onResize = () => {
|
|
181
|
+
this.addEvent({
|
|
182
|
+
type: "viewport",
|
|
183
|
+
timestamp: this.getRelativeTime(),
|
|
184
|
+
data: { width: window.innerWidth, height: window.innerHeight }
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
window.addEventListener("resize", onResize);
|
|
188
|
+
this.cleanupFns.push(() => window.removeEventListener("resize", onResize));
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
var ReplayPlayer = class {
|
|
192
|
+
config;
|
|
193
|
+
session = null;
|
|
194
|
+
state = "idle";
|
|
195
|
+
currentTime = 0;
|
|
196
|
+
animationFrameId = null;
|
|
197
|
+
constructor(config) {
|
|
198
|
+
this.config = {
|
|
199
|
+
speed: 1,
|
|
200
|
+
skipInactiveThreshold: 5e3,
|
|
201
|
+
showCursor: true,
|
|
202
|
+
showClickRipple: true,
|
|
203
|
+
highlightErrors: true,
|
|
204
|
+
...config
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
load(session) {
|
|
208
|
+
this.session = session;
|
|
209
|
+
this.currentTime = 0;
|
|
210
|
+
this.state = "idle";
|
|
211
|
+
}
|
|
212
|
+
play() {
|
|
213
|
+
if (!this.session) return;
|
|
214
|
+
this.state = "playing";
|
|
215
|
+
}
|
|
216
|
+
pause() {
|
|
217
|
+
this.state = "paused";
|
|
218
|
+
if (this.animationFrameId) {
|
|
219
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
stop() {
|
|
223
|
+
this.pause();
|
|
224
|
+
this.currentTime = 0;
|
|
225
|
+
this.state = "idle";
|
|
226
|
+
}
|
|
227
|
+
seek(time) {
|
|
228
|
+
if (!this.session) return;
|
|
229
|
+
this.currentTime = Math.max(0, Math.min(time, this.session.duration));
|
|
230
|
+
}
|
|
231
|
+
setSpeed(speed) {
|
|
232
|
+
this.config.speed = speed;
|
|
233
|
+
}
|
|
234
|
+
getState() {
|
|
235
|
+
return this.state;
|
|
236
|
+
}
|
|
237
|
+
getCurrentTime() {
|
|
238
|
+
return this.currentTime;
|
|
239
|
+
}
|
|
240
|
+
getDuration() {
|
|
241
|
+
return this.session?.duration || 0;
|
|
242
|
+
}
|
|
243
|
+
destroy() {
|
|
244
|
+
this.stop();
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
function createReplayRecorder(config) {
|
|
248
|
+
return new ReplayRecorder(config);
|
|
249
|
+
}
|
|
250
|
+
function createReplayPlayer(config) {
|
|
251
|
+
return new ReplayPlayer(config);
|
|
252
|
+
}
|
|
253
|
+
export {
|
|
254
|
+
ReplayPlayer,
|
|
255
|
+
ReplayRecorder,
|
|
256
|
+
createReplayPlayer,
|
|
257
|
+
createReplayRecorder
|
|
258
|
+
};
|