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