iidrak-analytics-react 1.5.0 → 1.6.0

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
@@ -5,7 +5,9 @@ A powerful, offline-first analytics SDK for React Native applications that inclu
5
5
  ## Features
6
6
 
7
7
  - 📊 **Event Tracking**: Log custom events with arbitrary parameters.
8
+ - 🚀 **Optimized Bandwidth**: Uses a Minimal Payload architecture for auto-clicks to save battery, data, and database costs, only sending full telemetry on session starts.
8
9
  - 🎥 **Session Replay**: Record screen interactions and touch events for playback analysis.
10
+ - 🎯 **Advanced Touch Context**: Automatically maps React Fiber tree to extract clicked text, component paths (e.g. `TouchableOpacity > Text`), and testing IDs without manual instrumentation.
9
11
  - 🛒 **Cart Management**: Built-in shopping cart state management.
10
12
  - 📴 **Offline Support**: Automatically queues events when offline and flushes them when connection is restored.
11
13
  - 🆔 **User & Session Management**: Auto-generates session IDs and supports user identification.
@@ -0,0 +1,39 @@
1
+ # MetaStreamIO (iidrak-analytics-react) Documentation
2
+
3
+ ## Overview
4
+ `iidrak-analytics-react` is a robust, offline-capable React Native analytics and session recording SDK. It is designed to capture user behavior smoothly without bloated payloads or manual developer instrumentation.
5
+
6
+ ## Key Architecture Concepts
7
+
8
+ ### 1. Minimal Payload Optimization
9
+ Traditional analytics tools drain user battery and network bandwidth by repeatedly sending static parameters (like the phone model, app version, and network status) on every single button tap.
10
+
11
+ Our SDK is heavily optimized. It sends a **Full Payload** only when a user initiates a session (`session_start`). For all subsequent thousands of interactions (like screen taps, swiping, or custom events), the SDK dynamically strips out these static properties and transmits a **Minimal Payload**.
12
+
13
+ This allows you to link all data backend via the unique `Session ID` while processing millions of events per second with virtually zero bandwidth overhead.
14
+
15
+ ### 2. Auto Component Mapping
16
+ By wrapping your application in the `MetaStreamProvider`, the SDK hooks directly into the React Native rendering engine (the Fiber Tree). When a user taps the screen, the SDK crawls up the UI tree in milliseconds to discover context.
17
+
18
+ This means a raw X/Y tap instantly becomes:
19
+ - **`component_type`**: E.g., `Text`, `TextInput`, `Image`.
20
+ - **`text_content`**: The literal word the user tapped on (e.g. `Add to Cart`).
21
+ - **`component_path`**: The ancestry tree of where this tap occurred (`Text > TouchableOpacity > ScrollViewContainer`).
22
+
23
+ ### 3. Queue Strategy
24
+ The SDK contains a powerful offline-first buffering engine.
25
+ - All interactions are serialized into models and placed into a robust internal device queue.
26
+ - If the device drops offline, the queue enters sleep mode.
27
+ - The moment the device connection is restored, the queue securely processes and flushes the backlog to your collector endpoints.
28
+
29
+ ## Example Backend Ingestion Strategy
30
+
31
+ Since payloads arrive in `MINIMAL` formats, it is up to your analytics backend to enrich the clickstream.
32
+
33
+ **Example ingestion:**
34
+ 1. A `session_start` event arrives: INSERT to `Sessions` table. (Contains device height, memory, version).
35
+ 2. A `login_success` minimal event arrives: INSERT to `Events` table, linking via `session_id`.
36
+ 3. An `auto_click` minimal event arrives: INSERT to `Interactions` table. Your dashboard runs a `JOIN` on the `Sessions` table using the `session_id` to build heatmaps segmented by the user's specific screen size.
37
+
38
+ ---
39
+ *Generated automatically by MetaStream SDK documentation builder.*
Binary file
@@ -197,6 +197,14 @@ class MetaStreamIO {
197
197
  });
198
198
  // Update recorder session ID
199
199
  if (this.recorder) this.recorder.setSessionId(this.session.session.id);
200
+
201
+ // Push the full payload event to register the session
202
+ setTimeout(() => {
203
+ this.trackEvent({
204
+ eventName: "session_start",
205
+ eventParameters: [],
206
+ });
207
+ }, 1500); // Small delay to allow enriching device info before it queues
200
208
  }
