@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.
Files changed (3) hide show
  1. package/dist/index.js +200 -23
  2. package/dist/index.mjs +200 -23
  3. 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
- url: window.location.href,
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
- onStart?.(sessionId);
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: sampling ? {
87
- mousemove: sampling.mousemove,
88
- mouseInteraction: sampling.mouseInteraction,
89
- scroll: sampling.scroll,
90
- media: sampling.media,
91
- input: sampling.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: privacy?.maskAllInputs ?? true,
97
- maskInputOptions: privacy?.maskInputOptions ?? {
188
+ maskAllInputs: currentPrivacy?.maskAllInputs ?? true,
189
+ maskInputOptions: currentPrivacy?.maskInputOptions ?? {
98
190
  password: true,
99
191
  email: true
100
192
  },
101
- maskTextSelector: privacy?.maskTextSelector,
102
- blockSelector: privacy?.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
- const handleUnload = () => {
112
- if (sessionIdRef.current && eventsBufferRef.current.length > 0) {
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
- handleUnload();
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
- onError?.(error instanceof Error ? error : new Error(String(error)));
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, siteId, apiKey })],
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, sampling, privacy, onStart, onError]);
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
- url: window.location.href,
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
- onStart?.(sessionId);
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: sampling ? {
53
- mousemove: sampling.mousemove,
54
- mouseInteraction: sampling.mouseInteraction,
55
- scroll: sampling.scroll,
56
- media: sampling.media,
57
- input: sampling.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: privacy?.maskAllInputs ?? true,
63
- maskInputOptions: privacy?.maskInputOptions ?? {
154
+ maskAllInputs: currentPrivacy?.maskAllInputs ?? true,
155
+ maskInputOptions: currentPrivacy?.maskInputOptions ?? {
64
156
  password: true,
65
157
  email: true
66
158
  },
67
- maskTextSelector: privacy?.maskTextSelector,
68
- blockSelector: privacy?.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
- const handleUnload = () => {
78
- if (sessionIdRef.current && eventsBufferRef.current.length > 0) {
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
- handleUnload();
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
- onError?.(error instanceof Error ? error : new Error(String(error)));
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, siteId, apiKey })],
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, sampling, privacy, onStart, onError]);
316
+ }, [siteId, apiKey, apiUrl]);
140
317
  return null;
141
318
  }
142
319
  var index_default = NevisionRecorder;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neroom/nevision",
3
- "version": "0.1.4",
3
+ "version": "0.1.7",
4
4
  "description": "React SDK for NEROOM/NEVISION session recording",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",