@repliqo/sdk-react-native 0.3.8 → 0.4.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/INTEGRATION_GUIDE.md +1384 -1312
- package/android/src/main/java/com/repliqo/screencapture/MultiWindowCapture.java +242 -242
- package/android/src/main/java/com/repliqo/screencapture/ScreenCaptureModule.java +94 -94
- package/dist/components/RepliqoFlatList.d.ts +5 -0
- package/dist/components/RepliqoFlatList.js +39 -0
- package/dist/components/RepliqoScrollView.d.ts +3 -0
- package/dist/components/RepliqoScrollView.js +16 -265
- package/dist/components/useRepliqoScrollTracking.d.ts +33 -0
- package/dist/components/useRepliqoScrollTracking.js +304 -0
- package/dist/core/client.d.ts +52 -0
- package/dist/core/client.js +304 -16
- package/dist/core/config.d.ts +2 -2
- package/dist/core/config.js +8 -2
- package/dist/core/storage.d.ts +29 -0
- package/dist/core/storage.js +33 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/snapshot/capture.d.ts +7 -0
- package/dist/snapshot/capture.js +17 -5
- package/dist/trackers/error.tracker.d.ts +25 -0
- package/dist/trackers/error.tracker.js +114 -37
- package/dist/trackers/navigation.tracker.js +11 -0
- package/dist/transport/api.client.d.ts +34 -1
- package/dist/transport/api.client.js +102 -19
- package/dist/transport/batch-queue.d.ts +53 -1
- package/dist/transport/batch-queue.js +126 -19
- package/dist/types/events.d.ts +12 -0
- package/package.json +68 -64
- package/src/components/RepliqoFlatList.tsx +64 -0
- package/src/components/RepliqoScrollView.tsx +53 -302
- package/src/components/useRepliqoScrollTracking.ts +366 -0
- package/src/core/client.ts +953 -628
- package/src/core/config.ts +40 -30
- package/src/core/storage.ts +51 -0
- package/src/index.ts +54 -50
- package/src/snapshot/NativeScreenCapture.ts +62 -62
- package/src/snapshot/capture.ts +154 -142
- package/src/trackers/error.tracker.ts +211 -136
- package/src/trackers/navigation.tracker.ts +107 -98
- package/src/transport/api.client.ts +430 -327
- package/src/transport/batch-queue.ts +212 -95
- package/src/types/events.ts +72 -60
|
@@ -1,11 +1,60 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.BatchQueue = void 0;
|
|
3
|
+
exports.BatchQueue = exports.PartialFlushError = exports.NonRetryableFlushError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Thrown by an onFlush callback to signal that the batch was PERMANENTLY
|
|
6
|
+
* rejected (e.g. HTTP 400/413 — malformed or oversized payload). The queue
|
|
7
|
+
* drops the batch instead of retrying it forever, which would otherwise
|
|
8
|
+
* block the queue and evict everything behind it.
|
|
9
|
+
*/
|
|
10
|
+
class NonRetryableFlushError extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'NonRetryableFlushError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.NonRetryableFlushError = NonRetryableFlushError;
|
|
17
|
+
/**
|
|
18
|
+
* Thrown by an onFlush callback that delivers items in MULTIPLE requests
|
|
19
|
+
* (e.g. one events batch per session) when only a PREFIX of the items was
|
|
20
|
+
* consumed. Without it the queue would treat the flush as all-or-nothing:
|
|
21
|
+
* a transient failure after a successful sub-batch would RE-SEND the
|
|
22
|
+
* delivered items on retry (duplicates), and a poison sub-batch would
|
|
23
|
+
* DROP items belonging to never-attempted sub-batches.
|
|
24
|
+
*
|
|
25
|
+
* `consumedCount` = items delivered or permanently dropped, counted from
|
|
26
|
+
* the FRONT of the flushed array (sub-batches must be processed in order).
|
|
27
|
+
* `transient` = whether the stop reason was retryable (applies backoff and
|
|
28
|
+
* keeps the remaining items for retry).
|
|
29
|
+
*/
|
|
30
|
+
class PartialFlushError extends Error {
|
|
31
|
+
constructor(consumedCount, transient, message) {
|
|
32
|
+
super(message ?? `Partial flush: ${consumedCount} consumed`);
|
|
33
|
+
this.consumedCount = consumedCount;
|
|
34
|
+
this.transient = transient;
|
|
35
|
+
this.name = 'PartialFlushError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.PartialFlushError = PartialFlushError;
|
|
4
39
|
class BatchQueue {
|
|
5
40
|
constructor(maxSize = 20, flushIntervalMs = 30000, onFlush, maxBufferSize = 1000) {
|
|
6
41
|
this.buffer = [];
|
|
7
42
|
this.intervalId = null;
|
|
8
|
-
|
|
43
|
+
/**
|
|
44
|
+
* In-flight flush promise. flush() returns THIS promise when a flush is
|
|
45
|
+
* already running (instead of resolving immediately), so callers like
|
|
46
|
+
* endSession() that `await flush()` actually wait for delivery.
|
|
47
|
+
*/
|
|
48
|
+
this.inFlight = null;
|
|
49
|
+
/**
|
|
50
|
+
* Items evicted from the FRONT of the buffer while a flush was in flight.
|
|
51
|
+
* Evicted items are the oldest — i.e. part of the in-flight batch — so
|
|
52
|
+
* after a successful flush we must remove (sent - alreadyEvicted) items,
|
|
53
|
+
* or we'd delete unsent NEW items that shifted to the front.
|
|
54
|
+
*/
|
|
55
|
+
this.evictedDuringFlush = 0;
|
|
56
|
+
/** Number of items captured by the in-flight flush (0 when idle). */
|
|
57
|
+
this.inFlightCount = 0;
|
|
9
58
|
// Exponential backoff state: on each failure we double the skip count,
|
|
10
59
|
// capped at 8 ticks. A successful flush resets it to 0.
|
|
11
60
|
this.consecutiveFailures = 0;
|
|
@@ -18,40 +67,86 @@ class BatchQueue {
|
|
|
18
67
|
add(item) {
|
|
19
68
|
// Evict oldest items if buffer is at max capacity
|
|
20
69
|
if (this.buffer.length >= this.maxBufferSize) {
|
|
21
|
-
|
|
70
|
+
const evictCount = this.buffer.length - this.maxBufferSize + 1;
|
|
71
|
+
this.buffer.splice(0, evictCount);
|
|
72
|
+
if (this.inFlight) {
|
|
73
|
+
this.evictedDuringFlush += evictCount;
|
|
74
|
+
}
|
|
22
75
|
}
|
|
23
76
|
this.buffer.push(item);
|
|
24
77
|
if (this.buffer.length >= this.maxSize) {
|
|
25
78
|
this.flush();
|
|
26
79
|
}
|
|
27
80
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Flush the buffer. If a flush is already in flight, returns its promise
|
|
83
|
+
* (never a silent no-op). Never rejects — failures are kept in the buffer
|
|
84
|
+
* and retried with backoff by the auto-flush timer.
|
|
85
|
+
*/
|
|
86
|
+
flush() {
|
|
87
|
+
if (this.inFlight) {
|
|
88
|
+
return this.inFlight;
|
|
89
|
+
}
|
|
90
|
+
if (this.buffer.length === 0) {
|
|
91
|
+
return Promise.resolve();
|
|
31
92
|
}
|
|
32
|
-
this.
|
|
93
|
+
this.inFlight = this.doFlush().finally(() => {
|
|
94
|
+
this.inFlight = null;
|
|
95
|
+
});
|
|
96
|
+
return this.inFlight;
|
|
97
|
+
}
|
|
98
|
+
async doFlush() {
|
|
99
|
+
this.evictedDuringFlush = 0;
|
|
33
100
|
const items = [...this.buffer];
|
|
101
|
+
this.inFlightCount = items.length;
|
|
102
|
+
/** Remove `count` in-flight items from the buffer front, net of any
|
|
103
|
+
* that were already evicted (or cleared) while the flush ran. */
|
|
104
|
+
const removeConsumed = (count) => {
|
|
105
|
+
const toRemove = Math.max(0, Math.min(count, items.length) - this.evictedDuringFlush);
|
|
106
|
+
this.buffer.splice(0, toRemove);
|
|
107
|
+
// Consumed items absorb the eviction credit first.
|
|
108
|
+
this.evictedDuringFlush = Math.max(0, this.evictedDuringFlush - Math.min(count, items.length));
|
|
109
|
+
};
|
|
34
110
|
try {
|
|
35
111
|
await this.onFlush(items);
|
|
36
|
-
|
|
37
|
-
// New items might have been added during the async flush,
|
|
38
|
-
// so we splice out only the count we sent.
|
|
39
|
-
this.buffer.splice(0, items.length);
|
|
40
|
-
// Successful flush resets the backoff state.
|
|
112
|
+
removeConsumed(items.length);
|
|
41
113
|
this.consecutiveFailures = 0;
|
|
42
114
|
this.ticksToSkip = 0;
|
|
43
115
|
}
|
|
44
|
-
catch {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
116
|
+
catch (err) {
|
|
117
|
+
if (err instanceof PartialFlushError) {
|
|
118
|
+
// A prefix of the items was delivered/dropped by a multi-request
|
|
119
|
+
// flush; keep only the unconsumed remainder.
|
|
120
|
+
removeConsumed(err.consumedCount);
|
|
121
|
+
if (err.transient) {
|
|
122
|
+
this.applyBackoff();
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
this.consecutiveFailures = 0;
|
|
126
|
+
this.ticksToSkip = 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else if (err instanceof NonRetryableFlushError) {
|
|
130
|
+
// Poison batch: the server permanently rejected it. Drop it so it
|
|
131
|
+
// can't block the queue forever.
|
|
132
|
+
removeConsumed(items.length);
|
|
133
|
+
this.consecutiveFailures = 0;
|
|
134
|
+
this.ticksToSkip = 0;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// Transient failure: keep events in buffer for retry with
|
|
138
|
+
// exponential backoff (1, 2, 4, 8 ticks skipped).
|
|
139
|
+
this.applyBackoff();
|
|
140
|
+
}
|
|
50
141
|
}
|
|
51
142
|
finally {
|
|
52
|
-
this.
|
|
143
|
+
this.inFlightCount = 0;
|
|
53
144
|
}
|
|
54
145
|
}
|
|
146
|
+
applyBackoff() {
|
|
147
|
+
this.consecutiveFailures++;
|
|
148
|
+
this.ticksToSkip = Math.min(Math.pow(2, this.consecutiveFailures - 1), BatchQueue.MAX_BACKOFF_TICKS);
|
|
149
|
+
}
|
|
55
150
|
startAutoFlush() {
|
|
56
151
|
if (this.intervalId !== null) {
|
|
57
152
|
return;
|
|
@@ -75,6 +170,18 @@ class BatchQueue {
|
|
|
75
170
|
get pending() {
|
|
76
171
|
return this.buffer.length;
|
|
77
172
|
}
|
|
173
|
+
/** Read-only snapshot of the buffered items (for persistence). */
|
|
174
|
+
peekAll() {
|
|
175
|
+
return [...this.buffer];
|
|
176
|
+
}
|
|
177
|
+
/** Remove every buffered item (after persisting, or on session discard). */
|
|
178
|
+
clear() {
|
|
179
|
+
this.buffer.length = 0;
|
|
180
|
+
// If a flush is in flight, all of its items just left the buffer —
|
|
181
|
+
// account for them as evicted so the flush's completion splice does
|
|
182
|
+
// not delete NEW items added after this clear().
|
|
183
|
+
this.evictedDuringFlush = this.inFlightCount;
|
|
184
|
+
}
|
|
78
185
|
}
|
|
79
186
|
exports.BatchQueue = BatchQueue;
|
|
80
187
|
BatchQueue.MAX_BACKOFF_TICKS = 8;
|
package/dist/types/events.d.ts
CHANGED
|
@@ -50,5 +50,17 @@ export interface SDKConfig {
|
|
|
50
50
|
/** Enable full-content screen captures for heatmap backgrounds. Defaults to true when enableSnapshots is true. */
|
|
51
51
|
enableHeatmapCapture?: boolean;
|
|
52
52
|
enableCrashTracking?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Async key-value storage for offline persistence (events buffered at
|
|
55
|
+
* backgrounding + crash reports). Pass AsyncStorage from
|
|
56
|
+
* @react-native-async-storage/async-storage, or any compatible object.
|
|
57
|
+
* If omitted, the SDK tries to auto-load AsyncStorage; if unavailable,
|
|
58
|
+
* persistence is disabled (in-memory only).
|
|
59
|
+
*/
|
|
60
|
+
storage?: {
|
|
61
|
+
getItem(key: string): Promise<string | null>;
|
|
62
|
+
setItem(key: string, value: string): Promise<void>;
|
|
63
|
+
removeItem(key: string): Promise<void>;
|
|
64
|
+
};
|
|
53
65
|
debug?: boolean;
|
|
54
66
|
}
|
package/package.json
CHANGED
|
@@ -1,64 +1,68 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@repliqo/sdk-react-native",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Session replay & analytics SDK for React Native. Captures screenshots (including modals/alerts), navigation, screen visits, and custom events.",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "https://github.com/SbtnAvls/app-analytics-api",
|
|
10
|
-
"directory": "packages/sdk-react-native"
|
|
11
|
-
},
|
|
12
|
-
"homepage": "https://repliqo.odmservice.site",
|
|
13
|
-
"license": "MIT",
|
|
14
|
-
"keywords": [
|
|
15
|
-
"react-native",
|
|
16
|
-
"analytics",
|
|
17
|
-
"session-replay",
|
|
18
|
-
"screen-capture",
|
|
19
|
-
"heatmaps",
|
|
20
|
-
"uxcam",
|
|
21
|
-
"repliqo"
|
|
22
|
-
],
|
|
23
|
-
"files": [
|
|
24
|
-
"dist/",
|
|
25
|
-
"android/src/",
|
|
26
|
-
"android/build.gradle",
|
|
27
|
-
"src/",
|
|
28
|
-
"INTEGRATION_GUIDE.md"
|
|
29
|
-
],
|
|
30
|
-
"scripts": {
|
|
31
|
-
"build": "tsc",
|
|
32
|
-
"test": "jest",
|
|
33
|
-
"prepublishOnly": "npm run build"
|
|
34
|
-
},
|
|
35
|
-
"publishConfig": {
|
|
36
|
-
"access": "public"
|
|
37
|
-
},
|
|
38
|
-
"peerDependencies": {
|
|
39
|
-
"@react-navigation/native": ">=6.0.0",
|
|
40
|
-
"react": ">=17.0.0",
|
|
41
|
-
"react-native": ">=0.68.0",
|
|
42
|
-
"react-native-view-shot": ">=3.0.0"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"react
|
|
60
|
-
"react-native
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@repliqo/sdk-react-native",
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Session replay & analytics SDK for React Native. Captures screenshots (including modals/alerts), navigation, screen visits, and custom events.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/SbtnAvls/app-analytics-api",
|
|
10
|
+
"directory": "packages/sdk-react-native"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://repliqo.odmservice.site",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"react-native",
|
|
16
|
+
"analytics",
|
|
17
|
+
"session-replay",
|
|
18
|
+
"screen-capture",
|
|
19
|
+
"heatmaps",
|
|
20
|
+
"uxcam",
|
|
21
|
+
"repliqo"
|
|
22
|
+
],
|
|
23
|
+
"files": [
|
|
24
|
+
"dist/",
|
|
25
|
+
"android/src/",
|
|
26
|
+
"android/build.gradle",
|
|
27
|
+
"src/",
|
|
28
|
+
"INTEGRATION_GUIDE.md"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc",
|
|
32
|
+
"test": "jest",
|
|
33
|
+
"prepublishOnly": "npm run build"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@react-navigation/native": ">=6.0.0",
|
|
40
|
+
"react": ">=17.0.0",
|
|
41
|
+
"react-native": ">=0.68.0",
|
|
42
|
+
"react-native-view-shot": ">=3.0.0",
|
|
43
|
+
"@react-native-async-storage/async-storage": ">=1.17.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"react-native-view-shot": {
|
|
47
|
+
"optional": true
|
|
48
|
+
},
|
|
49
|
+
"@react-navigation/native": {
|
|
50
|
+
"optional": true
|
|
51
|
+
},
|
|
52
|
+
"@react-native-async-storage/async-storage": {
|
|
53
|
+
"optional": true
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@react-navigation/native": "^6.1.0",
|
|
58
|
+
"@types/jest": "^29.5.0",
|
|
59
|
+
"@types/react": "^18.2.0",
|
|
60
|
+
"@types/react-native": "^0.72.0",
|
|
61
|
+
"jest": "^29.7.0",
|
|
62
|
+
"react": "^18.2.0",
|
|
63
|
+
"react-native": "^0.73.0",
|
|
64
|
+
"react-native-view-shot": "^4.0.3",
|
|
65
|
+
"ts-jest": "^29.1.0",
|
|
66
|
+
"typescript": "^5.3.0"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { FlatList, type FlatListProps } from 'react-native';
|
|
3
|
+
import { useRepliqoScrollTracking } from './useRepliqoScrollTracking';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Drop-in replacement for React Native's FlatList with the same analytics
|
|
7
|
+
* behavior as RepliqoScrollView:
|
|
8
|
+
*
|
|
9
|
+
* 1. Reports scroll offset for accurate heatmap touch coords
|
|
10
|
+
* 2. Captures viewport tiles on scroll stops AND during continuous
|
|
11
|
+
* scroll for collaborative screen building
|
|
12
|
+
* 3. Registers its window-relative bounds with the SDK so touches can
|
|
13
|
+
* be auto-classified as "content" (inside the list) or "fixed" UI
|
|
14
|
+
*
|
|
15
|
+
* Note on tiles: FlatList virtualizes — off-screen rows aren't rendered —
|
|
16
|
+
* but that's exactly why the collaborative tile approach works here too:
|
|
17
|
+
* tiles are captured from what's ON screen as the user naturally scrolls.
|
|
18
|
+
*
|
|
19
|
+
* Note on contentHeight: with virtualization, RN estimates the content
|
|
20
|
+
* size until rows render; onContentSizeChange keeps our metadata in sync
|
|
21
|
+
* as the estimate refines.
|
|
22
|
+
*/
|
|
23
|
+
function RepliqoFlatListInner<ItemT>(
|
|
24
|
+
{
|
|
25
|
+
onScroll,
|
|
26
|
+
onMomentumScrollEnd,
|
|
27
|
+
onScrollEndDrag,
|
|
28
|
+
onLayout,
|
|
29
|
+
onContentSizeChange,
|
|
30
|
+
...props
|
|
31
|
+
}: FlatListProps<ItemT>,
|
|
32
|
+
ref: React.Ref<FlatList<ItemT>>,
|
|
33
|
+
) {
|
|
34
|
+
const tracking = useRepliqoScrollTracking(ref, {
|
|
35
|
+
onScroll,
|
|
36
|
+
onMomentumScrollEnd,
|
|
37
|
+
onScrollEndDrag,
|
|
38
|
+
onLayout,
|
|
39
|
+
onContentSizeChange,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<FlatList<ItemT>
|
|
44
|
+
ref={tracking.setRef as React.Ref<FlatList<ItemT>>}
|
|
45
|
+
{...props}
|
|
46
|
+
onScroll={tracking.onScroll}
|
|
47
|
+
onScrollEndDrag={tracking.onScrollEndDrag}
|
|
48
|
+
onMomentumScrollEnd={tracking.onMomentumScrollEnd}
|
|
49
|
+
onLayout={tracking.onLayout}
|
|
50
|
+
onContentSizeChange={tracking.onContentSizeChange}
|
|
51
|
+
scrollEventThrottle={props.scrollEventThrottle ?? 16}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const RepliqoFlatList = React.forwardRef(RepliqoFlatListInner) as <
|
|
57
|
+
ItemT,
|
|
58
|
+
>(
|
|
59
|
+
props: FlatListProps<ItemT> & {
|
|
60
|
+
ref?: React.Ref<FlatList<ItemT>>;
|
|
61
|
+
},
|
|
62
|
+
) => React.ReactElement;
|
|
63
|
+
|
|
64
|
+
(RepliqoFlatList as any).displayName = 'RepliqoFlatList';
|