201
209
  } catch (err) {
202
210
  errors.push("session");
@@ -365,35 +373,66 @@ class MetaStreamIO {
365
373
  ) {
366
374
  this.logger.log("event", this.constant.MetaStreamIO_Log_SendOnce);
367
375
  } else {
368
- let e = new EventModel({
369
- account_balances: this.utility.copyObject(
370
- this.account.account.list()
371
- ),
372
- account_type: this.account.account.type,
373
- app: this.app_id,
374
- app_environment: this.app_environment,
375
- app_info: null,
376
- app_performance: null,
377
- cart: cart,
378
- cart_id: temporaryCart ? temporaryCart.id : this.cart.cart.id,
379
- channel: this.channel,
380
- ciam_id: this.user.ciam_id,
381
- client_event_date: client_event_date,
382
- client_event_timestamp: client_event_timestamp,
383
- device: null,
384
- device_token: this.user.device_token,
385
- email_id: this.user.email_id,
386
- event_name: eventName,
387
- event_params: tempEventParameters,
388
- network: null,
389
- session: this.utility.copyObject(
390
- this.session.logSession(this.previous.event_time)
391
- ),
392
- transaction_id: transactionId,
393
- user_id: this.user.id,
394
- user_country: this.user.country,
395
- user_properties: this.utility.copyObject(this.userproperties.list()),
396
- });
376
+ let e;
377
+
378
+ if (eventName === 'auto_click') {
379
+ // MINIMAL PAYLOAD FOR AUTO CLICKS
380
+ e = new EventModel({
381
+ app: this.app_id,
382
+ app_environment: this.app_environment,
383
+ client_event_date: client_event_date,
384
+ client_event_timestamp: client_event_timestamp,
385
+ event_name: eventName,
386
+ event_params: tempEventParameters,
387
+ session: { id: this.session.session.id }, // Just pass session ID
388
+ user_id: this.user.id,
389
+ // Skip everything else to save bandwidth
390
+ device: null,
391
+ app_info: null,
392
+ app_performance: null,
393
+ network: null,
394
+ user_properties: null,
395
+ account_balances: null,
396
+ account_type: null,
397
+ cart: null,
398
+ user_country: null,
399
+ email_id: null,
400
+ device_token: null,
401
+ ciam_id: null,
402
+ channel: null
403
+ });
404
+ } else {
405
+ // FULL PAYLOAD FOR CUSTOM EVENTS & SESSION START
406
+ e = new EventModel({
407
+ account_balances: this.utility.copyObject(
408
+ this.account.account.list()
409
+ ),
410
+ account_type: this.account.account.type,
411
+ app: this.app_id,
412
+ app_environment: this.app_environment,
413
+ app_info: null,
414
+ app_performance: null,
415
+ cart: cart,
416
+ cart_id: temporaryCart ? temporaryCart.id : this.cart.cart.id,
417
+ channel: this.channel,
418
+ ciam_id: this.user.ciam_id,
419
+ client_event_date: client_event_date,
420
+ client_event_timestamp: client_event_timestamp,
421
+ device: null,
422
+ device_token: this.user.device_token,
423
+ email_id: this.user.email_id,
424
+ event_name: eventName,
425
+ event_params: tempEventParameters,
426
+ network: null,
427
+ session: this.utility.copyObject(
428
+ this.session.logSession(this.previous.event_time)
429
+ ),
430
+ transaction_id: transactionId,
431
+ user_id: this.user.id,
432
+ user_country: this.user.country,
433
+ user_properties: this.utility.copyObject(this.userproperties.list()),
434
+ });
435
+ }
397
436
 
398
437
  this.storeEventMeta({
399
438
  eventTimestamp: client_event_timestamp,
@@ -495,15 +534,17 @@ class MetaStreamIO {
495
534
  async runQueue() {
496
535
  return this.queue.run(
497
536
  async function (event) {
498
- // Enrich
499
- event.app_info = await this.environment.logAppInfo().catch(() => null);
500
- event.app_performance = await this.environment
501
- .logAppPerformance()
502
- .catch(() => null);
503
- event.device = await this.environment.logDevice().catch(() => null);
504
- event.network = await this.environment
505
- .logNetworkInfo()
506
- .catch(() => null);
537
+ // Enrich only ONE time per session to save battery/bandwidth
538
+ if (event.event_name === 'session_start') {
539
+ event.app_info = await this.environment.logAppInfo().catch(() => null);
540
+ event.app_performance = await this.environment
541
+ .logAppPerformance()
542
+ .catch(() => null);
543
+ event.device = await this.environment.logDevice().catch(() => null);
544
+ event.network = await this.environment
545
+ .logNetworkInfo()
546
+ .catch(() => null);
547
+ }
507
548
 
508
549
  let response = await this.post_event(event.json())
509
550
  .then((res) => {
@@ -52,53 +52,35 @@ class MetaStreamIONetwork {
52
52
  }
53
53
  });
54
54
 
55
- // Background heartbeat to auto-flush stuck events gracefully
56
55
  this._flushInterval = setInterval(() => {
57
56
  this.flushEvents();
58
- }, 3000); // Poll every 3 seconds for perfect sync
57
+ }, 3000); // Reverted back to 3 seconds. (Locking guarantees safety)
59
58
  }
60
59
 
61
60
  reset() {
62
61
  this.endpoint = null;
62
+ this._testingPromise = null;
63
63
  }
64
64
 
65
65
  async testConnection() {
66
- 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 () => {
67
70
  for (let _e in this.endpoints) {
68
- this.endpoint = await this.getData({ endpoint: this.endpoints[_e] })
69
- .then((data) => {
70
- try {
71
- if (data && data.status > 0) {
72
- return this.endpoints[_e];
73
- }
74
- return false;
75
- } catch (err) {
76
- return false;
77
- }
78
- })
79
- .catch(() => { return false; });
80
- if (this.endpoint) {
81
- this.logger.log(
82
- "network",
83
- this.constant.MetaStreamIO_Network_EndpointSet.format(this.endpoint)
84
- );
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];
85
75
  break;
86
76
  } else {
87
- this.logger.log(
88
- "network",
89
- this.constant.MetaStreamIO_Network_EndpointTestFailed.format(
90
- this.endpoint
91
- )
92
- );
93
- this.logger.log(
94
- "network",
95
- this.constant.MetaStreamIO_Network_SilentModeEnabled
96
- );
77
+ this.logger.log("network", this.constant.MetaStreamIO_Network_EndpointTestFailed.format(this.endpoints[_e]));
97
78
  }
98
79
  }
99
- }
100
-
101
- return Promise.resolve(this.endpoint);
80
+ this._testingPromise = null;
81
+ return this.endpoint;
82
+ })();
83
+ return this._testingPromise;
102
84
  }
103
85
 
104
86
  async getData({ endpoint = this.endpoint } = {}) {
@@ -115,14 +97,16 @@ class MetaStreamIONetwork {
115
97
  get_config.headers = Object.assign(get_config.headers, this.headers);
116
98
  }
117
99
 
100
+ const controller = new AbortController();
101
+ const timeoutId = setTimeout(() => controller.abort(), 3000);
102
+ get_config.signal = controller.signal;
103
+
118
104
  try {
119
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 3000));
120
- const response = await Promise.race([
121
- fetch(endpoint, get_config),
122
- timeoutPromise
123
- ]);
105
+ const response = await fetch(endpoint, get_config);
106
+ clearTimeout(timeoutId);
124
107
  return { status: response.status, body: "OK" };
125
108
  } catch (err) {
109
+ clearTimeout(timeoutId);
126
110
  return { status: 0, body: String(err) };
127
111
  }
128
112
  }
