@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 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 = 1e4;
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
- if (initializedRef.current) {
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
- document.addEventListener("visibilitychange", () => {
213
+ const handleVisibilityChange = () => {
214
214
  if (document.visibilityState === "hidden") {
215
215
  handleFinalSync(false);
216
216
  }
217
- });
218
- window.addEventListener("beforeunload", () => {
217
+ };
218
+ const handleBeforeUnload = () => {
219
219
  handleFinalSync(true);
220
- });
221
- window.addEventListener("pagehide", (e) => {
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
- const sent = navigator.sendBeacon(
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, siteId, apiKey })],
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
- if (sessionIdRef.current) {
340
- sendChunk(apiUrl, sessionIdRef.current, true);
341
- endSession(apiUrl, sessionIdRef.current);
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 = 1e4;
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
- if (initializedRef.current) {
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
- document.addEventListener("visibilitychange", () => {
179
+ const handleVisibilityChange = () => {
180
180
  if (document.visibilityState === "hidden") {
181
181
  handleFinalSync(false);
182
182
  }
183
- });
184
- window.addEventListener("beforeunload", () => {
183
+ };
184
+ const handleBeforeUnload = () => {
185
185
  handleFinalSync(true);
186
- });
187
- window.addEventListener("pagehide", (e) => {
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
- const sent = navigator.sendBeacon(
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, siteId, apiKey })],
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
- if (sessionIdRef.current) {
306
- sendChunk(apiUrl, sessionIdRef.current, true);
307
- endSession(apiUrl, sessionIdRef.current);
308
- }
289
+ sessionIdRef.current = null;
290
+ chunkIndexRef.current = 0;
291
+ eventsBufferRef.current = [];
309
292
  };
310
293
  }, [siteId, apiKey, apiUrl]);
311
294
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neroom/nevision",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "React SDK for NEROOM/NEVISION session recording",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",