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 +2 -0
- package/documentation.md +39 -0
- package/documentation.pdf +0 -0
- package/metastreamio/metastreamio.interface.js +79 -38
- package/metastreamio/metastreamio.network.js +50 -57
- package/metastreamio/metastreamio.provider.js +63 -0
- package/metastreamio/metastreamio.queue.js +13 -6
- package/metastreamio/metastreamio.recorder.js +26 -5
- package/package.json +1 -1
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.
|
package/documentation.md
ADDED
|
@@ -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
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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.
|
|
500
|
-
|
|
501
|
-
.
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
.
|
|
506
|
-
|
|
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); //
|
|
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 (
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|
120
|
-
|
|
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
|
|
216
|
+
const controller = new AbortController();
|
|
217
|
+
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
|
218
|
+
post_config.signal = controller.signal;
|
|
233
219
|
|
|
234
|
-
await
|
|
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
|
-
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
|
317
|
+
const controller = new AbortController();
|
|
318
|
+
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
|
326
319
|
|
|
327
320
|
try {
|
|
328
|
-
const response = await
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
330
|
+
body: JSON.stringify([event]),
|
|
331
|
+
signal: controller.signal
|
|
332
|
+
}).then(() => clearTimeout(timeoutId)).catch(() => clearTimeout(timeoutId));
|
|
312
333
|
} catch (e) { }
|
|
313
334
|
}
|
|
314
335
|
}
|