@repliqo/sdk-react-native 0.3.8 → 0.4.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/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 +1 -1
- package/dist/core/config.js +5 -1
- 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 +38 -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,95 +1,212 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
this.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by an onFlush callback to signal that the batch was PERMANENTLY
|
|
3
|
+
* rejected (e.g. HTTP 400/413 — malformed or oversized payload). The queue
|
|
4
|
+
* drops the batch instead of retrying it forever, which would otherwise
|
|
5
|
+
* block the queue and evict everything behind it.
|
|
6
|
+
*/
|
|
7
|
+
export class NonRetryableFlushError extends Error {
|
|
8
|
+
constructor(message: string) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'NonRetryableFlushError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Thrown by an onFlush callback that delivers items in MULTIPLE requests
|
|
16
|
+
* (e.g. one events batch per session) when only a PREFIX of the items was
|
|
17
|
+
* consumed. Without it the queue would treat the flush as all-or-nothing:
|
|
18
|
+
* a transient failure after a successful sub-batch would RE-SEND the
|
|
19
|
+
* delivered items on retry (duplicates), and a poison sub-batch would
|
|
20
|
+
* DROP items belonging to never-attempted sub-batches.
|
|
21
|
+
*
|
|
22
|
+
* `consumedCount` = items delivered or permanently dropped, counted from
|
|
23
|
+
* the FRONT of the flushed array (sub-batches must be processed in order).
|
|
24
|
+
* `transient` = whether the stop reason was retryable (applies backoff and
|
|
25
|
+
* keeps the remaining items for retry).
|
|
26
|
+
*/
|
|
27
|
+
export class PartialFlushError extends Error {
|
|
28
|
+
constructor(
|
|
29
|
+
public readonly consumedCount: number,
|
|
30
|
+
public readonly transient: boolean,
|
|
31
|
+
message?: string,
|
|
32
|
+
) {
|
|
33
|
+
super(message ?? `Partial flush: ${consumedCount} consumed`);
|
|
34
|
+
this.name = 'PartialFlushError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class BatchQueue<T> {
|
|
39
|
+
private buffer: T[] = [];
|
|
40
|
+
private maxSize: number;
|
|
41
|
+
private maxBufferSize: number;
|
|
42
|
+
private flushIntervalMs: number;
|
|
43
|
+
private onFlush: (items: T[]) => Promise<void>;
|
|
44
|
+
private intervalId: ReturnType<typeof setInterval> | null = null;
|
|
45
|
+
/**
|
|
46
|
+
* In-flight flush promise. flush() returns THIS promise when a flush is
|
|
47
|
+
* already running (instead of resolving immediately), so callers like
|
|
48
|
+
* endSession() that `await flush()` actually wait for delivery.
|
|
49
|
+
*/
|
|
50
|
+
private inFlight: Promise<void> | null = null;
|
|
51
|
+
/**
|
|
52
|
+
* Items evicted from the FRONT of the buffer while a flush was in flight.
|
|
53
|
+
* Evicted items are the oldest — i.e. part of the in-flight batch — so
|
|
54
|
+
* after a successful flush we must remove (sent - alreadyEvicted) items,
|
|
55
|
+
* or we'd delete unsent NEW items that shifted to the front.
|
|
56
|
+
*/
|
|
57
|
+
private evictedDuringFlush = 0;
|
|
58
|
+
/** Number of items captured by the in-flight flush (0 when idle). */
|
|
59
|
+
private inFlightCount = 0;
|
|
60
|
+
// Exponential backoff state: on each failure we double the skip count,
|
|
61
|
+
// capped at 8 ticks. A successful flush resets it to 0.
|
|
62
|
+
private consecutiveFailures: number = 0;
|
|
63
|
+
private ticksToSkip: number = 0;
|
|
64
|
+
private static readonly MAX_BACKOFF_TICKS = 8;
|
|
65
|
+
|
|
66
|
+
constructor(
|
|
67
|
+
maxSize: number = 20,
|
|
68
|
+
flushIntervalMs: number = 30000,
|
|
69
|
+
onFlush: (items: T[]) => Promise<void>,
|
|
70
|
+
maxBufferSize: number = 1000,
|
|
71
|
+
) {
|
|
72
|
+
this.maxSize = maxSize;
|
|
73
|
+
this.flushIntervalMs = flushIntervalMs;
|
|
74
|
+
this.onFlush = onFlush;
|
|
75
|
+
this.maxBufferSize = maxBufferSize;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
add(item: T): void {
|
|
79
|
+
// Evict oldest items if buffer is at max capacity
|
|
80
|
+
if (this.buffer.length >= this.maxBufferSize) {
|
|
81
|
+
const evictCount = this.buffer.length - this.maxBufferSize + 1;
|
|
82
|
+
this.buffer.splice(0, evictCount);
|
|
83
|
+
if (this.inFlight) {
|
|
84
|
+
this.evictedDuringFlush += evictCount;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.buffer.push(item);
|
|
89
|
+
if (this.buffer.length >= this.maxSize) {
|
|
90
|
+
this.flush();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Flush the buffer. If a flush is already in flight, returns its promise
|
|
96
|
+
* (never a silent no-op). Never rejects — failures are kept in the buffer
|
|
97
|
+
* and retried with backoff by the auto-flush timer.
|
|
98
|
+
*/
|
|
99
|
+
flush(): Promise<void> {
|
|
100
|
+
if (this.inFlight) {
|
|
101
|
+
return this.inFlight;
|
|
102
|
+
}
|
|
103
|
+
if (this.buffer.length === 0) {
|
|
104
|
+
return Promise.resolve();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.inFlight = this.doFlush().finally(() => {
|
|
108
|
+
this.inFlight = null;
|
|
109
|
+
});
|
|
110
|
+
return this.inFlight;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private async doFlush(): Promise<void> {
|
|
114
|
+
this.evictedDuringFlush = 0;
|
|
115
|
+
const items = [...this.buffer];
|
|
116
|
+
this.inFlightCount = items.length;
|
|
117
|
+
|
|
118
|
+
/** Remove `count` in-flight items from the buffer front, net of any
|
|
119
|
+
* that were already evicted (or cleared) while the flush ran. */
|
|
120
|
+
const removeConsumed = (count: number) => {
|
|
121
|
+
const toRemove = Math.max(
|
|
122
|
+
0,
|
|
123
|
+
Math.min(count, items.length) - this.evictedDuringFlush,
|
|
124
|
+
);
|
|
125
|
+
this.buffer.splice(0, toRemove);
|
|
126
|
+
// Consumed items absorb the eviction credit first.
|
|
127
|
+
this.evictedDuringFlush = Math.max(
|
|
128
|
+
0,
|
|
129
|
+
this.evictedDuringFlush - Math.min(count, items.length),
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
await this.onFlush(items);
|
|
135
|
+
removeConsumed(items.length);
|
|
136
|
+
this.consecutiveFailures = 0;
|
|
137
|
+
this.ticksToSkip = 0;
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (err instanceof PartialFlushError) {
|
|
140
|
+
// A prefix of the items was delivered/dropped by a multi-request
|
|
141
|
+
// flush; keep only the unconsumed remainder.
|
|
142
|
+
removeConsumed(err.consumedCount);
|
|
143
|
+
if (err.transient) {
|
|
144
|
+
this.applyBackoff();
|
|
145
|
+
} else {
|
|
146
|
+
this.consecutiveFailures = 0;
|
|
147
|
+
this.ticksToSkip = 0;
|
|
148
|
+
}
|
|
149
|
+
} else if (err instanceof NonRetryableFlushError) {
|
|
150
|
+
// Poison batch: the server permanently rejected it. Drop it so it
|
|
151
|
+
// can't block the queue forever.
|
|
152
|
+
removeConsumed(items.length);
|
|
153
|
+
this.consecutiveFailures = 0;
|
|
154
|
+
this.ticksToSkip = 0;
|
|
155
|
+
} else {
|
|
156
|
+
// Transient failure: keep events in buffer for retry with
|
|
157
|
+
// exponential backoff (1, 2, 4, 8 ticks skipped).
|
|
158
|
+
this.applyBackoff();
|
|
159
|
+
}
|
|
160
|
+
} finally {
|
|
161
|
+
this.inFlightCount = 0;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private applyBackoff(): void {
|
|
166
|
+
this.consecutiveFailures++;
|
|
167
|
+
this.ticksToSkip = Math.min(
|
|
168
|
+
Math.pow(2, this.consecutiveFailures - 1),
|
|
169
|
+
BatchQueue.MAX_BACKOFF_TICKS,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
startAutoFlush(): void {
|
|
174
|
+
if (this.intervalId !== null) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
this.intervalId = setInterval(() => {
|
|
178
|
+
// Honor backoff: skip this tick if we're still in a cooldown from
|
|
179
|
+
// a previous failure.
|
|
180
|
+
if (this.ticksToSkip > 0) {
|
|
181
|
+
this.ticksToSkip--;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
this.flush();
|
|
185
|
+
}, this.flushIntervalMs);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
stopAutoFlush(): void {
|
|
189
|
+
if (this.intervalId !== null) {
|
|
190
|
+
clearInterval(this.intervalId);
|
|
191
|
+
this.intervalId = null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
get pending(): number {
|
|
196
|
+
return this.buffer.length;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Read-only snapshot of the buffered items (for persistence). */
|
|
200
|
+
peekAll(): readonly T[] {
|
|
201
|
+
return [...this.buffer];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Remove every buffered item (after persisting, or on session discard). */
|
|
205
|
+
clear(): void {
|
|
206
|
+
this.buffer.length = 0;
|
|
207
|
+
// If a flush is in flight, all of its items just left the buffer —
|
|
208
|
+
// account for them as evicted so the flush's completion splice does
|
|
209
|
+
// not delete NEW items added after this clear().
|
|
210
|
+
this.evictedDuringFlush = this.inFlightCount;
|
|
211
|
+
}
|
|
212
|
+
}
|
package/src/types/events.ts
CHANGED
|
@@ -1,60 +1,72 @@
|
|
|
1
|
-
export type EventType = 'touch' | 'scroll' | 'gesture' | 'navigation' | 'custom';
|
|
2
|
-
|
|
3
|
-
export interface AnalyticsEvent {
|
|
4
|
-
type: EventType;
|
|
5
|
-
screenName?: string;
|
|
6
|
-
data: Record<string, any>;
|
|
7
|
-
timestamp: string; // ISO 8601
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface TouchEventData {
|
|
11
|
-
x: number;
|
|
12
|
-
y: number;
|
|
13
|
-
elementTag?: string;
|
|
14
|
-
elementText?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface NavigationEventData {
|
|
18
|
-
fromScreen: string;
|
|
19
|
-
toScreen: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ScreenVisit {
|
|
23
|
-
sessionId: string;
|
|
24
|
-
screenName: string;
|
|
25
|
-
enteredAt: string;
|
|
26
|
-
exitedAt?: string;
|
|
27
|
-
duration?: number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface DeviceInfo {
|
|
31
|
-
os: string;
|
|
32
|
-
osVersion: string;
|
|
33
|
-
model: string;
|
|
34
|
-
brand: string;
|
|
35
|
-
screenWidth: number;
|
|
36
|
-
screenHeight: number;
|
|
37
|
-
appVersion?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface SDKConfig {
|
|
41
|
-
apiKey: string;
|
|
42
|
-
appId: string;
|
|
43
|
-
batchSize?: number;
|
|
44
|
-
flushInterval?: number;
|
|
45
|
-
enableTouchTracking?: boolean;
|
|
46
|
-
enableNavigationTracking?: boolean;
|
|
47
|
-
enableSnapshots?: boolean;
|
|
48
|
-
snapshotInterval?: number;
|
|
49
|
-
maxSnapshotsPerSession?: number;
|
|
50
|
-
/** Max snapshots per network batch. Defaults to 8. */
|
|
51
|
-
snapshotBatchSize?: number;
|
|
52
|
-
/** Auto-flush interval for the snapshot queue (ms). Defaults to 15000. */
|
|
53
|
-
snapshotFlushInterval?: number;
|
|
54
|
-
/** Max snapshots kept in memory if the backend is unreachable. Defaults to 32. */
|
|
55
|
-
snapshotMaxBufferSize?: number;
|
|
56
|
-
/** Enable full-content screen captures for heatmap backgrounds. Defaults to true when enableSnapshots is true. */
|
|
57
|
-
enableHeatmapCapture?: boolean;
|
|
58
|
-
enableCrashTracking?: boolean;
|
|
59
|
-
|
|
60
|
-
|
|
1
|
+
export type EventType = 'touch' | 'scroll' | 'gesture' | 'navigation' | 'custom';
|
|
2
|
+
|
|
3
|
+
export interface AnalyticsEvent {
|
|
4
|
+
type: EventType;
|
|
5
|
+
screenName?: string;
|
|
6
|
+
data: Record<string, any>;
|
|
7
|
+
timestamp: string; // ISO 8601
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TouchEventData {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
elementTag?: string;
|
|
14
|
+
elementText?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface NavigationEventData {
|
|
18
|
+
fromScreen: string;
|
|
19
|
+
toScreen: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ScreenVisit {
|
|
23
|
+
sessionId: string;
|
|
24
|
+
screenName: string;
|
|
25
|
+
enteredAt: string;
|
|
26
|
+
exitedAt?: string;
|
|
27
|
+
duration?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DeviceInfo {
|
|
31
|
+
os: string;
|
|
32
|
+
osVersion: string;
|
|
33
|
+
model: string;
|
|
34
|
+
brand: string;
|
|
35
|
+
screenWidth: number;
|
|
36
|
+
screenHeight: number;
|
|
37
|
+
appVersion?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SDKConfig {
|
|
41
|
+
apiKey: string;
|
|
42
|
+
appId: string;
|
|
43
|
+
batchSize?: number;
|
|
44
|
+
flushInterval?: number;
|
|
45
|
+
enableTouchTracking?: boolean;
|
|
46
|
+
enableNavigationTracking?: boolean;
|
|
47
|
+
enableSnapshots?: boolean;
|
|
48
|
+
snapshotInterval?: number;
|
|
49
|
+
maxSnapshotsPerSession?: number;
|
|
50
|
+
/** Max snapshots per network batch. Defaults to 8. */
|
|
51
|
+
snapshotBatchSize?: number;
|
|
52
|
+
/** Auto-flush interval for the snapshot queue (ms). Defaults to 15000. */
|
|
53
|
+
snapshotFlushInterval?: number;
|
|
54
|
+
/** Max snapshots kept in memory if the backend is unreachable. Defaults to 32. */
|
|
55
|
+
snapshotMaxBufferSize?: number;
|
|
56
|
+
/** Enable full-content screen captures for heatmap backgrounds. Defaults to true when enableSnapshots is true. */
|
|
57
|
+
enableHeatmapCapture?: boolean;
|
|
58
|
+
enableCrashTracking?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Async key-value storage for offline persistence (events buffered at
|
|
61
|
+
* backgrounding + crash reports). Pass AsyncStorage from
|
|
62
|
+
* @react-native-async-storage/async-storage, or any compatible object.
|
|
63
|
+
* If omitted, the SDK tries to auto-load AsyncStorage; if unavailable,
|
|
64
|
+
* persistence is disabled (in-memory only).
|
|
65
|
+
*/
|
|
66
|
+
storage?: {
|
|
67
|
+
getItem(key: string): Promise<string | null>;
|
|
68
|
+
setItem(key: string, value: string): Promise<void>;
|
|
69
|
+
removeItem(key: string): Promise<void>;
|
|
70
|
+
};
|
|
71
|
+
debug?: boolean;
|
|
72
|
+
}
|