@@ -229,13 +213,13 @@ class MetaStreamIONetwork {
229
213
  return { status: 200, body: "Stored offline" };
230
214
  }
231
215
 
232
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 3000));
216
+ const controller = new AbortController();
217
+ const timeoutId = setTimeout(() => controller.abort(), 3000);
218
+ post_config.signal = controller.signal;
233
219
 
234
- await Promise.race([
235
- fetch(endpoint, post_config),
236
- timeoutPromise
237
- ])
220
+ await fetch(endpoint, post_config)
238
221
  .then((response) => {
222
+ clearTimeout(timeoutId);
239
223
  rx.status = response.status;
240
224
  let _contentType = response.headers.get("content-type");
241
225
  if (!_contentType || !_contentType.includes("application/json")) {
@@ -252,6 +236,7 @@ class MetaStreamIONetwork {
252
236
  }
253
237
  })
254
238
  .catch(async (err) => {
239
+ clearTimeout(timeoutId);
255
240
  this.logger.warn("network", "fetch() error in post()", err.message || err);
256
241
  rx.status = 500;
257
242
  rx.body = "fetch error";
@@ -303,11 +288,18 @@ class MetaStreamIONetwork {
303
288
  // Pre-flight check: Do not spam the Native Bridge with 20 offline events if the server route is down
304
289
  const pingEndpoint = this.endpoint || (this.endpoints.length > 0 ? this.endpoints[0] : null);
305
290
  if (pingEndpoint) {
306
- const pingTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 1500));
307
- const routeValid = await Promise.race([
308
- fetch(pingEndpoint, { method: "OPTIONS" }),
309
- pingTimeout
310
- ]).then(() => true).catch(() => false);
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
+ }
311
303
 
312
304
  if (!routeValid) {
313
305
  this.logger.log("network", "flushEvents aborted: Node route not fully established natively yet.");
@@ -322,17 +314,17 @@ class MetaStreamIONetwork {
322
314
  if (this.headers) {
323
315
  reqHeaders = Object.assign(reqHeaders, this.headers);
324
316
  }
325
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 3000));
317
+ const controller = new AbortController();
318
+ const timeoutId = setTimeout(() => controller.abort(), 3000);
326
319
 
327
320
  try {
328
- const response = await Promise.race([
329
- fetch(e.endpoint, {
330
- method: "POST",
331
- headers: reqHeaders,
332
- body: JSON.stringify(e.payload),
333
- }),
334
- timeoutPromise
335
- ]);
321
+ const response = await fetch(e.endpoint, {
322
+ method: "POST",
323
+ headers: reqHeaders,
324
+ body: JSON.stringify(e.payload),
325
+ signal: controller.signal
326
+ });
327
+ clearTimeout(timeoutId);
336
328
 
337
329
  if (response.ok) {
338
330
  this.logger.log("network", "Retried & sent event successfully.");
@@ -340,6 +332,7 @@ class MetaStreamIONetwork {
340
332
  this.logger.warn("network", `Retry got non-OK status (${response.status}), dropping event.`);
341
333
  }
342
334
  } catch (err) {
335
+ clearTimeout(timeoutId);
343
336
  this.logger.log("network", "Retry failed (still offline or bad host), keeping event.", err.message);
344
337
  remainingEvents.push(e);
345
338
  }
@@ -23,6 +23,69 @@ class MetaStreamProvider extends Component {
23
23
  if (this.tracker && this.tracker.recorder) {
24
24
  this.tracker.recorder.recordInteraction(type, e.nativeEvent);
25
25
  }
26
+
27
+ // Auto-capture all screen taps/clicks
28
+ if (type === 'touch_end' && this.tracker && typeof this.tracker.trackEvent === 'function') {
29
+ const { pageX, pageY, locationX, locationY, target, timestamp } = e.nativeEvent;
30
+
31
+ let componentType = 'unknown';
32
+ let testID = 'none';
33
+ let accessibilityLabel = 'none';
34
+ let textContent = '';
35
+ let componentPath = [];
36
+
37
+ // Traverse the React Fiber tree to extract component names, text, and testIDs
38
+ try {
39
+ let fiber = e._targetInst;
40
+ while (fiber) {
41
+ if (fiber.memoizedProps) {
42
+ if (testID === 'none' && fiber.memoizedProps.testID) testID = fiber.memoizedProps.testID;
43
+ if (accessibilityLabel === 'none' && fiber.memoizedProps.accessibilityLabel) accessibilityLabel = fiber.memoizedProps.accessibilityLabel;
44
+
45
+ // Extract text content if available
46
+ let children = fiber.memoizedProps.children;
47
+ if (!textContent && typeof children === 'string') {
48
+ textContent = children;
49
+ } else if (!textContent && Array.isArray(children)) {
50
+ const stringChildren = children.filter(c => typeof c === 'string').join('');
51
+ if (stringChildren) textContent = stringChildren;
52
+ }
53
+ }
54
+
55
+ // Try to get the component's name (e.g., 'Text', 'RCTImageView', 'Button')
56
+ if (fiber.elementType) {
57
+ let name = typeof fiber.elementType === 'string' ? fiber.elementType : (fiber.elementType.name || fiber.elementType.displayName);
58
+ if (name) {
59
+ if (name !== 'View' && name !== 'RCTView' && componentType === 'unknown') {
60
+ componentType = name;
61
+ }
62
+ // Build a path of component hierarchy (limit to nearest 5 interesting elements to avoid giant payload)
63
+ if (name !== 'View' && name !== 'RCTView' && componentPath.length < 5) {
64
+ componentPath.push(name);
65
+ }
66
+ }
67
+ }
68
+ fiber = fiber.return; // Traverse up the tree
69
+ }
70
+ } catch (err) {
71
+ // Ignore any errors if structure changes in future RN versions
72
+ }
73
+
74
+ this.tracker.trackEvent({
75
+ eventName: 'auto_click',
76
+ eventParameters: [
77
+ { key: 'x', value: Math.round(pageX || locationX || 0) },
78
+ { key: 'y', value: Math.round(pageY || locationY || 0) },
79
+ { key: 'target_node', value: target ? String(target) : 'unknown' },
80
+ { key: 'component_type', value: String(componentType) },
81
+ { key: 'component_path', value: componentPath.join(' > ') || 'unknown' },
82
+ { key: 'text_content', value: textContent ? String(textContent) : 'none' },
83
+ { key: 'test_id', value: String(testID) },
84
+ { key: 'accessibility_label', value: String(accessibilityLabel) },
85
+ { key: 'timestamp', value: timestamp || Date.now() }
86
+ ]
87
+ });
88
+ }
26
89
  }
27
90
 
28
91
  render() {
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iidrak-analytics-react",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "react native client for metastreamio",
5
5
  "peerDependencies": {
6
6
  "react-native-mmkv": ">=4.0.0",