@neroom/nevision 0.1.4 → 0.1.7
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.js +200 -23
- package/dist/index.mjs +200 -23
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -39,6 +39,77 @@ var import_react = require("react");
|
|
|
39
39
|
var DEFAULT_API_URL = "https://api.ne-room.io";
|
|
40
40
|
var CHUNK_INTERVAL = 1e4;
|
|
41
41
|
var MAX_EVENTS_PER_CHUNK = 100;
|
|
42
|
+
var DB_NAME = "nevision_recordings";
|
|
43
|
+
var STORE_NAME = "pending_events";
|
|
44
|
+
var EventStore = class {
|
|
45
|
+
constructor() {
|
|
46
|
+
this.db = null;
|
|
47
|
+
this.dbReady = this.openDB();
|
|
48
|
+
}
|
|
49
|
+
openDB() {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
if (typeof indexedDB === "undefined") {
|
|
52
|
+
reject(new Error("IndexedDB not supported"));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const request = indexedDB.open(DB_NAME, 1);
|
|
56
|
+
request.onerror = () => reject(request.error);
|
|
57
|
+
request.onsuccess = () => {
|
|
58
|
+
this.db = request.result;
|
|
59
|
+
resolve(request.result);
|
|
60
|
+
};
|
|
61
|
+
request.onupgradeneeded = (event) => {
|
|
62
|
+
const db = event.target.result;
|
|
63
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
64
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: "id" });
|
|
65
|
+
store.createIndex("sessionId", "sessionId", { unique: false });
|
|
66
|
+
store.createIndex("timestamp", "timestamp", { unique: false });
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async saveChunk(chunk) {
|
|
72
|
+
const db = await this.dbReady;
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
75
|
+
const store = tx.objectStore(STORE_NAME);
|
|
76
|
+
const request = store.put(chunk);
|
|
77
|
+
request.onerror = () => reject(request.error);
|
|
78
|
+
request.onsuccess = () => resolve();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async deleteChunk(id) {
|
|
82
|
+
const db = await this.dbReady;
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
85
|
+
const store = tx.objectStore(STORE_NAME);
|
|
86
|
+
const request = store.delete(id);
|
|
87
|
+
request.onerror = () => reject(request.error);
|
|
88
|
+
request.onsuccess = () => resolve();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async getPendingChunks() {
|
|
92
|
+
const db = await this.dbReady;
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
95
|
+
const store = tx.objectStore(STORE_NAME);
|
|
96
|
+
const request = store.getAll();
|
|
97
|
+
request.onerror = () => reject(request.error);
|
|
98
|
+
request.onsuccess = () => resolve(request.result || []);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async getChunksBySession(sessionId) {
|
|
102
|
+
const db = await this.dbReady;
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
105
|
+
const store = tx.objectStore(STORE_NAME);
|
|
106
|
+
const index = store.index("sessionId");
|
|
107
|
+
const request = index.getAll(sessionId);
|
|
108
|
+
request.onerror = () => reject(request.error);
|
|
109
|
+
request.onsuccess = () => resolve(request.result || []);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
42
113
|
function NevisionRecorder({
|
|
43
114
|
siteId,
|
|
44
115
|
apiKey,
|
|
@@ -52,11 +123,26 @@ function NevisionRecorder({
|
|
|
52
123
|
const eventsBufferRef = (0, import_react.useRef)([]);
|
|
53
124
|
const stopFnRef = (0, import_react.useRef)(null);
|
|
54
125
|
const chunkIndexRef = (0, import_react.useRef)(0);
|
|
126
|
+
const eventStoreRef = (0, import_react.useRef)(null);
|
|
127
|
+
const onStartRef = (0, import_react.useRef)(onStart);
|
|
128
|
+
const onErrorRef = (0, import_react.useRef)(onError);
|
|
129
|
+
const samplingRef = (0, import_react.useRef)(sampling);
|
|
130
|
+
const privacyRef = (0, import_react.useRef)(privacy);
|
|
131
|
+
onStartRef.current = onStart;
|
|
132
|
+
onErrorRef.current = onError;
|
|
133
|
+
samplingRef.current = sampling;
|
|
134
|
+
privacyRef.current = privacy;
|
|
55
135
|
(0, import_react.useEffect)(() => {
|
|
136
|
+
let isActive = true;
|
|
56
137
|
let intervalId;
|
|
138
|
+
let sessionStartTime = 0;
|
|
57
139
|
const init = async () => {
|
|
58
140
|
try {
|
|
141
|
+
eventStoreRef.current = new EventStore();
|
|
142
|
+
await retrySendPendingChunks(apiUrl);
|
|
143
|
+
if (!isActive) return;
|
|
59
144
|
const { record } = await import("rrweb");
|
|
145
|
+
if (!isActive) return;
|
|
60
146
|
const startResponse = await fetch(`${apiUrl}/public/recordings/start`, {
|
|
61
147
|
method: "POST",
|
|
62
148
|
headers: {
|
|
@@ -65,41 +151,47 @@ function NevisionRecorder({
|
|
|
65
151
|
body: JSON.stringify({
|
|
66
152
|
siteId,
|
|
67
153
|
apiKey,
|
|
68
|
-
|
|
154
|
+
initialUrl: window.location.href,
|
|
69
155
|
userAgent: navigator.userAgent,
|
|
70
156
|
screenWidth: window.screen.width,
|
|
71
157
|
screenHeight: window.screen.height,
|
|
72
158
|
viewportWidth: window.innerWidth,
|
|
73
|
-
viewportHeight: window.innerHeight
|
|
159
|
+
viewportHeight: window.innerHeight,
|
|
160
|
+
referrer: document.referrer || ""
|
|
74
161
|
})
|
|
75
162
|
});
|
|
76
163
|
if (!startResponse.ok) {
|
|
77
164
|
throw new Error(`Failed to start recording: ${startResponse.status}`);
|
|
78
165
|
}
|
|
166
|
+
if (!isActive) return;
|
|
79
167
|
const { sessionId } = await startResponse.json();
|
|
80
168
|
sessionIdRef.current = sessionId;
|
|
81
|
-
|
|
169
|
+
sessionStartTime = Date.now();
|
|
170
|
+
onStartRef.current?.(sessionId);
|
|
171
|
+
const currentSampling = samplingRef.current;
|
|
172
|
+
const currentPrivacy = privacyRef.current;
|
|
82
173
|
const recordConfig = {
|
|
83
174
|
emit: (event) => {
|
|
84
175
|
eventsBufferRef.current.push(event);
|
|
176
|
+
persistEventsToStorage();
|
|
85
177
|
},
|
|
86
|
-
sampling:
|
|
87
|
-
mousemove:
|
|
88
|
-
mouseInteraction:
|
|
89
|
-
scroll:
|
|
90
|
-
media:
|
|
91
|
-
input:
|
|
178
|
+
sampling: currentSampling ? {
|
|
179
|
+
mousemove: currentSampling.mousemove,
|
|
180
|
+
mouseInteraction: currentSampling.mouseInteraction,
|
|
181
|
+
scroll: currentSampling.scroll,
|
|
182
|
+
media: currentSampling.media,
|
|
183
|
+
input: currentSampling.input
|
|
92
184
|
} : {
|
|
93
185
|
mousemove: 50,
|
|
94
186
|
scroll: 150
|
|
95
187
|
},
|
|
96
|
-
maskAllInputs:
|
|
97
|
-
maskInputOptions:
|
|
188
|
+
maskAllInputs: currentPrivacy?.maskAllInputs ?? true,
|
|
189
|
+
maskInputOptions: currentPrivacy?.maskInputOptions ?? {
|
|
98
190
|
password: true,
|
|
99
191
|
email: true
|
|
100
192
|
},
|
|
101
|
-
maskTextSelector:
|
|
102
|
-
blockSelector:
|
|
193
|
+
maskTextSelector: currentPrivacy?.maskTextSelector,
|
|
194
|
+
blockSelector: currentPrivacy?.blockSelector
|
|
103
195
|
};
|
|
104
196
|
const stopFn = record(recordConfig);
|
|
105
197
|
if (stopFn) {
|
|
@@ -108,26 +200,100 @@ function NevisionRecorder({
|
|
|
108
200
|
intervalId = setInterval(() => {
|
|
109
201
|
sendChunk(apiUrl, sessionId);
|
|
110
202
|
}, CHUNK_INTERVAL);
|
|
111
|
-
|
|
112
|
-
|
|
203
|
+
let finalSyncDone = false;
|
|
204
|
+
const handleFinalSync = (isPageHiding = false) => {
|
|
205
|
+
if (finalSyncDone || !sessionIdRef.current) return;
|
|
206
|
+
if (eventsBufferRef.current.length > 0) {
|
|
113
207
|
sendChunk(apiUrl, sessionIdRef.current, true);
|
|
208
|
+
}
|
|
209
|
+
if (isPageHiding) {
|
|
210
|
+
finalSyncDone = true;
|
|
114
211
|
endSession(apiUrl, sessionIdRef.current);
|
|
115
212
|
}
|
|
116
213
|
};
|
|
117
|
-
window.addEventListener("beforeunload", handleUnload);
|
|
118
214
|
document.addEventListener("visibilitychange", () => {
|
|
119
215
|
if (document.visibilityState === "hidden") {
|
|
120
|
-
|
|
216
|
+
handleFinalSync(false);
|
|
121
217
|
}
|
|
122
218
|
});
|
|
219
|
+
window.addEventListener("beforeunload", () => {
|
|
220
|
+
handleFinalSync(true);
|
|
221
|
+
});
|
|
222
|
+
window.addEventListener("pagehide", (e) => {
|
|
223
|
+
handleFinalSync(!e.persisted);
|
|
224
|
+
});
|
|
123
225
|
} catch (error) {
|
|
124
|
-
|
|
226
|
+
onErrorRef.current?.(error instanceof Error ? error : new Error(String(error)));
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
let persistTimeout = null;
|
|
230
|
+
const persistEventsToStorage = () => {
|
|
231
|
+
if (persistTimeout) return;
|
|
232
|
+
persistTimeout = setTimeout(async () => {
|
|
233
|
+
persistTimeout = null;
|
|
234
|
+
if (!sessionIdRef.current || !eventStoreRef.current) return;
|
|
235
|
+
if (eventsBufferRef.current.length === 0) return;
|
|
236
|
+
const events = [...eventsBufferRef.current];
|
|
237
|
+
const chunkId = `${sessionIdRef.current}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
238
|
+
try {
|
|
239
|
+
await eventStoreRef.current.saveChunk({
|
|
240
|
+
id: chunkId,
|
|
241
|
+
sessionId: sessionIdRef.current,
|
|
242
|
+
siteId,
|
|
243
|
+
apiKey,
|
|
244
|
+
events,
|
|
245
|
+
chunkIndex: chunkIndexRef.current,
|
|
246
|
+
timestamp: Date.now()
|
|
247
|
+
});
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
}, 1e3);
|
|
251
|
+
};
|
|
252
|
+
const retrySendPendingChunks = async (url) => {
|
|
253
|
+
if (!eventStoreRef.current) return;
|
|
254
|
+
try {
|
|
255
|
+
const pendingChunks = await eventStoreRef.current.getPendingChunks();
|
|
256
|
+
for (const chunk of pendingChunks) {
|
|
257
|
+
try {
|
|
258
|
+
const response = await fetch(`${url}/public/recordings/chunk`, {
|
|
259
|
+
method: "POST",
|
|
260
|
+
headers: { "Content-Type": "application/json" },
|
|
261
|
+
body: JSON.stringify({
|
|
262
|
+
sessionId: chunk.sessionId,
|
|
263
|
+
siteId: chunk.siteId,
|
|
264
|
+
apiKey: chunk.apiKey,
|
|
265
|
+
events: chunk.events,
|
|
266
|
+
chunkIndex: chunk.chunkIndex
|
|
267
|
+
})
|
|
268
|
+
});
|
|
269
|
+
if (response.ok) {
|
|
270
|
+
await eventStoreRef.current?.deleteChunk(chunk.id);
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
125
276
|
}
|
|
126
277
|
};
|
|
127
278
|
const sendChunk = async (url, sessionId, isBeacon = false) => {
|
|
128
279
|
if (eventsBufferRef.current.length === 0) return;
|
|
129
280
|
const events = eventsBufferRef.current.splice(0, MAX_EVENTS_PER_CHUNK);
|
|
130
281
|
const chunkIndex = chunkIndexRef.current++;
|
|
282
|
+
const chunkId = `${sessionId}_${chunkIndex}_${Date.now()}`;
|
|
283
|
+
if (eventStoreRef.current) {
|
|
284
|
+
try {
|
|
285
|
+
await eventStoreRef.current.saveChunk({
|
|
286
|
+
id: chunkId,
|
|
287
|
+
sessionId,
|
|
288
|
+
siteId,
|
|
289
|
+
apiKey,
|
|
290
|
+
events,
|
|
291
|
+
chunkIndex,
|
|
292
|
+
timestamp: Date.now()
|
|
293
|
+
});
|
|
294
|
+
} catch {
|
|
295
|
+
}
|
|
296
|
+
}
|
|
131
297
|
const payload = JSON.stringify({
|
|
132
298
|
sessionId,
|
|
133
299
|
siteId,
|
|
@@ -136,41 +302,52 @@ function NevisionRecorder({
|
|
|
136
302
|
chunkIndex
|
|
137
303
|
});
|
|
138
304
|
if (isBeacon && navigator.sendBeacon) {
|
|
139
|
-
navigator.sendBeacon(
|
|
305
|
+
const sent = navigator.sendBeacon(
|
|
140
306
|
`${url}/public/recordings/chunk`,
|
|
141
307
|
new Blob([payload], { type: "application/json" })
|
|
142
308
|
);
|
|
309
|
+
if (sent && eventStoreRef.current) {
|
|
310
|
+
eventStoreRef.current.deleteChunk(chunkId).catch(() => {
|
|
311
|
+
});
|
|
312
|
+
}
|
|
143
313
|
} else {
|
|
144
314
|
try {
|
|
145
|
-
await fetch(`${url}/public/recordings/chunk`, {
|
|
315
|
+
const response = await fetch(`${url}/public/recordings/chunk`, {
|
|
146
316
|
method: "POST",
|
|
147
317
|
headers: { "Content-Type": "application/json" },
|
|
148
318
|
body: payload
|
|
149
319
|
});
|
|
320
|
+
if (response.ok && eventStoreRef.current) {
|
|
321
|
+
await eventStoreRef.current.deleteChunk(chunkId);
|
|
322
|
+
}
|
|
150
323
|
} catch {
|
|
151
|
-
eventsBufferRef.current.unshift(...events);
|
|
152
324
|
}
|
|
153
325
|
}
|
|
154
326
|
};
|
|
155
327
|
const endSession = (url, sessionId) => {
|
|
328
|
+
const durationMs = sessionStartTime > 0 ? Date.now() - sessionStartTime : 0;
|
|
156
329
|
navigator.sendBeacon?.(
|
|
157
330
|
`${url}/public/recordings/end`,
|
|
158
331
|
new Blob(
|
|
159
|
-
[JSON.stringify({ sessionId,
|
|
332
|
+
[JSON.stringify({ sessionId, durationMs })],
|
|
160
333
|
{ type: "application/json" }
|
|
161
334
|
)
|
|
162
335
|
);
|
|
163
336
|
};
|
|
164
337
|
init();
|
|
165
338
|
return () => {
|
|
339
|
+
isActive = false;
|
|
166
340
|
clearInterval(intervalId);
|
|
167
341
|
stopFnRef.current?.();
|
|
168
342
|
if (sessionIdRef.current) {
|
|
169
343
|
sendChunk(apiUrl, sessionIdRef.current, true);
|
|
170
344
|
endSession(apiUrl, sessionIdRef.current);
|
|
171
345
|
}
|
|
346
|
+
sessionIdRef.current = null;
|
|
347
|
+
chunkIndexRef.current = 0;
|
|
348
|
+
eventsBufferRef.current = [];
|
|
172
349
|
};
|
|
173
|
-
}, [siteId, apiKey, apiUrl
|
|
350
|
+
}, [siteId, apiKey, apiUrl]);
|
|
174
351
|
return null;
|
|
175
352
|
}
|
|
176
353
|
var index_default = NevisionRecorder;
|
package/dist/index.mjs
CHANGED
|
@@ -5,6 +5,77 @@ import { useEffect, useRef } from "react";
|
|
|
5
5
|
var DEFAULT_API_URL = "https://api.ne-room.io";
|
|
6
6
|
var CHUNK_INTERVAL = 1e4;
|
|
7
7
|
var MAX_EVENTS_PER_CHUNK = 100;
|
|
8
|
+
var DB_NAME = "nevision_recordings";
|
|
9
|
+
var STORE_NAME = "pending_events";
|
|
10
|
+
var EventStore = class {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.db = null;
|
|
13
|
+
this.dbReady = this.openDB();
|
|
14
|
+
}
|
|
15
|
+
openDB() {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
if (typeof indexedDB === "undefined") {
|
|
18
|
+
reject(new Error("IndexedDB not supported"));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const request = indexedDB.open(DB_NAME, 1);
|
|
22
|
+
request.onerror = () => reject(request.error);
|
|
23
|
+
request.onsuccess = () => {
|
|
24
|
+
this.db = request.result;
|
|
25
|
+
resolve(request.result);
|
|
26
|
+
};
|
|
27
|
+
request.onupgradeneeded = (event) => {
|
|
28
|
+
const db = event.target.result;
|
|
29
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
30
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: "id" });
|
|
31
|
+
store.createIndex("sessionId", "sessionId", { unique: false });
|
|
32
|
+
store.createIndex("timestamp", "timestamp", { unique: false });
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async saveChunk(chunk) {
|
|
38
|
+
const db = await this.dbReady;
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
41
|
+
const store = tx.objectStore(STORE_NAME);
|
|
42
|
+
const request = store.put(chunk);
|
|
43
|
+
request.onerror = () => reject(request.error);
|
|
44
|
+
request.onsuccess = () => resolve();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async deleteChunk(id) {
|
|
48
|
+
const db = await this.dbReady;
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
51
|
+
const store = tx.objectStore(STORE_NAME);
|
|
52
|
+
const request = store.delete(id);
|
|
53
|
+
request.onerror = () => reject(request.error);
|
|
54
|
+
request.onsuccess = () => resolve();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async getPendingChunks() {
|
|
58
|
+
const db = await this.dbReady;
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
61
|
+
const store = tx.objectStore(STORE_NAME);
|
|
62
|
+
const request = store.getAll();
|
|
63
|
+
request.onerror = () => reject(request.error);
|
|
64
|
+
request.onsuccess = () => resolve(request.result || []);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async getChunksBySession(sessionId) {
|
|
68
|
+
const db = await this.dbReady;
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
71
|
+
const store = tx.objectStore(STORE_NAME);
|
|
72
|
+
const index = store.index("sessionId");
|
|
73
|
+
const request = index.getAll(sessionId);
|
|
74
|
+
request.onerror = () => reject(request.error);
|
|
75
|
+
request.onsuccess = () => resolve(request.result || []);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
};
|
|
8
79
|
function NevisionRecorder({
|
|
9
80
|
siteId,
|
|
10
81
|
apiKey,
|
|
@@ -18,11 +89,26 @@ function NevisionRecorder({
|
|
|
18
89
|
const eventsBufferRef = useRef([]);
|
|
19
90
|
const stopFnRef = useRef(null);
|
|
20
91
|
const chunkIndexRef = useRef(0);
|
|
92
|
+
const eventStoreRef = useRef(null);
|
|
93
|
+
const onStartRef = useRef(onStart);
|
|
94
|
+
const onErrorRef = useRef(onError);
|
|
95
|
+
const samplingRef = useRef(sampling);
|
|
96
|
+
const privacyRef = useRef(privacy);
|
|
97
|
+
onStartRef.current = onStart;
|
|
98
|
+
onErrorRef.current = onError;
|
|
99
|
+
samplingRef.current = sampling;
|
|
100
|
+
privacyRef.current = privacy;
|
|
21
101
|
useEffect(() => {
|
|
102
|
+
let isActive = true;
|
|
22
103
|
let intervalId;
|
|
104
|
+
let sessionStartTime = 0;
|
|
23
105
|
const init = async () => {
|
|
24
106
|
try {
|
|
107
|
+
eventStoreRef.current = new EventStore();
|
|
108
|
+
await retrySendPendingChunks(apiUrl);
|
|
109
|
+
if (!isActive) return;
|
|
25
110
|
const { record } = await import("rrweb");
|
|
111
|
+
if (!isActive) return;
|
|
26
112
|
const startResponse = await fetch(`${apiUrl}/public/recordings/start`, {
|
|
27
113
|
method: "POST",
|
|
28
114
|
headers: {
|
|
@@ -31,41 +117,47 @@ function NevisionRecorder({
|
|
|
31
117
|
body: JSON.stringify({
|
|
32
118
|
siteId,
|
|
33
119
|
apiKey,
|
|
34
|
-
|
|
120
|
+
initialUrl: window.location.href,
|
|
35
121
|
userAgent: navigator.userAgent,
|
|
36
122
|
screenWidth: window.screen.width,
|
|
37
123
|
screenHeight: window.screen.height,
|
|
38
124
|
viewportWidth: window.innerWidth,
|
|
39
|
-
viewportHeight: window.innerHeight
|
|
125
|
+
viewportHeight: window.innerHeight,
|
|
126
|
+
referrer: document.referrer || ""
|
|
40
127
|
})
|
|
41
128
|
});
|
|
42
129
|
if (!startResponse.ok) {
|
|
43
130
|
throw new Error(`Failed to start recording: ${startResponse.status}`);
|
|
44
131
|
}
|
|
132
|
+
if (!isActive) return;
|
|
45
133
|
const { sessionId } = await startResponse.json();
|
|
46
134
|
sessionIdRef.current = sessionId;
|
|
47
|
-
|
|
135
|
+
sessionStartTime = Date.now();
|
|
136
|
+
onStartRef.current?.(sessionId);
|
|
137
|
+
const currentSampling = samplingRef.current;
|
|
138
|
+
const currentPrivacy = privacyRef.current;
|
|
48
139
|
const recordConfig = {
|
|
49
140
|
emit: (event) => {
|
|
50
141
|
eventsBufferRef.current.push(event);
|
|
142
|
+
persistEventsToStorage();
|
|
51
143
|
},
|
|
52
|
-
sampling:
|
|
53
|
-
mousemove:
|
|
54
|
-
mouseInteraction:
|
|
55
|
-
scroll:
|
|
56
|
-
media:
|
|
57
|
-
input:
|
|
144
|
+
sampling: currentSampling ? {
|
|
145
|
+
mousemove: currentSampling.mousemove,
|
|
146
|
+
mouseInteraction: currentSampling.mouseInteraction,
|
|
147
|
+
scroll: currentSampling.scroll,
|
|
148
|
+
media: currentSampling.media,
|
|
149
|
+
input: currentSampling.input
|
|
58
150
|
} : {
|
|
59
151
|
mousemove: 50,
|
|
60
152
|
scroll: 150
|
|
61
153
|
},
|
|
62
|
-
maskAllInputs:
|
|
63
|
-
maskInputOptions:
|
|
154
|
+
maskAllInputs: currentPrivacy?.maskAllInputs ?? true,
|
|
155
|
+
maskInputOptions: currentPrivacy?.maskInputOptions ?? {
|
|
64
156
|
password: true,
|
|
65
157
|
email: true
|
|
66
158
|
},
|
|
67
|
-
maskTextSelector:
|
|
68
|
-
blockSelector:
|
|
159
|
+
maskTextSelector: currentPrivacy?.maskTextSelector,
|
|
160
|
+
blockSelector: currentPrivacy?.blockSelector
|
|
69
161
|
};
|
|
70
162
|
const stopFn = record(recordConfig);
|
|
71
163
|
if (stopFn) {
|
|
@@ -74,26 +166,100 @@ function NevisionRecorder({
|
|
|
74
166
|
intervalId = setInterval(() => {
|
|
75
167
|
sendChunk(apiUrl, sessionId);
|
|
76
168
|
}, CHUNK_INTERVAL);
|
|
77
|
-
|
|
78
|
-
|
|
169
|
+
let finalSyncDone = false;
|
|
170
|
+
const handleFinalSync = (isPageHiding = false) => {
|
|
171
|
+
if (finalSyncDone || !sessionIdRef.current) return;
|
|
172
|
+
if (eventsBufferRef.current.length > 0) {
|
|
79
173
|
sendChunk(apiUrl, sessionIdRef.current, true);
|
|
174
|
+
}
|
|
175
|
+
if (isPageHiding) {
|
|
176
|
+
finalSyncDone = true;
|
|
80
177
|
endSession(apiUrl, sessionIdRef.current);
|
|
81
178
|
}
|
|
82
179
|
};
|
|
83
|
-
window.addEventListener("beforeunload", handleUnload);
|
|
84
180
|
document.addEventListener("visibilitychange", () => {
|
|
85
181
|
if (document.visibilityState === "hidden") {
|
|
86
|
-
|
|
182
|
+
handleFinalSync(false);
|
|
87
183
|
}
|
|
88
184
|
});
|
|
185
|
+
window.addEventListener("beforeunload", () => {
|
|
186
|
+
handleFinalSync(true);
|
|
187
|
+
});
|
|
188
|
+
window.addEventListener("pagehide", (e) => {
|
|
189
|
+
handleFinalSync(!e.persisted);
|
|
190
|
+
});
|
|
89
191
|
} catch (error) {
|
|
90
|
-
|
|
192
|
+
onErrorRef.current?.(error instanceof Error ? error : new Error(String(error)));
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
let persistTimeout = null;
|
|
196
|
+
const persistEventsToStorage = () => {
|
|
197
|
+
if (persistTimeout) return;
|
|
198
|
+
persistTimeout = setTimeout(async () => {
|
|
199
|
+
persistTimeout = null;
|
|
200
|
+
if (!sessionIdRef.current || !eventStoreRef.current) return;
|
|
201
|
+
if (eventsBufferRef.current.length === 0) return;
|
|
202
|
+
const events = [...eventsBufferRef.current];
|
|
203
|
+
const chunkId = `${sessionIdRef.current}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
204
|
+
try {
|
|
205
|
+
await eventStoreRef.current.saveChunk({
|
|
206
|
+
id: chunkId,
|
|
207
|
+
sessionId: sessionIdRef.current,
|
|
208
|
+
siteId,
|
|
209
|
+
apiKey,
|
|
210
|
+
events,
|
|
211
|
+
chunkIndex: chunkIndexRef.current,
|
|
212
|
+
timestamp: Date.now()
|
|
213
|
+
});
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
}, 1e3);
|
|
217
|
+
};
|
|
218
|
+
const retrySendPendingChunks = async (url) => {
|
|
219
|
+
if (!eventStoreRef.current) return;
|
|
220
|
+
try {
|
|
221
|
+
const pendingChunks = await eventStoreRef.current.getPendingChunks();
|
|
222
|
+
for (const chunk of pendingChunks) {
|
|
223
|
+
try {
|
|
224
|
+
const response = await fetch(`${url}/public/recordings/chunk`, {
|
|
225
|
+
method: "POST",
|
|
226
|
+
headers: { "Content-Type": "application/json" },
|
|
227
|
+
body: JSON.stringify({
|
|
228
|
+
sessionId: chunk.sessionId,
|
|
229
|
+
siteId: chunk.siteId,
|
|
230
|
+
apiKey: chunk.apiKey,
|
|
231
|
+
events: chunk.events,
|
|
232
|
+
chunkIndex: chunk.chunkIndex
|
|
233
|
+
})
|
|
234
|
+
});
|
|
235
|
+
if (response.ok) {
|
|
236
|
+
await eventStoreRef.current?.deleteChunk(chunk.id);
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
91
242
|
}
|
|
92
243
|
};
|
|
93
244
|
const sendChunk = async (url, sessionId, isBeacon = false) => {
|
|
94
245
|
if (eventsBufferRef.current.length === 0) return;
|
|
95
246
|
const events = eventsBufferRef.current.splice(0, MAX_EVENTS_PER_CHUNK);
|
|
96
247
|
const chunkIndex = chunkIndexRef.current++;
|
|
248
|
+
const chunkId = `${sessionId}_${chunkIndex}_${Date.now()}`;
|
|
249
|
+
if (eventStoreRef.current) {
|
|
250
|
+
try {
|
|
251
|
+
await eventStoreRef.current.saveChunk({
|
|
252
|
+
id: chunkId,
|
|
253
|
+
sessionId,
|
|
254
|
+
siteId,
|
|
255
|
+
apiKey,
|
|
256
|
+
events,
|
|
257
|
+
chunkIndex,
|
|
258
|
+
timestamp: Date.now()
|
|
259
|
+
});
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
}
|
|
97
263
|
const payload = JSON.stringify({
|
|
98
264
|
sessionId,
|
|
99
265
|
siteId,
|
|
@@ -102,41 +268,52 @@ function NevisionRecorder({
|
|
|
102
268
|
chunkIndex
|
|
103
269
|
});
|
|
104
270
|
if (isBeacon && navigator.sendBeacon) {
|
|
105
|
-
navigator.sendBeacon(
|
|
271
|
+
const sent = navigator.sendBeacon(
|
|
106
272
|
`${url}/public/recordings/chunk`,
|
|
107
273
|
new Blob([payload], { type: "application/json" })
|
|
108
274
|
);
|
|
275
|
+
if (sent && eventStoreRef.current) {
|
|
276
|
+
eventStoreRef.current.deleteChunk(chunkId).catch(() => {
|
|
277
|
+
});
|
|
278
|
+
}
|
|
109
279
|
} else {
|
|
110
280
|
try {
|
|
111
|
-
await fetch(`${url}/public/recordings/chunk`, {
|
|
281
|
+
const response = await fetch(`${url}/public/recordings/chunk`, {
|
|
112
282
|
method: "POST",
|
|
113
283
|
headers: { "Content-Type": "application/json" },
|
|
114
284
|
body: payload
|
|
115
285
|
});
|
|
286
|
+
if (response.ok && eventStoreRef.current) {
|
|
287
|
+
await eventStoreRef.current.deleteChunk(chunkId);
|
|
288
|
+
}
|
|
116
289
|
} catch {
|
|
117
|
-
eventsBufferRef.current.unshift(...events);
|
|
118
290
|
}
|
|
119
291
|
}
|
|
120
292
|
};
|
|
121
293
|
const endSession = (url, sessionId) => {
|
|
294
|
+
const durationMs = sessionStartTime > 0 ? Date.now() - sessionStartTime : 0;
|
|
122
295
|
navigator.sendBeacon?.(
|
|
123
296
|
`${url}/public/recordings/end`,
|
|
124
297
|
new Blob(
|
|
125
|
-
[JSON.stringify({ sessionId,
|
|
298
|
+
[JSON.stringify({ sessionId, durationMs })],
|
|
126
299
|
{ type: "application/json" }
|
|
127
300
|
)
|
|
128
301
|
);
|
|
129
302
|
};
|
|
130
303
|
init();
|
|
131
304
|
return () => {
|
|
305
|
+
isActive = false;
|
|
132
306
|
clearInterval(intervalId);
|
|
133
307
|
stopFnRef.current?.();
|
|
134
308
|
if (sessionIdRef.current) {
|
|
135
309
|
sendChunk(apiUrl, sessionIdRef.current, true);
|
|
136
310
|
endSession(apiUrl, sessionIdRef.current);
|
|
137
311
|
}
|
|
312
|
+
sessionIdRef.current = null;
|
|
313
|
+
chunkIndexRef.current = 0;
|
|
314
|
+
eventsBufferRef.current = [];
|
|
138
315
|
};
|
|
139
|
-
}, [siteId, apiKey, apiUrl
|
|
316
|
+
}, [siteId, apiKey, apiUrl]);
|
|
140
317
|
return null;
|
|
141
318
|
}
|
|
142
319
|
var index_default = NevisionRecorder;
|