iidrak-analytics-react 1.3.0 → 1.5.1

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/README.md CHANGED
@@ -18,8 +18,13 @@ This package relies on several peer dependencies for device info, storage, and s
18
18
  ```bash
19
19
  npm install iidrak-analytics-react
20
20
 
21
- # Install required peer dependencies
22
- npm install react-native-mmkv react-native-nitro-modules @react-native-community/netinfo react-native-device-info react-native-view-shot react-native-safe-area-context
21
+ # Install required peer dependencies. For storage, choose ONE: MMKV or AsyncStorage
22
+ npm install react-native-nitro-modules react-native-mmkv
23
+ # OR
24
+ npm install @react-native-async-storage/async-storage
25
+
26
+ # Install core peer dependencies
27
+ npm install @react-native-community/netinfo react-native-device-info react-native-view-shot react-native-safe-area-context
23
28
  ```
24
29
 
25
30
  **iOS Users**: after installing, remember to run:
@@ -44,6 +49,13 @@ The configuration object requires strict structure for `app`, `config`, and `use
44
49
  ```javascript
45
50
  import { MetaStreamIO, MetaStreamProvider } from 'iidrak-analytics-react';
46
51
 
52
+ // Recommended: Import your preferred storage engine
53
+ import { createMMKV } from 'react-native-mmkv';
54
+ const mmkvStorage = createMMKV();
55
+
56
+ // Alternatively, import AsyncStorage
57
+ // import AsyncStorage from '@react-native-async-storage/async-storage';
58
+
47
59
  const iidrakConfig = {
48
60
  // Application Details
49
61
  app: {
@@ -70,6 +82,17 @@ const iidrakConfig = {
70
82
  sessionLength: 1800, // Session timeout in seconds (e.g. 30 mins)
71
83
  quality: 0.1, // Recording quality (0.1 - 1.0)
72
84
  fps: 2, // Frames Per Second for recording
85
+
86
+ // Provide the storage engine to the SDK.
87
+ // Why wrap MMKV? The SDK strictly expects an asynchronous `getItem/setItem` Interface.
88
+ // MMKV v4 is completely synchronous C++. This wrapper bridges MMKV into a standard Promise architecture.
89
+ storageEngine: {
90
+ getItem: async (key) => mmkvStorage.getString(key) || null,
91
+ setItem: async (key, value) => { mmkvStorage.set(key, value); return Promise.resolve(); },
92
+ removeItem: async (key) => { mmkvStorage.remove(key); return Promise.resolve(); }
93
+ },
94
+ // For AsyncStorage, no wrapper is needed because it natively uses Promises:
95
+ // storageEngine: AsyncStorage,
73
96
  },
74
97
 
75
98
  // Initial User Info
@@ -56,82 +56,87 @@ class MetaStreamIOEnvironment {
56
56
  }
57
57
 
58
58
  async logAppInfo() {
59
- let app_id = null;
60
- let first_installation_time = null;
61
- let installation_id = null;
62
- let install_referrer = null;
63
- let last_update_time = null;
64
- let version = null;
65
-
66
- app_id = DeviceInfo.getBundleId() ? DeviceInfo.getBundleId() : null;
67
-
68
- first_installation_time = await DeviceInfo.getFirstInstallTime()
69
- .then((result) => {
70
- if (this.date.validateDatetime(result)) {
71
- return result;
72
- } else {
59
+ if (this._appInfoCached) return this._appInfoCached;
60
+ if (this._appInfoPromise) return this._appInfoPromise;
61
+
62
+ this._appInfoPromise = (async () => {
63
+ let app_id = null;
64
+ let first_installation_time = null;
65
+ let installation_id = null;
66
+ let install_referrer = null;
67
+ let last_update_time = null;
68
+ let version = null;
69
+
70
+ app_id = DeviceInfo.getBundleId() ? DeviceInfo.getBundleId() : null;
71
+
72
+ first_installation_time = await DeviceInfo.getFirstInstallTime()
73
+ .then((result) => {
74
+ if (this.date.validateDatetime(result)) {
75
+ return result;
76
+ } else {
77
+ return null;
78
+ }
79
+ })
80
+ .catch((err) => {
81
+ this.logger.warn(
82
+ this.constant.MetaStreamIO_Logger_Category_Environment,
83
+ err
84
+ );
73
85
  return null;
74
- }
75
- })
76
- .catch((err) => {
77
- this.logger.warn(
78
- this.constant.MetaStreamIO_Logger_Category_Environment,
79
- err
80
- );
81
- return null;
82
- });
86
+ });
83
87
 
84
- installation_id = "deprecated"; // Constants.installationId;
85
-
86
- install_referrer = await DeviceInfo.getInstallReferrer()
87
- .then((result) => {
88
- return result;
89
- })
90
- .catch((err) => {
91
- this.logger.warn(
92
- this.constant.MetaStreamIO_Logger_Category_Environment,
93
- err
94
- );
95
- return null;
96
- });
88
+ installation_id = "deprecated"; // Constants.installationId;
97
89
 
98
- last_update_time = await DeviceInfo.getLastUpdateTime()
99
- .then((result) => {
100
- if (this.date.validateDatetime(result)) {
90
+ install_referrer = await DeviceInfo.getInstallReferrer()
91
+ .then((result) => {
101
92
  return result;
102
- } else {
93
+ })
94
+ .catch((err) => {
95
+ this.logger.warn(
96
+ this.constant.MetaStreamIO_Logger_Category_Environment,
97
+ err
98
+ );
103
99
  return null;
104
- }
105
- })
106
- .catch((err) => {
107
- this.logger.warn(
108
- this.constant.MetaStreamIO_Logger_Category_Environment,
109
- err
110
- );
111
- return null;
112
- });
113
-
114
- version = DeviceInfo.getVersion() ? DeviceInfo.getVersion() : null;
115
-
116
- this.app_info = new AppInfoModel({
117
- first_installation_time: first_installation_time,
118
- id: app_id,
119
- installation_id: installation_id,
120
- install_referrer: install_referrer,
121
- last_update_time: last_update_time,
122
- version: version,
123
- });
100
+ });
101
+
102
+ last_update_time = await DeviceInfo.getLastUpdateTime()
103
+ .then((result) => {
104
+ if (this.date.validateDatetime(result)) {
105
+ return result;
106
+ } else {
107
+ return null;
108
+ }
109
+ })
110
+ .catch((err) => {
111
+ this.logger.warn(
112
+ this.constant.MetaStreamIO_Logger_Category_Environment,
113
+ err
114
+ );
115
+ return null;
116
+ });
124
117
 
125
- this.logger.log(
126
- this.constant.MetaStreamIO_Logger_Category_Environment,
127
- this.constant.MetaStreamIO_Environment_AppInfo.format(
128
- JSON.stringify(this.app_info)
129
- )
130
- );
118
+ version = DeviceInfo.getVersion() ? DeviceInfo.getVersion() : null;
131
119
 
132
- return this.app_info.json();
120
+ this.app_info = new AppInfoModel({
121
+ first_installation_time: first_installation_time,
122
+ id: app_id,
123
+ installation_id: installation_id,
124
+ install_referrer: install_referrer,
125
+ last_update_time: last_update_time,
126
+ version: version,
127
+ });
133
128
 
134
- // return Promise.resolve(this.app_info.json());
129
+ this.logger.log(
130
+ this.constant.MetaStreamIO_Logger_Category_Environment,
131
+ this.constant.MetaStreamIO_Environment_AppInfo.format(
132
+ JSON.stringify(this.app_info)
133
+ )
134
+ );
135
+
136
+ this._appInfoCached = this.app_info.json();
137
+ return this._appInfoCached;
138
+ })();
139
+ return this._appInfoPromise;
135
140
  }
136
141
 
137
142
  async getAppPerformance() {
@@ -185,146 +190,153 @@ class MetaStreamIOEnvironment {
185
190
  }
186
191
 
187
192
  async logDevice() {
188
- let brand,
189
- device_type,
190
- model,
191
- operating_system,
192
- operating_system_version,
193
- pin_or_fingerprint_set,
194
- screen_width,
195
- screen_height,
196
- storage_total,
197
- storage_free,
198
- supported_architectures,
199
- timezone_offset_sec,
200
- total_memory,
201
- unique_device_id,
202
- used_memory;
203
-
204
- brand = await DeviceInfo.getBrand();
205
-
206
- device_type = await DeviceInfo.getDeviceType();
207
-
208
- model = DeviceInfo.getModel();
209
-
210
- operating_system = DeviceInfo.getSystemName();
211
-
212
- operating_system_version = DeviceInfo.getSystemVersion();
213
-
214
- try {
215
- screen_height = Math.round(Dimensions.get("window").height);
216
- } catch {
217
- screen_height = null;
218
- }
219
-
220
- try {
221
- screen_width = Math.round(Dimensions.get("window").width);
222
- } catch {
223
- screen_width = null;
224
- }
225
-
226
- storage_free = await DeviceInfo.getFreeDiskStorage()
227
- .then((result) => {
228
- return result;
229
- })
230
- .catch((err) => {
231
- this.logger.warn(
232
- this.constant.MetaStreamIO_Logger_Category_Environment,
233
- err
234
- );
235
- });
236
-
237
- storage_total = await DeviceInfo.getTotalDiskCapacity()
238
- .then((result) => {
239
- return result;
240
- })
241
- .catch((err) => {
242
- this.logger.warn(
243
- this.constant.MetaStreamIO_Logger_Category_Environment,
244
- err
245
- );
246
- });
247
-
248
- supported_architectures = await DeviceInfo.supportedAbis()
249
- .then((result) => {
250
- return result.join(",");
251
- })
252
- .catch((err) => {
253
- this.logger.warn(
254
- this.constant.MetaStreamIO_Logger_Category_Environment,
255
- err
256
- );
257
- });
258
-
259
- total_memory = await DeviceInfo.getTotalMemory()
260
- .then((result) => {
261
- return result;
262
- })
263
- .catch((err) => {
264
- this.logger.warn(
265
- this.constant.MetaStreamIO_Logger_Category_Environment,
266
- err
267
- );
268
- });
269
-
270
- unique_device_id = DeviceInfo.getUniqueId()
271
- ? DeviceInfo.getUniqueId()
272
- : null;
273
-
274
- used_memory = await DeviceInfo.getUsedMemory()
275
- .then((result) => {
276
- return result;
277
- })
278
- .catch((err) => {
279
- this.logger.warn(
280
- this.constant.MetaStreamIO_Logger_Category_Environment,
281
- err
282
- );
283
- });
284
-
285
- try {
286
- timezone_offset_sec = this.date.getDeviceTimezoneOffset();
287
- } catch {
288
- timezone_offset_sec = null;
289
- }
290
-
291
- pin_or_fingerprint_set = await DeviceInfo.isPinOrFingerprintSet()
292
- .then((result) => {
293
- return result;
294
- })
295
- .catch((err) => {
296
- this.logger.warn(
297
- this.constant.MetaStreamIO_Logger_Category_Environment,
298
- err
299
- );
193
+ if (this._deviceCached) return this._deviceCached;
194
+ if (this._devicePromise) return this._devicePromise;
195
+
196
+ this._devicePromise = (async () => {
197
+ let brand,
198
+ device_type,
199
+ model,
200
+ operating_system,
201
+ operating_system_version,
202
+ pin_or_fingerprint_set,
203
+ screen_width,
204
+ screen_height,
205
+ storage_total,
206
+ storage_free,
207
+ supported_architectures,
208
+ timezone_offset_sec,
209
+ total_memory,
210
+ unique_device_id,
211
+ used_memory;
212
+
213
+ brand = await DeviceInfo.getBrand();
214
+
215
+ device_type = await DeviceInfo.getDeviceType();
216
+
217
+ model = DeviceInfo.getModel();
218
+
219
+ operating_system = DeviceInfo.getSystemName();
220
+
221
+ operating_system_version = DeviceInfo.getSystemVersion();
222
+
223
+ try {
224
+ screen_height = Math.round(Dimensions.get("window").height);
225
+ } catch {
226
+ screen_height = null;
227
+ }
228
+
229
+ try {
230
+ screen_width = Math.round(Dimensions.get("window").width);
231
+ } catch {
232
+ screen_width = null;
233
+ }
234
+
235
+ storage_free = await DeviceInfo.getFreeDiskStorage()
236
+ .then((result) => {
237
+ return result;
238
+ })
239
+ .catch((err) => {
240
+ this.logger.warn(
241
+ this.constant.MetaStreamIO_Logger_Category_Environment,
242
+ err
243
+ );
244
+ });
245
+
246
+ storage_total = await DeviceInfo.getTotalDiskCapacity()
247
+ .then((result) => {
248
+ return result;
249
+ })
250
+ .catch((err) => {
251
+ this.logger.warn(
252
+ this.constant.MetaStreamIO_Logger_Category_Environment,
253
+ err
254
+ );
255
+ });
256
+
257
+ supported_architectures = await DeviceInfo.supportedAbis()
258
+ .then((result) => {
259
+ return result.join(",");
260
+ })
261
+ .catch((err) => {
262
+ this.logger.warn(
263
+ this.constant.MetaStreamIO_Logger_Category_Environment,
264
+ err
265
+ );
266
+ });
267
+
268
+ total_memory = await DeviceInfo.getTotalMemory()
269
+ .then((result) => {
270
+ return result;
271
+ })
272
+ .catch((err) => {
273
+ this.logger.warn(
274
+ this.constant.MetaStreamIO_Logger_Category_Environment,
275
+ err
276
+ );
277
+ });
278
+
279
+ unique_device_id = DeviceInfo.getUniqueId()
280
+ ? DeviceInfo.getUniqueId()
281
+ : null;
282
+
283
+ used_memory = await DeviceInfo.getUsedMemory()
284
+ .then((result) => {
285
+ return result;
286
+ })
287
+ .catch((err) => {
288
+ this.logger.warn(
289
+ this.constant.MetaStreamIO_Logger_Category_Environment,
290
+ err
291
+ );
292
+ });
293
+
294
+ try {
295
+ timezone_offset_sec = this.date.getDeviceTimezoneOffset();
296
+ } catch {
297
+ timezone_offset_sec = null;
298
+ }
299
+
300
+ pin_or_fingerprint_set = await DeviceInfo.isPinOrFingerprintSet()
301
+ .then((result) => {
302
+ return result;
303
+ })
304
+ .catch((err) => {
305
+ this.logger.warn(
306
+ this.constant.MetaStreamIO_Logger_Category_Environment,
307
+ err
308
+ );
309
+ });
310
+
311
+ this.device = new DeviceModel({
312
+ brand: brand,
313
+ device_type: device_type,
314
+ model: model,
315
+ operating_system: operating_system,
316
+ operating_system_version: operating_system_version,
317
+ pin_or_fingerprint_set: pin_or_fingerprint_set,
318
+ screen_height: screen_height,
319
+ screen_width: screen_width,
320
+ storage_free: storage_free,
321
+ storage_total: storage_total,
322
+ supported_architectures: supported_architectures,
323
+ timezone_offset_sec: timezone_offset_sec,
324
+ total_memory: total_memory,
325
+ unique_device_id: unique_device_id,
326
+ used_memory: used_memory,
300
327
  });
301
328
 
302
- this.device = new DeviceModel({
303
- brand: brand,
304
- device_type: device_type,
305
- model: model,
306
- operating_system: operating_system,
307
- operating_system_version: operating_system_version,
308
- pin_or_fingerprint_set: pin_or_fingerprint_set,
309
- screen_height: screen_height,
310
- screen_width: screen_width,
311
- storage_free: storage_free,
312
- storage_total: storage_total,
313
- supported_architectures: supported_architectures,
314
- timezone_offset_sec: timezone_offset_sec,
315
- total_memory: total_memory,
316
- unique_device_id: unique_device_id,
317
- used_memory: used_memory,
318
- });
319
-
320
- this.logger.log(
321
- this.constant.MetaStreamIO_Logger_Category_Environment,
322
- this.constant.MetaStreamIO_Environment_Device.format(
323
- JSON.stringify(this.device)
324
- )
325
- );
326
-
327
- return Promise.resolve(this.device.json());
329
+ this.logger.log(
330
+ this.constant.MetaStreamIO_Logger_Category_Environment,
331
+ this.constant.MetaStreamIO_Environment_Device.format(
332
+ JSON.stringify(this.device)
333
+ )
334
+ );
335
+
336
+ this._deviceCached = this.device.json();
337
+ return this._deviceCached;
338
+ })();
339
+ return this._devicePromise;
328
340
  }
329
341
 
330
342
  async getNetworkInfo() {
@@ -338,68 +350,66 @@ class MetaStreamIOEnvironment {
338
350
  }
339
351
 
340
352
  async logNetworkInfo() {
341
- let carrier,
342
- carrier_iso_country_code,
343
- cellular_generation,
344
- mobile_country_code,
345
- mobile_network_code,
346
- network_state_type,
347
- net_info;
348
-
349
- carrier = await DeviceInfo.getCarrier()
350
- .then((result) => {
351
- return result;
352
- })
353
- .catch((err) => {
354
- this.logger.warn(
355
- this.constant.MetaStreamIO_Logger_Category_Environment,
356
- err
357
- );
358
- });
359
-
360
- net_info = await NetInfo.fetch()
361
- .then((state) => {
362
- return {
353
+ if (this._networkCached && (Date.now() - this._networkCachedTime < 5000)) {
354
+ return this._networkCached;
355
+ }
356
+ if (this._networkPromise) return this._networkPromise;
357
+
358
+ this._networkPromise = (async () => {
359
+ let carrier,
360
+ carrier_iso_country_code,
361
+ cellular_generation,
362
+ mobile_country_code,
363
+ mobile_network_code,
364
+ network_state_type,
365
+ net_info;
366
+
367
+ carrier = await Promise.race([
368
+ DeviceInfo.getCarrier().then(r => r).catch(() => null),
369
+ new Promise(res => setTimeout(() => res(null), 500))
370
+ ]);
371
+
372
+ net_info = await Promise.race([
373
+ NetInfo.fetch().then((state) => ({
363
374
  connection_type: state.type,
364
375
  connection_active: state.isConnected,
365
- cellular_generation:
366
- state.type === "cellular" ? state.details.cellularGeneration : null,
367
- };
368
- })
369
- .catch((err) => {
370
- this.logger.warn(
371
- this.constant.MetaStreamIO_Logger_Category_Environment,
372
- err
373
- );
374
- });
376
+ cellular_generation: state.type === "cellular" ? state.details.cellularGeneration : null,
377
+ })).catch(() => ({ connection_type: "unknown", connection_active: true, cellular_generation: null })),
378
+ new Promise(res => setTimeout(() => res({ connection_type: "unknown", connection_active: true, cellular_generation: null }), 500))
379
+ ]);
375
380
 
376
- carrier_iso_country_code = null;
381
+ carrier_iso_country_code = null;
377
382
 
378
- cellular_generation = net_info.cellular_generation;
383
+ cellular_generation = net_info.cellular_generation;
379
384
 
380
- mobile_country_code = null;
385
+ mobile_country_code = null;
381
386
 
382
- mobile_network_code = null;
387
+ mobile_network_code = null;
383
388
 
384
- network_state_type = net_info.connection_type;
389
+ network_state_type = net_info.connection_type;
385
390
 
386
- this.logger.log(
387
- this.constant.MetaStreamIO_Logger_Category_Environment,
388
- this.constant.MetaStreamIO_Environment_Network.format(
389
- JSON.stringify(this.network_info)
390
- )
391
- );
391
+ this.logger.log(
392
+ this.constant.MetaStreamIO_Logger_Category_Environment,
393
+ this.constant.MetaStreamIO_Environment_Network.format(
394
+ JSON.stringify(this.network_info)
395
+ )
396
+ );
392
397
 
393
- this.network_info = new NetworkInfoModel({
394
- carrier: carrier,
395
- carrier_iso_country_code: carrier_iso_country_code,
396
- cellular_generation: cellular_generation,
397
- mobile_country_code: mobile_country_code,
398
- mobile_network_code: mobile_network_code,
399
- network_state_type: network_state_type,
400
- });
398
+ this.network_info = new NetworkInfoModel({
399
+ carrier: carrier,
400
+ carrier_iso_country_code: carrier_iso_country_code,
401
+ cellular_generation: cellular_generation,
402
+ mobile_country_code: mobile_country_code,
403
+ mobile_network_code: mobile_network_code,
404
+ network_state_type: network_state_type,
405
+ });
401
406
 
402
- return Promise.resolve(this.network_info.json());
407
+ this._networkCached = this.network_info.json();
408
+ this._networkCachedTime = Date.now();
409
+ this._networkPromise = null;
410
+ return this._networkCached;
411
+ })();
412
+ return this._networkPromise;
403
413
  }
404
414
  }
405
415
 
@@ -82,6 +82,7 @@ class MetaStreamIO {
82
82
  silentMode: this.config.silentMode,
83
83
  endpoints: this.appConfig.endpoints,
84
84
  headers: this.appConfig.headers,
85
+ storageEngine: this.config.storageEngine,
85
86
  });
86
87
  this.account = new MetaStreamIOAccountData({
87
88
  constants: this.constant,
@@ -469,7 +470,7 @@ class MetaStreamIO {
469
470
  );
470
471
  return Promise.resolve(event_data);
471
472
  } else {
472
- this.logger.error(
473
+ this.logger.warn(
473
474
  "event",
474
475
  this.constant.MetaStreamIO_Log_EventTrackFailed.format(_data)
475
476
  );
@@ -480,13 +481,14 @@ class MetaStreamIO {
480
481
  return Promise.reject(_data);
481
482
  }
482
483
  })
483
- .catch((err) => {
484
- this.logger.error("event", "<post_event> could not post: " + err);
485
- return Promise.reject(err);
484
+ .catch(async (err) => {
485
+ this.logger.warn("event", "<post_event> could not post: " + (err.message || JSON.stringify(err)));
486
+ await this.network.storeOfflineEvent(this.network.endpoint, event_data);
487
+ return Promise.resolve(event_data); // Resolve to deque, since it's now safely offline
486
488
  });
487
489
  return status;
488
490
  } else {
489
- this.logger.error("event", "<post_event> event_data is undefined");
491
+ this.logger.warn("event", "<post_event> event_data is undefined");
490
492
  }
491
493
  }
492
494
 
@@ -515,7 +517,7 @@ class MetaStreamIO {
515
517
  return Promise.resolve();
516
518
  })
517
519
  .catch((err) => {
518
- this.logger.error(
520
+ this.logger.warn(
519
521
  "event",
520
522
  "<runQueue> this.post_event failed: " + err
521
523
  );
@@ -526,7 +528,7 @@ class MetaStreamIO {
526
528
  try {
527
529
  this.queue.enqueue(event);
528
530
  } catch (err) {
529
- this.logger.error(
531
+ this.logger.warn(
530
532
  "event",
531
533
  "<runQueue> could not enqueue event: " + err
532
534
  );
@@ -1,13 +1,6 @@
1
1
  "use strict";
2
2
 
3
3
  import { strf } from "./string.format.js";
4
- import { createMMKV } from "react-native-mmkv";
5
- const mmkvStorage = createMMKV();
6
- const AsyncStorage = {
7
- getItem: async (key) => mmkvStorage.getString(key) || null,
8
- setItem: async (key, value) => { mmkvStorage.set(key, value); return Promise.resolve(); },
9
- removeItem: async (key) => { mmkvStorage.delete(key); return Promise.resolve(); }
10
- };
11
4
  import NetInfo from "@react-native-community/netinfo";
12
5
 
13
6
  strf();
@@ -21,7 +14,7 @@ const EVENT_QUEUE_KEY = "metastream_offline_events";
21
14
  */
22
15
 
23
16
  class MetaStreamIONetwork {
24
- constructor({ constants, endpoints, logger, silentMode, headers } = {}) {
17
+ constructor({ constants, endpoints, logger, silentMode, headers, storageEngine } = {}) {
25
18
  this.silentMode = silentMode;
26
19
  this.constant = constants;
27
20
  this.endpoint = null;
@@ -29,13 +22,23 @@ class MetaStreamIONetwork {
29
22
  this.headers = headers;
30
23
  this.logger = logger;
31
24
 
25
+ // Use provided storageEngine from config or fallback to a transient in-memory store
26
+ this.storageEngine = storageEngine || {
27
+ _memory: {},
28
+ getItem: async (key) => this.storageEngine._memory[key] || null,
29
+ setItem: async (key, value) => { this.storageEngine._memory[key] = value; return Promise.resolve(); },
30
+ removeItem: async (key) => { delete this.storageEngine._memory[key]; return Promise.resolve(); }
31
+ };
32
+
32
33
  // Initialize offline handling
33
34
  this.initOfflineHandling();
34
35
  }
35
36
 
36
37
  initOfflineHandling() {
38
+ this.isOnline = true; // Default assume online, Listener updates it
37
39
  // Flush on startup if connected
38
40
  NetInfo.fetch().then(state => {
41
+ this.isOnline = state.isConnected;
39
42
  if (state.isConnected) {
40
43
  this.flushEvents();
41
44
  }
@@ -43,64 +46,50 @@ class MetaStreamIONetwork {
43
46
 
44
47
  // Flush when network comes back online
45
48
  NetInfo.addEventListener(state => {
49
+ this.isOnline = state.isConnected;
46
50
  if (state.isConnected) {
47
51
  this.flushEvents();
48
52
  }
49
53
  });
54
+
55
+ this._flushInterval = setInterval(() => {
56
+ this.flushEvents();
57
+ }, 3000); // Reverted back to 3 seconds. (Locking guarantees safety)
50
58
  }
51
59
 
52
60
  reset() {
53
61
  this.endpoint = null;
62
+ this._testingPromise = null;
54
63
  }
55
64
 
56
65
  async testConnection() {
57
- if (!this.endpoint || this.endpoint === null) {
66
+ if (this.endpoint) return Promise.resolve(this.endpoint);
67
+ if (this._testingPromise) return this._testingPromise;
68
+
69
+ this._testingPromise = (async () => {
58
70
  for (let _e in this.endpoints) {
59
- this.endpoint = await this.getData({ endpoint: this.endpoints[_e] })
60
- .then((data) => {
61
- try {
62
- if (data.status === 200) {
63
- return this.endpoints[_e];
64
- }
65
- } catch (err) {
66
- return false;
67
- }
68
- })
69
- .catch(() => { });
70
- if (this.endpoint) {
71
- this.logger.log(
72
- "network",
73
- this.constant.MetaStreamIO_Network_EndpointSet.format(this.endpoint)
74
- );
71
+ let testEp = await this.getData({ endpoint: this.endpoints[_e] });
72
+ if (testEp && testEp.status > 0) {
73
+ this.logger.log("network", this.constant.MetaStreamIO_Network_EndpointSet.format(this.endpoints[_e]));
74
+ this.endpoint = this.endpoints[_e];
75
75
  break;
76
76
  } else {
77
- this.logger.log(
78
- "network",
79
- this.constant.MetaStreamIO_Network_EndpointTestFailed.format(
80
- this.endpoint
81
- )
82
- );
83
- this.logger.log(
84
- "network",
85
- this.constant.MetaStreamIO_Network_SilentModeEnabled
86
- );
77
+ this.logger.log("network", this.constant.MetaStreamIO_Network_EndpointTestFailed.format(this.endpoints[_e]));
87
78
  }
88
79
  }
89
- }
90
-
91
- return Promise.resolve(this.endpoint);
80
+ this._testingPromise = null;
81
+ return this.endpoint;
82
+ })();
83
+ return this._testingPromise;
92
84
  }
93
85
 
94
86
  async getData({ endpoint = this.endpoint } = {}) {
95
- var rx;
96
-
97
87
  let get_config = {
98
88
  method: "GET",
99
- // mode: "no-cors", // required for local testing
100
89
  cache: "no-cache",
101
90
  credentials: "same-origin",
102
- redirect: "follow", // manual, *follow, error
103
- referrerPolicy: "no-referrer", // no-referrer, *client
91
+ redirect: "follow",
92
+ referrerPolicy: "no-referrer",
104
93
  headers: {},
105
94
  };
106
95
 
@@ -108,38 +97,17 @@ class MetaStreamIONetwork {
108
97
  get_config.headers = Object.assign(get_config.headers, this.headers);
109
98
  }
110
99
 
100
+ const controller = new AbortController();
101
+ const timeoutId = setTimeout(() => controller.abort(), 3000);
102
+ get_config.signal = controller.signal;
103
+
111
104
  try {
112
- await fetch(endpoint, get_config)
113
- .then((response) => {
114
- const contentType = response.headers.get("content-type");
115
- if (!contentType || !contentType.includes("application/json")) {
116
- return null;
117
- } else {
118
- return response;
119
- }
120
- })
121
- .catch((err) => {
122
- return Promise.resolve(err);
123
- })
124
- .then(async (response) => {
125
- rx = {
126
- body: await response.json(),
127
- status: response.status,
128
- };
129
- })
130
- .catch((err) => {
131
- return {
132
- body: err,
133
- status: 400,
134
- };
135
- });
136
- return rx;
105
+ const response = await fetch(endpoint, get_config);
106
+ clearTimeout(timeoutId);
107
+ return { status: response.status, body: "OK" };
137
108
  } catch (err) {
138
- return Promise.reject(err);
139
- throw this.constant.MetaStreamIO_Network_GetDataIssue.format(
140
- err,
141
- endpoint
142
- );
109
+ clearTimeout(timeoutId);
110
+ return { status: 0, body: String(err) };
143
111
  }
144
112
  }
145
113
 
@@ -161,12 +129,25 @@ class MetaStreamIONetwork {
161
129
  };
162
130
  }
163
131
 
132
+ if (!this.isOnline) {
133
+ this.logger.log("network", "Offline detected in postData. Storing event.");
134
+ const fallbackEndpoint = this.endpoints.length > 0 ? this.endpoints[0] : null;
135
+ if (fallbackEndpoint) {
136
+ await this.storeOfflineEvent(fallbackEndpoint + (endpoint ? endpoint : ""), data);
137
+ return {
138
+ body: "Stored offline",
139
+ status: 200
140
+ };
141
+ }
142
+ }
143
+
164
144
  if (!this.endpoint) {
165
- _endpoint = await this.testConnection()
166
- .then((rx_endpoint) => {
167
- return Promise.resolve(rx_endpoint + (endpoint ? endpoint : ""));
168
- })
169
- .catch((err) => err);
145
+ const rx_endpoint = await this.testConnection().catch(() => null);
146
+ if (rx_endpoint) {
147
+ _endpoint = rx_endpoint + (endpoint ? endpoint : "");
148
+ } else {
149
+ _endpoint = null;
150
+ }
170
151
  }
171
152
 
172
153
  if (_endpoint) {
@@ -204,6 +185,9 @@ class MetaStreamIONetwork {
204
185
  }
205
186
 
206
187
  async post({ data = {}, endpoint = this.endpoint } = {}) {
188
+ // Non-blocking trigger to flush stuck offline events (if any)
189
+ this.flushEvents();
190
+
207
191
  let rx = {};
208
192
 
209
193
  let post_config = {
@@ -223,95 +207,160 @@ class MetaStreamIONetwork {
223
207
  post_config.headers = Object.assign(post_config.headers, this.headers);
224
208
  }
225
209
 
226
- let isOffline = false;
227
- await NetInfo.fetch().then(state => {
228
- if (!state.isConnected) {
229
- isOffline = true;
230
- }
231
- });
232
-
233
- if (isOffline) {
210
+ if (!this.isOnline) {
234
211
  this.logger.log("network", "Offline detected. Storing event.");
235
212
  await this.storeOfflineEvent(endpoint, data);
236
213
  return { status: 200, body: "Stored offline" };
237
214
  }
238
215
 
216
+ const controller = new AbortController();
217
+ const timeoutId = setTimeout(() => controller.abort(), 3000);
218
+ post_config.signal = controller.signal;
219
+
239
220
  await fetch(endpoint, post_config)
240
221
  .then((response) => {
222
+ clearTimeout(timeoutId);
223
+ rx.status = response.status;
241
224
  let _contentType = response.headers.get("content-type");
242
225
  if (!_contentType || !_contentType.includes("application/json")) {
243
226
  return null;
244
227
  } else {
245
- rx.status = response.status;
246
228
  return response;
247
229
  }
248
230
  })
249
231
  .then(async (response) => {
250
- rx.body = await response.json();
232
+ if (response) {
233
+ rx.body = await response.json();
234
+ } else {
235
+ rx.body = "OK";
236
+ }
251
237
  })
252
238
  .catch(async (err) => {
253
- this.logger.error("network", "fetch() response", err);
254
- // Store offline on network error
255
- await this.storeOfflineEvent(endpoint, data);
256
- rx.status = 200; // Treat as success to deque
257
- rx.body = "Stored offline on error";
239
+ clearTimeout(timeoutId);
240
+ this.logger.warn("network", "fetch() error in post()", err.message || err);
241
+ rx.status = 500;
242
+ rx.body = "fetch error";
243
+ throw err;
258
244
  });
259
245
 
260
246
  return rx;
261
247
  }
262
248
 
249
+ async _getQueue() {
250
+ try {
251
+ const raw = await this.storageEngine.getItem(EVENT_QUEUE_KEY);
252
+ if (!raw) return [];
253
+ const parsed = JSON.parse(raw);
254
+ return Array.isArray(parsed) ? parsed : [];
255
+ } catch {
256
+ return [];
257
+ }
258
+ }
259
+
263
260
  async storeOfflineEvent(endpoint, payload) {
264
261
  try {
265
- const existing = JSON.parse(await AsyncStorage.getItem(EVENT_QUEUE_KEY)) || [];
262
+ const existing = await this._getQueue();
266
263
  const newEvent = {
267
264
  endpoint,
268
265
  payload,
269
266
  failedAt: new Date().toISOString(),
270
267
  };
271
268
  existing.push(newEvent);
272
- await AsyncStorage.setItem(EVENT_QUEUE_KEY, JSON.stringify(existing));
269
+ await this.storageEngine.setItem(EVENT_QUEUE_KEY, JSON.stringify(existing));
273
270
  this.logger.log("network", `Stored event locally. Queue size: ${existing.length}`);
274
271
  } catch (error) {
275
- this.logger.error("network", "Failed to store event:", error);
272
+ this.logger.error("network", "Failed to store event:", error.message || error);
276
273
  }
277
274
  }
278
275
 
279
276
  async flushEvents() {
277
+ if (this._isFlushing) return;
278
+ this._isFlushing = true;
279
+
280
280
  try {
281
- const stored = JSON.parse(await AsyncStorage.getItem(EVENT_QUEUE_KEY)) || [];
281
+ const stored = await this._getQueue();
282
282
  if (stored.length === 0) {
283
283
  return;
284
284
  }
285
285
 
286
286
  this.logger.log("network", `Flushing ${stored.length} stored event(s)...`);
287
+
288
+ // Pre-flight check: Do not spam the Native Bridge with 20 offline events if the server route is down
289
+ const pingEndpoint = this.endpoint || (this.endpoints.length > 0 ? this.endpoints[0] : null);
290
+ if (pingEndpoint) {
291
+ let routeValid = false;
292
+ const controller = new AbortController();
293
+ const timeoutId = setTimeout(() => controller.abort(), 1500);
294
+
295
+ try {
296
+ await fetch(pingEndpoint, { method: "OPTIONS", signal: controller.signal });
297
+ routeValid = true;
298
+ } catch (err) {
299
+ routeValid = false;
300
+ } finally {
301
+ clearTimeout(timeoutId);
302
+ }
303
+
304
+ if (!routeValid) {
305
+ this.logger.log("network", "flushEvents aborted: Node route not fully established natively yet.");
306
+ return;
307
+ }
308
+ }
309
+
287
310
  const remainingEvents = [];
288
311
 
289
312
  for (const e of stored) {
313
+ let reqHeaders = { "Content-Type": "application/json" };
314
+ if (this.headers) {
315
+ reqHeaders = Object.assign(reqHeaders, this.headers);
316
+ }
317
+ const controller = new AbortController();
318
+ const timeoutId = setTimeout(() => controller.abort(), 3000);
319
+
290
320
  try {
291
321
  const response = await fetch(e.endpoint, {
292
322
  method: "POST",
293
- headers: { "Content-Type": "application/json" },
323
+ headers: reqHeaders,
294
324
  body: JSON.stringify(e.payload),
325
+ signal: controller.signal
295
326
  });
327
+ clearTimeout(timeoutId);
296
328
 
297
329
  if (response.ok) {
298
- this.logger.log("network", "Retried & sent event");
330
+ this.logger.log("network", "Retried & sent event successfully.");
299
331
  } else {
300
- // Drop event if server rejects it (4xx, 5xx), avoid infinite loop
301
- this.logger.warn("network", "Retry failed with non-OK status, dropping event");
332
+ this.logger.warn("network", `Retry got non-OK status (${response.status}), dropping event.`);
302
333
  }
303
334
  } catch (err) {
304
- // Keep if network error
335
+ clearTimeout(timeoutId);
336
+ this.logger.log("network", "Retry failed (still offline or bad host), keeping event.", err.message);
305
337
  remainingEvents.push(e);
306
338
  }
307
339
  }
308
340
 
309
- await AsyncStorage.setItem(EVENT_QUEUE_KEY, JSON.stringify(remainingEvents));
341
+ const currentQueue = await this._getQueue();
342
+ // Only keep events that were just inserted (after we started flushing)
343
+ // We assume new events were appended to the end of the array.
344
+ let newAdditions = [];
345
+ if (currentQueue.length > stored.length) {
346
+ newAdditions = currentQueue.slice(stored.length);
347
+ }
348
+
349
+ const updatedQueue = [...remainingEvents, ...newAdditions];
350
+
351
+ if (updatedQueue.length > 0) {
352
+ await this.storageEngine.setItem(EVENT_QUEUE_KEY, JSON.stringify(updatedQueue));
353
+ } else {
354
+ await this.storageEngine.removeItem(EVENT_QUEUE_KEY);
355
+ }
356
+
310
357
  if (remainingEvents.length < stored.length) {
311
- this.logger.log("network", `Flush complete. Remaining: ${remainingEvents.length}`);
358
+ this.logger.log("network", `Flush complete. Remaining: ${updatedQueue.length}`);
312
359
  }
313
360
  } catch (error) {
314
- this.logger.error("network", "Failed to flush events:", error);
361
+ this.logger.warn("network", "Failed to flush events:", error.message || error);
362
+ } finally {
363
+ this._isFlushing = false;
315
364
  }
316
365
  }
317
366
 
@@ -57,12 +57,19 @@ Queue.prototype.peek = function () {
57
57
  * The MetaStreamIO SAUCE
58
58
  *
59
59
  */
60
- Queue.prototype.run = function (callback) {
61
- while (this.size() > 0) {
62
- callback(this.dequeue()).catch(err => {
63
- console.error('<queue callback>', err);
64
- });
60
+ Queue.prototype.run = async function (callback) {
61
+ if (this._isRunning) return;
62
+ this._isRunning = true;
63
+
64
+ try {
65
+ while (this.size() > 0) {
66
+ await callback(this.dequeue()).catch(err => {
67
+ console.error('<queue callback>', err);
68
+ });
69
+ }
70
+ } finally {
71
+ this._isRunning = false;
65
72
  }
66
73
  };
67
74
 
68
- export {Queue};
75
+ export { Queue };
@@ -139,9 +139,14 @@ class MetaStreamIORecorder {
139
139
  // Note: We use a separate fetch here because the payload format is different from standard analytics
140
140
  const url = `${this.endpoint}/api/apps/${this.app_id}/sessions`;
141
141
  this.logger.log("recorder", `Sending session creation request to: ${url}`);
142
+
143
+ const controller = new AbortController();
144
+ const timeoutId = setTimeout(() => controller.abort(), 3000);
145
+
142
146
  await fetch(url, {
143
147
  method: 'POST',
144
148
  headers: { 'Content-Type': 'application/json' },
149
+ signal: controller.signal,
145
150
  body: JSON.stringify({
146
151
  session_id: this.sessionId,
147
152
  device_info: {
@@ -152,6 +157,7 @@ class MetaStreamIORecorder {
152
157
  }
153
158
  })
154
159
  }).then(async res => {
160
+ clearTimeout(timeoutId);
155
161
  this.logger.log("recorder", "Session creation response status: " + res.status);
156
162
  if (res.ok) {
157
163
  const data = await res.json();
@@ -164,6 +170,7 @@ class MetaStreamIORecorder {
164
170
  this.logger.error("recorder", "Failed to start session on recording server: " + res.status);
165
171
  }
166
172
  }).catch(e => {
173
+ clearTimeout(timeoutId);
167
174
  this.logger.error("recorder", "Connection error to recording server: " + e + " to " + url);
168
175
  });
169
176
 
@@ -200,6 +207,9 @@ class MetaStreamIORecorder {
200
207
  console.log("Starting loop");
201
208
  this.intervalId = setInterval(async () => {
202
209
  if (!viewRef.current) return;
210
+ if (this._isCapturing) return;
211
+
212
+ this._isCapturing = true;
203
213
 
204
214
  try {
205
215
  // Capture as base64 for comparison and direct upload
@@ -241,13 +251,15 @@ class MetaStreamIORecorder {
241
251
  // Measure privacy zones
242
252
  const zones = await this.measurePrivacyZones(viewRef);
243
253
 
244
- console.log("DBG:: Uploading frame with " + zones.length + " privacy zones");
245
- this.uploadFrame(base64, isDuplicate, zones);
254
+ // console.log("DBG:: Uploading frame with " + zones.length + " privacy zones");
255
+ await this.uploadFrame(base64, isDuplicate, zones);
246
256
  this.lastUploadTime = now;
247
257
  this.lastBase64 = base64;
248
258
  }
249
259
  } catch (e) {
250
260
  // Silent fail on capture error to avoid log spam
261
+ } finally {
262
+ this._isCapturing = false;
251
263
  }
252
264
  }, intervalMs);
253
265
  }
@@ -277,13 +289,18 @@ class MetaStreamIORecorder {
277
289
  }
278
290
 
279
291
  try {
292
+ const controller = new AbortController();
293
+ const timeoutId = setTimeout(() => controller.abort(), 1500);
294
+
280
295
  await fetch(`${this.endpoint}/api/apps/${this.app_id}/sessions/${this.recorderSessionId}/screenshot`, {
281
296
  method: 'POST',
282
297
  body: formData,
283
298
  headers: {
284
299
  'Content-Type': 'multipart/form-data',
285
- }
300
+ },
301
+ signal: controller.signal
286
302
  });
303
+ clearTimeout(timeoutId);
287
304
  } catch (e) {
288
305
  // Network error, drop frame
289
306
  }
@@ -304,11 +321,15 @@ class MetaStreamIORecorder {
304
321
  };
305
322
 
306
323
  try {
324
+ const controller = new AbortController();
325
+ const timeoutId = setTimeout(() => controller.abort(), 1500);
326
+
307
327
  fetch(`${this.endpoint}/api/apps/${this.app_id}/sessions/${this.recorderSessionId}/events`, {
308
328
  method: 'POST',
309
329
  headers: { 'Content-Type': 'application/json' },
310
- body: JSON.stringify([event])
311
- }).catch(() => { });
330
+ body: JSON.stringify([event]),
331
+ signal: controller.signal
332
+ }).then(() => clearTimeout(timeoutId)).catch(() => clearTimeout(timeoutId));
312
333
  } catch (e) { }
313
334
  }
314
335
  }
@@ -12,6 +12,7 @@ export class ConfigConstructorModel {
12
12
  quality = null,
13
13
  fps = null,
14
14
  recordingEndpoint = null,
15
+ storageEngine = null,
15
16
  } = {}) {
16
17
  this.logging = logging;
17
18
  this.loggingLevel = loggingLevel;
@@ -26,6 +27,7 @@ export class ConfigConstructorModel {
26
27
  this.quality = quality;
27
28
  this.fps = fps;
28
29
  this.recordingEndpoint = recordingEndpoint;
30
+ this.storageEngine = storageEngine;
29
31
  }
30
32
  json() {
31
33
  return {
@@ -41,6 +43,7 @@ export class ConfigConstructorModel {
41
43
  quality: this.quality,
42
44
  fps: this.fps,
43
45
  recordingEndpoint: this.recordingEndpoint,
46
+ storageEngine: this.storageEngine,
44
47
  };
45
48
  }
46
49
  }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "iidrak-analytics-react",
3
- "version": "1.3.0",
3
+ "version": "1.5.1",
4
4
  "description": "react native client for metastreamio",
5
5
  "peerDependencies": {
6
6
  "react-native-mmkv": ">=4.0.0",
7
7
  "react-native-nitro-modules": ">=0.30.0",
8
+ "@react-native-async-storage/async-storage": ">=1.17.0",
8
9
  "@react-native-community/netinfo": ">=11.5.2",
9
10
  "react": ">=18.0.0",
10
11
  "react-native": ">=0.70.0",
@@ -12,6 +13,17 @@
12
13
  "react-native-view-shot": ">=3.0.0",
13
14
  "react-native-safe-area-context": ">=4.0.0"
14
15
  },
16
+ "peerDependenciesMeta": {
17
+ "react-native-mmkv": {
18
+ "optional": true
19
+ },
20
+ "react-native-nitro-modules": {
21
+ "optional": true
22
+ },
23
+ "@react-native-async-storage/async-storage": {
24
+ "optional": true
25
+ }
26
+ },
15
27
  "dependencies": {
16
28
  "string-format": "^0.5.0"
17
29
  },