@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,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ApiClient = void 0;
4
+ const batch_queue_1 = require("./batch-queue");
5
+ /** Abort any request that hasn't completed within this window. */
6
+ const FETCH_TIMEOUT_MS = 15000;
4
7
  class ApiClient {
5
8
  constructor(baseUrl, apiKey, logger) {
6
9
  this.baseUrl = baseUrl.replace(/\/+$/, '');
@@ -13,9 +16,61 @@ class ApiClient {
13
16
  'x-api-key': this.apiKey,
14
17
  };
15
18
  }
19
+ /**
20
+ * fetch with a hard timeout. Without it, a single stalled request keeps
21
+ * the BatchQueue's in-flight lock held indefinitely — no flush can run,
22
+ * the buffer fills up and starts evicting events (silent data loss).
23
+ */
24
+ async fetchWithTimeout(url, init) {
25
+ const controller = new AbortController();
26
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
27
+ try {
28
+ return await fetch(url, { ...init, signal: controller.signal });
29
+ }
30
+ finally {
31
+ clearTimeout(timer);
32
+ }
33
+ }
34
+ /**
35
+ * Classify a non-ok batch response. Permanent PAYLOAD errors (the batch
36
+ * itself will never be accepted: 400 malformed, 413 too large, 422
37
+ * unprocessable, 404 gone) surface as NonRetryableFlushError so the
38
+ * queue drops the poison batch instead of retrying it forever.
39
+ *
40
+ * 401/403 are NOT the payload's fault — they mean the API key is wrong
41
+ * or was rotated. Treat them as transient (retry with backoff) so a key
42
+ * rotation doesn't silently destroy every live SDK's buffered data.
43
+ * 408/429 are transient by definition.
44
+ */
45
+ batchError(context, status) {
46
+ const message = `${context}: ${status}`;
47
+ const isPermanent = status >= 400 &&
48
+ status < 500 &&
49
+ status !== 401 &&
50
+ status !== 403 &&
51
+ status !== 408 &&
52
+ status !== 429;
53
+ return isPermanent
54
+ ? new batch_queue_1.NonRetryableFlushError(message)
55
+ : new Error(message);
56
+ }
57
+ /**
58
+ * Parse a 2xx response body, tolerating malformed JSON. The server
59
+ * already accepted the data — throwing here would make the queue retry
60
+ * an already-persisted batch and create duplicates.
61
+ */
62
+ async safeJson(response, fallback) {
63
+ try {
64
+ return (await response.json());
65
+ }
66
+ catch {
67
+ this.logger.warn('Response body was not valid JSON; treating as success');
68
+ return fallback;
69
+ }
70
+ }
16
71
  async startSession(deviceId, deviceInfo) {
17
72
  try {
18
- const response = await fetch(`${this.baseUrl}/api/sessions/start`, {
73
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/api/sessions/start`, {
19
74
  method: 'POST',
20
75
  headers: this.getHeaders(),
21
76
  body: JSON.stringify({ deviceId, deviceInfo }),
@@ -36,7 +91,7 @@ class ApiClient {
36
91
  }
37
92
  async endSession(sessionId) {
38
93
  try {
39
- const response = await fetch(`${this.baseUrl}/api/sessions/${sessionId}/end`, {
94
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/api/sessions/${sessionId}/end`, {
40
95
  method: 'PATCH',
41
96
  headers: this.getHeaders(),
42
97
  });
@@ -51,19 +106,41 @@ class ApiClient {
51
106
  this.logger.error('Error ending session:', error);
52
107
  }
53
108
  }
109
+ /**
110
+ * Attach the host app's user identity to a session.
111
+ * Fire-and-forget: errors are logged but never thrown.
112
+ */
113
+ async identifySession(sessionId, userId, traits) {
114
+ try {
115
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/api/sessions/${sessionId}/identify`, {
116
+ method: 'PATCH',
117
+ headers: this.getHeaders(),
118
+ body: JSON.stringify({ userId, traits }),
119
+ });
120
+ if (!response.ok) {
121
+ const text = await response.text();
122
+ this.logger.warn('Identify failed:', response.status, text?.substring(0, 200));
123
+ return;
124
+ }
125
+ this.logger.log('Session identified as user:', userId);
126
+ }
127
+ catch (error) {
128
+ this.logger.warn('Error identifying session:', error);
129
+ }
130
+ }
54
131
  async sendEventsBatch(sessionId, events) {
55
132
  try {
56
- const response = await fetch(`${this.baseUrl}/api/events/batch`, {
133
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/api/events/batch`, {
57
134
  method: 'POST',
58
135
  headers: this.getHeaders(),
59
136
  body: JSON.stringify({ sessionId, events }),
60
137
  });
61
138
  if (!response.ok) {
62
- const text = await response.text();
139
+ const text = await response.text().catch(() => '');
63
140
  this.logger.error('Failed to send events batch:', response.status, text);
64
- throw new Error(`Failed to send events batch: ${response.status}`);
141
+ throw this.batchError('Failed to send events batch', response.status);
65
142
  }
66
- const data = await response.json();
143
+ const data = await this.safeJson(response, { count: events.length });
67
144
  this.logger.log('Events batch sent, count:', data.count);
68
145
  return { count: data.count };
69
146
  }
@@ -74,15 +151,15 @@ class ApiClient {
74
151
  }
75
152
  async sendScreenVisitsBatch(visits) {
76
153
  try {
77
- const response = await fetch(`${this.baseUrl}/api/screens/visit/batch`, {
154
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/api/screens/visit/batch`, {
78
155
  method: 'POST',
79
156
  headers: this.getHeaders(),
80
157
  body: JSON.stringify({ visits }),
81
158
  });
82
159
  if (!response.ok) {
83
- const text = await response.text();
160
+ const text = await response.text().catch(() => '');
84
161
  this.logger.error('Failed to send screen visits batch:', response.status, text);
85
- throw new Error(`Failed to send screen visits batch: ${response.status}`);
162
+ throw this.batchError('Failed to send screen visits batch', response.status);
86
163
  }
87
164
  this.logger.log('Screen visits batch sent, count:', visits.length);
88
165
  }
@@ -93,17 +170,17 @@ class ApiClient {
93
170
  }
94
171
  async sendSnapshotsBatch(snapshots) {
95
172
  try {
96
- const response = await fetch(`${this.baseUrl}/api/snapshots/batch`, {
173
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/api/snapshots/batch`, {
97
174
  method: 'POST',
98
175
  headers: this.getHeaders(),
99
176
  body: JSON.stringify({ snapshots }),
100
177
  });
101
178
  if (!response.ok) {
102
- const text = await response.text();
179
+ const text = await response.text().catch(() => '');
103
180
  this.logger.error('Failed to send snapshots batch:', response.status, text);
104
- throw new Error(`Failed to send snapshots batch: ${response.status}`);
181
+ throw this.batchError('Failed to send snapshots batch', response.status);
105
182
  }
106
- const data = await response.json();
183
+ const data = await this.safeJson(response, { count: snapshots.length });
107
184
  this.logger.log('Snapshots batch sent, count:', data.count);
108
185
  return { count: data.count };
109
186
  }
@@ -112,28 +189,34 @@ class ApiClient {
112
189
  throw error;
113
190
  }
114
191
  }
192
+ /**
193
+ * Send a single crash report. Never throws; returns true when the
194
+ * server accepted it (used to clear the persisted pending copy).
195
+ */
115
196
  async sendCrashReport(crash) {
116
197
  try {
117
- const response = await fetch(`${this.baseUrl}/api/crashes`, {
198
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/api/crashes`, {
118
199
  method: 'POST',
119
200
  headers: this.getHeaders(),
120
201
  body: JSON.stringify(crash),
121
202
  });
122
203
  if (!response.ok) {
123
- const text = await response.text();
204
+ const text = await response.text().catch(() => '');
124
205
  this.logger.error('Failed to send crash report:', response.status, text);
125
- return;
206
+ return false;
126
207
  }
127
208
  this.logger.log('Crash report sent:', crash.type, crash.message);
209
+ return true;
128
210
  }
129
211
  catch (error) {
130
212
  // Fire-and-forget: catch errors silently, never throw
131
213
  this.logger.error('Error sending crash report:', error);
214
+ return false;
132
215
  }
133
216
  }
134
217
  async sendCrashesBatch(crashes) {
135
218
  try {
136
- const response = await fetch(`${this.baseUrl}/api/crashes/batch`, {
219
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/api/crashes/batch`, {
137
220
  method: 'POST',
138
221
  headers: this.getHeaders(),
139
222
  body: JSON.stringify({ crashes }),
@@ -184,7 +267,7 @@ class ApiClient {
184
267
  if (typeof windowHeight === 'number' && Number.isFinite(windowHeight)) {
185
268
  body.windowHeight = Math.round(windowHeight);
186
269
  }
187
- const response = await fetch(`${this.baseUrl}/api/screen-tiles`, {
270
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/api/screen-tiles`, {
188
271
  method: 'POST',
189
272
  headers: this.getHeaders(),
190
273
  body: JSON.stringify(body),
@@ -200,7 +283,7 @@ class ApiClient {
200
283
  }
201
284
  async uploadScreenCapture(screenName, image, width, height, isFullContent) {
202
285
  try {
203
- const response = await fetch(`${this.baseUrl}/api/screen-captures`, {
286
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/api/screen-captures`, {
204
287
  method: 'POST',
205
288
  headers: this.getHeaders(),
206
289
  body: JSON.stringify({
@@ -1,3 +1,30 @@
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 declare class NonRetryableFlushError extends Error {
8
+ constructor(message: string);
9
+ }
10
+ /**
11
+ * Thrown by an onFlush callback that delivers items in MULTIPLE requests
12
+ * (e.g. one events batch per session) when only a PREFIX of the items was
13
+ * consumed. Without it the queue would treat the flush as all-or-nothing:
14
+ * a transient failure after a successful sub-batch would RE-SEND the
15
+ * delivered items on retry (duplicates), and a poison sub-batch would
16
+ * DROP items belonging to never-attempted sub-batches.
17
+ *
18
+ * `consumedCount` = items delivered or permanently dropped, counted from
19
+ * the FRONT of the flushed array (sub-batches must be processed in order).
20
+ * `transient` = whether the stop reason was retryable (applies backoff and
21
+ * keeps the remaining items for retry).
22
+ */
23
+ export declare class PartialFlushError extends Error {
24
+ readonly consumedCount: number;
25
+ readonly transient: boolean;
26
+ constructor(consumedCount: number, transient: boolean, message?: string);
27
+ }
1
28
  export declare class BatchQueue<T> {
2
29
  private buffer;
3
30
  private maxSize;
@@ -5,14 +32,39 @@ export declare class BatchQueue<T> {
5
32
  private flushIntervalMs;
6
33
  private onFlush;
7
34
  private intervalId;
8
- private flushing;
35
+ /**
36
+ * In-flight flush promise. flush() returns THIS promise when a flush is
37
+ * already running (instead of resolving immediately), so callers like
38
+ * endSession() that `await flush()` actually wait for delivery.
39
+ */
40
+ private inFlight;
41
+ /**
42
+ * Items evicted from the FRONT of the buffer while a flush was in flight.
43
+ * Evicted items are the oldest — i.e. part of the in-flight batch — so
44
+ * after a successful flush we must remove (sent - alreadyEvicted) items,
45
+ * or we'd delete unsent NEW items that shifted to the front.
46
+ */
47
+ private evictedDuringFlush;
48
+ /** Number of items captured by the in-flight flush (0 when idle). */
49
+ private inFlightCount;
9
50
  private consecutiveFailures;
10
51
  private ticksToSkip;
11
52
  private static readonly MAX_BACKOFF_TICKS;
12
53
  constructor(maxSize: number | undefined, flushIntervalMs: number | undefined, onFlush: (items: T[]) => Promise<void>, maxBufferSize?: number);
13
54
  add(item: T): void;
55
+ /**
56
+ * Flush the buffer. If a flush is already in flight, returns its promise
57
+ * (never a silent no-op). Never rejects — failures are kept in the buffer
58
+ * and retried with backoff by the auto-flush timer.
59
+ */
14
60
  flush(): Promise<void>;
61
+ private doFlush;
62
+ private applyBackoff;
15
63
  startAutoFlush(): void;
16
64
  stopAutoFlush(): void;
17
65
  get pending(): number;
66
+ /** Read-only snapshot of the buffered items (for persistence). */
67
+ peekAll(): readonly T[];
68
+ /** Remove every buffered item (after persisting, or on session discard). */
69
+ clear(): void;
18
70
  }
@@ -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
- this.flushing = false;
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
- this.buffer.splice(0, this.buffer.length - this.maxBufferSize + 1);
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
- async flush() {
29
- if (this.buffer.length === 0 || this.flushing) {
30
- return;
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.flushing = true;
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
- // Only clear the items that were successfully flushed.
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
- // On failure, keep events in buffer so they can be retried.
46
- // Apply exponential backoff: 1, 2, 4, 8 ticks skipped before
47
- // the next auto-flush attempt.
48
- this.consecutiveFailures++;
49
- this.ticksToSkip = Math.min(Math.pow(2, this.consecutiveFailures - 1), BatchQueue.MAX_BACKOFF_TICKS);
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.flushing = false;
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;
@@ -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.3.7",
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
- "peerDependenciesMeta": {
45
- "react-native-view-shot": {
46
- "optional": true
47
- },
48
- "@react-navigation/native": {
49
- "optional": true
50
- }
51
- },
52
- "devDependencies": {
53
- "@react-navigation/native": "^6.1.0",
54
- "@types/jest": "^29.5.0",
55
- "@types/react": "^18.2.0",
56
- "@types/react-native": "^0.72.0",
57
- "jest": "^29.7.0",
58
- "react": "^18.2.0",
59
- "react-native": "^0.73.0",
60
- "react-native-view-shot": "^4.0.3",
61
- "ts-jest": "^29.1.0",
62
- "typescript": "^5.3.0"
63
- }
64
- }
1
+ {
2
+ "name": "@repliqo/sdk-react-native",
3
+ "version": "0.4.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
+ "@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
+ }