@neroom/nevision 0.1.6 → 0.1.8
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 +45 -62
- package/dist/index.mjs +45 -62
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -37,7 +37,7 @@ __export(index_exports, {
|
|
|
37
37
|
module.exports = __toCommonJS(index_exports);
|
|
38
38
|
var import_react = require("react");
|
|
39
39
|
var DEFAULT_API_URL = "https://api.ne-room.io";
|
|
40
|
-
var CHUNK_INTERVAL =
|
|
40
|
+
var CHUNK_INTERVAL = 2e3;
|
|
41
41
|
var MAX_EVENTS_PER_CHUNK = 100;
|
|
42
42
|
var DB_NAME = "nevision_recordings";
|
|
43
43
|
var STORE_NAME = "pending_events";
|
|
@@ -124,7 +124,6 @@ function NevisionRecorder({
|
|
|
124
124
|
const stopFnRef = (0, import_react.useRef)(null);
|
|
125
125
|
const chunkIndexRef = (0, import_react.useRef)(0);
|
|
126
126
|
const eventStoreRef = (0, import_react.useRef)(null);
|
|
127
|
-
const initializedRef = (0, import_react.useRef)(false);
|
|
128
127
|
const onStartRef = (0, import_react.useRef)(onStart);
|
|
129
128
|
const onErrorRef = (0, import_react.useRef)(onError);
|
|
130
129
|
const samplingRef = (0, import_react.useRef)(sampling);
|
|
@@ -134,16 +133,16 @@ function NevisionRecorder({
|
|
|
134
133
|
samplingRef.current = sampling;
|
|
135
134
|
privacyRef.current = privacy;
|
|
136
135
|
(0, import_react.useEffect)(() => {
|
|
137
|
-
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
initializedRef.current = true;
|
|
136
|
+
let isActive = true;
|
|
141
137
|
let intervalId;
|
|
138
|
+
let sessionStartTime = 0;
|
|
142
139
|
const init = async () => {
|
|
143
140
|
try {
|
|
144
141
|
eventStoreRef.current = new EventStore();
|
|
145
142
|
await retrySendPendingChunks(apiUrl);
|
|
143
|
+
if (!isActive) return;
|
|
146
144
|
const { record } = await import("rrweb");
|
|
145
|
+
if (!isActive) return;
|
|
147
146
|
const startResponse = await fetch(`${apiUrl}/public/recordings/start`, {
|
|
148
147
|
method: "POST",
|
|
149
148
|
headers: {
|
|
@@ -164,15 +163,16 @@ function NevisionRecorder({
|
|
|
164
163
|
if (!startResponse.ok) {
|
|
165
164
|
throw new Error(`Failed to start recording: ${startResponse.status}`);
|
|
166
165
|
}
|
|
166
|
+
if (!isActive) return;
|
|
167
167
|
const { sessionId } = await startResponse.json();
|
|
168
168
|
sessionIdRef.current = sessionId;
|
|
169
|
+
sessionStartTime = Date.now();
|
|
169
170
|
onStartRef.current?.(sessionId);
|
|
170
171
|
const currentSampling = samplingRef.current;
|
|
171
172
|
const currentPrivacy = privacyRef.current;
|
|
172
173
|
const recordConfig = {
|
|
173
174
|
emit: (event) => {
|
|
174
175
|
eventsBufferRef.current.push(event);
|
|
175
|
-
persistEventsToStorage();
|
|
176
176
|
},
|
|
177
177
|
sampling: currentSampling ? {
|
|
178
178
|
mousemove: currentSampling.mousemove,
|
|
@@ -210,44 +210,30 @@ function NevisionRecorder({
|
|
|
210
210
|
endSession(apiUrl, sessionIdRef.current);
|
|
211
211
|
}
|
|
212
212
|
};
|
|
213
|
-
|
|
213
|
+
const handleVisibilityChange = () => {
|
|
214
214
|
if (document.visibilityState === "hidden") {
|
|
215
215
|
handleFinalSync(false);
|
|
216
216
|
}
|
|
217
|
-
}
|
|
218
|
-
|
|
217
|
+
};
|
|
218
|
+
const handleBeforeUnload = () => {
|
|
219
219
|
handleFinalSync(true);
|
|
220
|
-
}
|
|
221
|
-
|
|
220
|
+
};
|
|
221
|
+
const handlePageHide = (e) => {
|
|
222
222
|
handleFinalSync(!e.persisted);
|
|
223
|
-
}
|
|
223
|
+
};
|
|
224
|
+
const handleWindowBlur = () => {
|
|
225
|
+
if (eventsBufferRef.current.length > 0 && sessionIdRef.current) {
|
|
226
|
+
sendChunk(apiUrl, sessionIdRef.current, true);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
230
|
+
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
231
|
+
window.addEventListener("pagehide", handlePageHide);
|
|
232
|
+
window.addEventListener("blur", handleWindowBlur);
|
|
224
233
|
} catch (error) {
|
|
225
234
|
onErrorRef.current?.(error instanceof Error ? error : new Error(String(error)));
|
|
226
235
|
}
|
|
227
236
|
};
|
|
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
237
|
const retrySendPendingChunks = async (url) => {
|
|
252
238
|
if (!eventStoreRef.current) return;
|
|
253
239
|
try {
|
|
@@ -276,23 +262,8 @@ function NevisionRecorder({
|
|
|
276
262
|
};
|
|
277
263
|
const sendChunk = async (url, sessionId, isBeacon = false) => {
|
|
278
264
|
if (eventsBufferRef.current.length === 0) return;
|
|
279
|
-
const events = eventsBufferRef.current.splice(0, MAX_EVENTS_PER_CHUNK);
|
|
265
|
+
const events = isBeacon ? eventsBufferRef.current.splice(0) : eventsBufferRef.current.splice(0, MAX_EVENTS_PER_CHUNK);
|
|
280
266
|
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
|
-
}
|
|
296
267
|
const payload = JSON.stringify({
|
|
297
268
|
sessionId,
|
|
298
269
|
siteId,
|
|
@@ -301,15 +272,26 @@ function NevisionRecorder({
|
|
|
301
272
|
chunkIndex
|
|
302
273
|
});
|
|
303
274
|
if (isBeacon && navigator.sendBeacon) {
|
|
304
|
-
|
|
275
|
+
navigator.sendBeacon(
|
|
305
276
|
`${url}/public/recordings/chunk`,
|
|
306
277
|
new Blob([payload], { type: "application/json" })
|
|
307
278
|
);
|
|
308
|
-
if (sent && eventStoreRef.current) {
|
|
309
|
-
eventStoreRef.current.deleteChunk(chunkId).catch(() => {
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
279
|
} else {
|
|
280
|
+
const chunkId = `${sessionId}_${chunkIndex}_${Date.now()}`;
|
|
281
|
+
if (eventStoreRef.current) {
|
|
282
|
+
try {
|
|
283
|
+
await eventStoreRef.current.saveChunk({
|
|
284
|
+
id: chunkId,
|
|
285
|
+
sessionId,
|
|
286
|
+
siteId,
|
|
287
|
+
apiKey,
|
|
288
|
+
events,
|
|
289
|
+
chunkIndex,
|
|
290
|
+
timestamp: Date.now()
|
|
291
|
+
});
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
}
|
|
313
295
|
try {
|
|
314
296
|
const response = await fetch(`${url}/public/recordings/chunk`, {
|
|
315
297
|
method: "POST",
|
|
@@ -324,22 +306,23 @@ function NevisionRecorder({
|
|
|
324
306
|
}
|
|
325
307
|
};
|
|
326
308
|
const endSession = (url, sessionId) => {
|
|
309
|
+
const durationMs = sessionStartTime > 0 ? Date.now() - sessionStartTime : 0;
|
|
327
310
|
navigator.sendBeacon?.(
|
|
328
311
|
`${url}/public/recordings/end`,
|
|
329
312
|
new Blob(
|
|
330
|
-
[JSON.stringify({ sessionId,
|
|
313
|
+
[JSON.stringify({ sessionId, durationMs })],
|
|
331
314
|
{ type: "application/json" }
|
|
332
315
|
)
|
|
333
316
|
);
|
|
334
317
|
};
|
|
335
318
|
init();
|
|
336
319
|
return () => {
|
|
320
|
+
isActive = false;
|
|
337
321
|
clearInterval(intervalId);
|
|
338
322
|
stopFnRef.current?.();
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
323
|
+
sessionIdRef.current = null;
|
|
324
|
+
chunkIndexRef.current = 0;
|
|
325
|
+
eventsBufferRef.current = [];
|
|
343
326
|
};
|
|
344
327
|
}, [siteId, apiKey, apiUrl]);
|
|
345
328
|
return null;
|
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/index.tsx
|
|
4
4
|
import { useEffect, useRef } from "react";
|
|
5
5
|
var DEFAULT_API_URL = "https://api.ne-room.io";
|
|
6
|
-
var CHUNK_INTERVAL =
|
|
6
|
+
var CHUNK_INTERVAL = 2e3;
|
|
7
7
|
var MAX_EVENTS_PER_CHUNK = 100;
|
|
8
8
|
var DB_NAME = "nevision_recordings";
|
|
9
9
|
var STORE_NAME = "pending_events";
|
|
@@ -90,7 +90,6 @@ function NevisionRecorder({
|
|
|
90
90
|
const stopFnRef = useRef(null);
|
|
91
91
|
const chunkIndexRef = useRef(0);
|
|
92
92
|
const eventStoreRef = useRef(null);
|
|
93
|
-
const initializedRef = useRef(false);
|
|
94
93
|
const onStartRef = useRef(onStart);
|
|
95
94
|
const onErrorRef = useRef(onError);
|
|
96
95
|
const samplingRef = useRef(sampling);
|
|
@@ -100,16 +99,16 @@ function NevisionRecorder({
|
|
|
100
99
|
samplingRef.current = sampling;
|
|
101
100
|
privacyRef.current = privacy;
|
|
102
101
|
useEffect(() => {
|
|
103
|
-
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
initializedRef.current = true;
|
|
102
|
+
let isActive = true;
|
|
107
103
|
let intervalId;
|
|
104
|
+
let sessionStartTime = 0;
|
|
108
105
|
const init = async () => {
|
|
109
106
|
try {
|
|
110
107
|
eventStoreRef.current = new EventStore();
|
|
111
108
|
await retrySendPendingChunks(apiUrl);
|
|
109
|
+
if (!isActive) return;
|
|
112
110
|
const { record } = await import("rrweb");
|
|
111
|
+
if (!isActive) return;
|
|
113
112
|
const startResponse = await fetch(`${apiUrl}/public/recordings/start`, {
|
|
114
113
|
method: "POST",
|
|
115
114
|
headers: {
|
|
@@ -130,15 +129,16 @@ function NevisionRecorder({
|
|
|
130
129
|
if (!startResponse.ok) {
|
|
131
130
|
throw new Error(`Failed to start recording: ${startResponse.status}`);
|
|
132
131
|
}
|
|
132
|
+
if (!isActive) return;
|
|
133
133
|
const { sessionId } = await startResponse.json();
|
|
134
134
|
sessionIdRef.current = sessionId;
|
|
135
|
+
sessionStartTime = Date.now();
|
|
135
136
|
onStartRef.current?.(sessionId);
|
|
136
137
|
const currentSampling = samplingRef.current;
|
|
137
138
|
const currentPrivacy = privacyRef.current;
|
|
138
139
|
const recordConfig = {
|
|
139
140
|
emit: (event) => {
|
|
140
141
|
eventsBufferRef.current.push(event);
|
|
141
|
-
persistEventsToStorage();
|
|
142
142
|
},
|
|
143
143
|
sampling: currentSampling ? {
|
|
144
144
|
mousemove: currentSampling.mousemove,
|
|
@@ -176,44 +176,30 @@ function NevisionRecorder({
|
|
|
176
176
|
endSession(apiUrl, sessionIdRef.current);
|
|
177
177
|
}
|
|
178
178
|
};
|
|
179
|
-
|
|
179
|
+
const handleVisibilityChange = () => {
|
|
180
180
|
if (document.visibilityState === "hidden") {
|
|
181
181
|
handleFinalSync(false);
|
|
182
182
|
}
|
|
183
|
-
}
|
|
184
|
-
|
|
183
|
+
};
|
|
184
|
+
const handleBeforeUnload = () => {
|
|
185
185
|
handleFinalSync(true);
|
|
186
|
-
}
|
|
187
|
-
|
|
186
|
+
};
|
|
187
|
+
const handlePageHide = (e) => {
|
|
188
188
|
handleFinalSync(!e.persisted);
|
|
189
|
-
}
|
|
189
|
+
};
|
|
190
|
+
const handleWindowBlur = () => {
|
|
191
|
+
if (eventsBufferRef.current.length > 0 && sessionIdRef.current) {
|
|
192
|
+
sendChunk(apiUrl, sessionIdRef.current, true);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
196
|
+
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
197
|
+
window.addEventListener("pagehide", handlePageHide);
|
|
198
|
+
window.addEventListener("blur", handleWindowBlur);
|
|
190
199
|
} catch (error) {
|
|
191
200
|
onErrorRef.current?.(error instanceof Error ? error : new Error(String(error)));
|
|
192
201
|
}
|
|
193
202
|
};
|
|
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
203
|
const retrySendPendingChunks = async (url) => {
|
|
218
204
|
if (!eventStoreRef.current) return;
|
|
219
205
|
try {
|
|
@@ -242,23 +228,8 @@ function NevisionRecorder({
|
|
|
242
228
|
};
|
|
243
229
|
const sendChunk = async (url, sessionId, isBeacon = false) => {
|
|
244
230
|
if (eventsBufferRef.current.length === 0) return;
|
|
245
|
-
const events = eventsBufferRef.current.splice(0, MAX_EVENTS_PER_CHUNK);
|
|
231
|
+
const events = isBeacon ? eventsBufferRef.current.splice(0) : eventsBufferRef.current.splice(0, MAX_EVENTS_PER_CHUNK);
|
|
246
232
|
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
|
-
}
|
|
262
233
|
const payload = JSON.stringify({
|
|
263
234
|
sessionId,
|
|
264
235
|
siteId,
|
|
@@ -267,15 +238,26 @@ function NevisionRecorder({
|
|
|
267
238
|
chunkIndex
|
|
268
239
|
});
|
|
269
240
|
if (isBeacon && navigator.sendBeacon) {
|
|
270
|
-
|
|
241
|
+
navigator.sendBeacon(
|
|
271
242
|
`${url}/public/recordings/chunk`,
|
|
272
243
|
new Blob([payload], { type: "application/json" })
|
|
273
244
|
);
|
|
274
|
-
if (sent && eventStoreRef.current) {
|
|
275
|
-
eventStoreRef.current.deleteChunk(chunkId).catch(() => {
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
245
|
} else {
|
|
246
|
+
const chunkId = `${sessionId}_${chunkIndex}_${Date.now()}`;
|
|
247
|
+
if (eventStoreRef.current) {
|
|
248
|
+
try {
|
|
249
|
+
await eventStoreRef.current.saveChunk({
|
|
250
|
+
id: chunkId,
|
|
251
|
+
sessionId,
|
|
252
|
+
siteId,
|
|
253
|
+
apiKey,
|
|
254
|
+
events,
|
|
255
|
+
chunkIndex,
|
|
256
|
+
timestamp: Date.now()
|
|
257
|
+
});
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
}
|
|
279
261
|
try {
|
|
280
262
|
const response = await fetch(`${url}/public/recordings/chunk`, {
|
|
281
263
|
method: "POST",
|
|
@@ -290,22 +272,23 @@ function NevisionRecorder({
|
|
|
290
272
|
}
|
|
291
273
|
};
|
|
292
274
|
const endSession = (url, sessionId) => {
|
|
275
|
+
const durationMs = sessionStartTime > 0 ? Date.now() - sessionStartTime : 0;
|
|
293
276
|
navigator.sendBeacon?.(
|
|
294
277
|
`${url}/public/recordings/end`,
|
|
295
278
|
new Blob(
|
|
296
|
-
[JSON.stringify({ sessionId,
|
|
279
|
+
[JSON.stringify({ sessionId, durationMs })],
|
|
297
280
|
{ type: "application/json" }
|
|
298
281
|
)
|
|
299
282
|
);
|
|
300
283
|
};
|
|
301
284
|
init();
|
|
302
285
|
return () => {
|
|
286
|
+
isActive = false;
|
|
303
287
|
clearInterval(intervalId);
|
|
304
288
|
stopFnRef.current?.();
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
289
|
+
sessionIdRef.current = null;
|
|
290
|
+
chunkIndexRef.current = 0;
|
|
291
|
+
eventsBufferRef.current = [];
|
|
309
292
|
};
|
|
310
293
|
}, [siteId, apiKey, apiUrl]);
|
|
311
294
|
return null;
|