@repliqo/sdk-react-native 0.3.7 → 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.
Files changed (48) hide show
  1. package/INTEGRATION_GUIDE.md +1384 -1312
  2. package/android/src/main/java/com/repliqo/screencapture/MultiWindowCapture.java +242 -230
  3. package/android/src/main/java/com/repliqo/screencapture/ScreenCaptureModule.java +94 -170
  4. package/dist/components/RepliqoFlatList.d.ts +5 -0
  5. package/dist/components/RepliqoFlatList.js +39 -0
  6. package/dist/components/RepliqoScrollView.d.ts +3 -0
  7. package/dist/components/RepliqoScrollView.js +16 -265
  8. package/dist/components/useRepliqoScrollTracking.d.ts +33 -0
  9. package/dist/components/useRepliqoScrollTracking.js +304 -0
  10. package/dist/core/client.d.ts +52 -5
  11. package/dist/core/client.js +310 -48
  12. package/dist/core/config.d.ts +1 -1
  13. package/dist/core/config.js +5 -1
  14. package/dist/core/storage.d.ts +29 -0
  15. package/dist/core/storage.js +33 -0
  16. package/dist/index.d.ts +3 -2
  17. package/dist/index.js +3 -2
  18. package/dist/snapshot/NativeScreenCapture.d.ts +6 -44
  19. package/dist/snapshot/NativeScreenCapture.js +6 -64
  20. package/dist/snapshot/capture.d.ts +7 -0
  21. package/dist/snapshot/capture.js +17 -5
  22. package/dist/trackers/error.tracker.d.ts +25 -0
  23. package/dist/trackers/error.tracker.js +114 -37
  24. package/dist/trackers/navigation.tracker.js +11 -0
  25. package/dist/transport/api.client.d.ts +34 -1
  26. package/dist/transport/api.client.js +102 -19
  27. package/dist/transport/batch-queue.d.ts +53 -1
  28. package/dist/transport/batch-queue.js +126 -19
  29. package/dist/types/events.d.ts +12 -0
  30. package/package.json +68 -64
  31. package/src/components/RepliqoFlatList.tsx +64 -0
  32. package/src/components/RepliqoScrollView.tsx +53 -302
  33. package/src/components/useRepliqoScrollTracking.ts +366 -0
  34. package/src/core/client.ts +953 -672
  35. package/src/core/config.ts +38 -30
  36. package/src/core/storage.ts +51 -0
  37. package/src/index.ts +54 -52
  38. package/src/snapshot/NativeScreenCapture.ts +62 -157
  39. package/src/snapshot/capture.ts +154 -142
  40. package/src/trackers/error.tracker.ts +211 -136
  41. package/src/trackers/navigation.tracker.ts +107 -98
  42. package/src/transport/api.client.ts +430 -327
  43. package/src/transport/batch-queue.ts +212 -95
  44. package/src/types/events.ts +72 -60
  45. package/android/src/main/java/com/repliqo/screencapture/FullContentCapture.java +0 -273
  46. package/android/src/main/java/com/repliqo/screencapture/PixelCopyScan.java +0 -279
  47. package/android/src/main/java/com/repliqo/screencapture/ScrollScanCapture.java +0 -248
  48. package/src/snapshot/ScreenCaptureManager.ts +0 -156
@@ -1,95 +1,212 @@
1
- export class BatchQueue<T> {
2
- private buffer: T[] = [];
3
- private maxSize: number;
4
- private maxBufferSize: number;
5
- private flushIntervalMs: number;
6
- private onFlush: (items: T[]) => Promise<void>;
7
- private intervalId: ReturnType<typeof setInterval> | null = null;
8
- private flushing: boolean = false;
9
- // Exponential backoff state: on each failure we double the skip count,
10
- // capped at 8 ticks. A successful flush resets it to 0.
11
- private consecutiveFailures: number = 0;
12
- private ticksToSkip: number = 0;
13
- private static readonly MAX_BACKOFF_TICKS = 8;
14
-
15
- constructor(
16
- maxSize: number = 20,
17
- flushIntervalMs: number = 30000,
18
- onFlush: (items: T[]) => Promise<void>,
19
- maxBufferSize: number = 1000,
20
- ) {
21
- this.maxSize = maxSize;
22
- this.flushIntervalMs = flushIntervalMs;
23
- this.onFlush = onFlush;
24
- this.maxBufferSize = maxBufferSize;
25
- }
26
-
27
- add(item: T): void {
28
- // Evict oldest items if buffer is at max capacity
29
- if (this.buffer.length >= this.maxBufferSize) {
30
- this.buffer.splice(0, this.buffer.length - this.maxBufferSize + 1);
31
- }
32
-
33
- this.buffer.push(item);
34
- if (this.buffer.length >= this.maxSize) {
35
- this.flush();
36
- }
37
- }
38
-
39
- async flush(): Promise<void> {
40
- if (this.buffer.length === 0 || this.flushing) {
41
- return;
42
- }
43
-
44
- this.flushing = true;
45
- const items = [...this.buffer];
46
-
47
- try {
48
- await this.onFlush(items);
49
- // Only clear the items that were successfully flushed.
50
- // New items might have been added during the async flush,
51
- // so we splice out only the count we sent.
52
- this.buffer.splice(0, items.length);
53
- // Successful flush resets the backoff state.
54
- this.consecutiveFailures = 0;
55
- this.ticksToSkip = 0;
56
- } catch {
57
- // On failure, keep events in buffer so they can be retried.
58
- // Apply exponential backoff: 1, 2, 4, 8 ticks skipped before
59
- // the next auto-flush attempt.
60
- this.consecutiveFailures++;
61
- this.ticksToSkip = Math.min(
62
- Math.pow(2, this.consecutiveFailures - 1),
63
- BatchQueue.MAX_BACKOFF_TICKS,
64
- );
65
- } finally {
66
- this.flushing = false;
67
- }
68
- }
69
-
70
- startAutoFlush(): void {
71
- if (this.intervalId !== null) {
72
- return;
73
- }
74
- this.intervalId = setInterval(() => {
75
- // Honor backoff: skip this tick if we're still in a cooldown from
76
- // a previous failure.
77
- if (this.ticksToSkip > 0) {
78
- this.ticksToSkip--;
79
- return;
80
- }
81
- this.flush();
82
- }, this.flushIntervalMs);
83
- }
84
-
85
- stopAutoFlush(): void {
86
- if (this.intervalId !== null) {
87
- clearInterval(this.intervalId);
88
- this.intervalId = null;
89
- }
90
- }
91
-
92
- get pending(): number {
93
- return this.buffer.length;
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
+ }
@@ -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
- debug?: boolean;
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
+ }