@repliqo/sdk-react-native 0.2.0 → 0.2.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.
@@ -54,7 +54,8 @@ class AppAnalytics {
54
54
  }
55
55
  // Screen capture manager for heatmap backgrounds.
56
56
  // Captures full ScrollView content on screen enter (24h cache).
57
- if (this.config.enableSnapshots) {
57
+ // Independent of session replay snapshots.
58
+ if (this.config.enableHeatmapCapture) {
58
59
  this.screenCaptureManager = new ScreenCaptureManager_1.ScreenCaptureManager((screenName, image, width, height, isFullContent) => this.apiClient.uploadScreenCapture(screenName, image, width, height, isFullContent), this.config.debug);
59
60
  }
60
61
  if (this.config.enableCrashTracking) {
@@ -19,6 +19,7 @@ exports.DEFAULT_CONFIG = {
19
19
  snapshotFlushInterval: 15000,
20
20
  // ~32 * 30KB ≈ 1 MB cap on in-memory buffer if the backend is down.
21
21
  snapshotMaxBufferSize: 32,
22
+ enableHeatmapCapture: true,
22
23
  enableCrashTracking: true,
23
24
  debug: false,
24
25
  };
@@ -1,15 +1,17 @@
1
- type UploadFn = (screenName: string, image: string, width: number, height: number, isFullContent: boolean) => Promise<void>;
1
+ export type UploadFn = (screenName: string, image: string, width: number, height: number, isFullContent: boolean) => Promise<void>;
2
2
  /**
3
3
  * Manages full-content screen captures for heatmap backgrounds.
4
4
  *
5
5
  * - Captures the full ScrollView content on screen enter
6
6
  * - Local cache with 24h TTL to avoid redundant captures
7
+ * - Cancels pending captures when navigating away (prevents cross-screen bugs)
7
8
  * - Async and non-blocking: never delays navigation or crashes the app
8
- * - Independent of the session replay snapshot system
9
9
  */
10
10
  export declare class ScreenCaptureManager {
11
11
  private cache;
12
12
  private pending;
13
+ private pendingTimer;
14
+ private currentScreen;
13
15
  private uploadFn;
14
16
  private debug;
15
17
  constructor(uploadFn: UploadFn, debug?: boolean);
@@ -17,15 +19,12 @@ export declare class ScreenCaptureManager {
17
19
  * Called when the user enters a screen. Schedules a full-content
18
20
  * capture after a delay if no recent capture exists.
19
21
  *
20
- * Fire-and-forget: never awaited by the caller, never throws.
22
+ * Cancels any pending capture from a previous screen to prevent
23
+ * cross-screen data corruption (capturing screen B but labeling as A).
21
24
  */
22
25
  onScreenEnter(screenName: string): void;
23
- /**
24
- * Check if a capture is needed for this screen.
25
- */
26
26
  private shouldCapture;
27
27
  private captureAndUpload;
28
- /** Reset the cache (e.g. on session end). */
28
+ private cancelPending;
29
29
  reset(): void;
30
30
  }
31
- export {};
@@ -6,18 +6,22 @@ const NativeScreenCapture_1 = require("./NativeScreenCapture");
6
6
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
7
7
  /** Wait for screen to render before capturing. */
8
8
  const RENDER_DELAY_MS = 800;
9
+ /** Max base64 size to upload (3MB base64 ≈ ~2.2MB binary). */
10
+ const MAX_IMAGE_SIZE = 3 * 1024 * 1024;
9
11
  /**
10
12
  * Manages full-content screen captures for heatmap backgrounds.
11
13
  *
12
14
  * - Captures the full ScrollView content on screen enter
13
15
  * - Local cache with 24h TTL to avoid redundant captures
16
+ * - Cancels pending captures when navigating away (prevents cross-screen bugs)
14
17
  * - Async and non-blocking: never delays navigation or crashes the app
15
- * - Independent of the session replay snapshot system
16
18
  */
17
19
  class ScreenCaptureManager {
18
20
  constructor(uploadFn, debug = false) {
19
21
  this.cache = new Map();
20
22
  this.pending = new Set();
23
+ this.pendingTimer = null;
24
+ this.currentScreen = null;
21
25
  this.uploadFn = uploadFn;
22
26
  this.debug = debug;
23
27
  }
@@ -25,26 +29,32 @@ class ScreenCaptureManager {
25
29
  * Called when the user enters a screen. Schedules a full-content
26
30
  * capture after a delay if no recent capture exists.
27
31
  *
28
- * Fire-and-forget: never awaited by the caller, never throws.
32
+ * Cancels any pending capture from a previous screen to prevent
33
+ * cross-screen data corruption (capturing screen B but labeling as A).
29
34
  */
30
35
  onScreenEnter(screenName) {
36
+ // Cancel any pending capture from the previous screen
37
+ this.cancelPending();
38
+ this.currentScreen = screenName;
31
39
  if (!this.shouldCapture(screenName))
32
40
  return;
33
- // Mark as pending to avoid duplicate captures from rapid navigation
34
41
  this.pending.add(screenName);
35
- // Delay to let the screen render completely (animations, data loading)
36
- setTimeout(() => {
42
+ this.pendingTimer = setTimeout(() => {
43
+ this.pendingTimer = null;
44
+ // Verify the user is STILL on this screen before capturing
45
+ if (this.currentScreen !== screenName) {
46
+ this.pending.delete(screenName);
47
+ if (this.debug) {
48
+ console.log(`[Repliqo] Screen capture skipped: navigated away from "${screenName}"`);
49
+ }
50
+ return;
51
+ }
37
52
  this.captureAndUpload(screenName).catch(() => { });
38
53
  }, RENDER_DELAY_MS);
39
54
  }
40
- /**
41
- * Check if a capture is needed for this screen.
42
- */
43
55
  shouldCapture(screenName) {
44
- // Already capturing this screen
45
56
  if (this.pending.has(screenName))
46
57
  return false;
47
- // Check local cache TTL
48
58
  const lastCapture = this.cache.get(screenName);
49
59
  if (lastCapture && Date.now() - lastCapture < CACHE_TTL_MS) {
50
60
  return false;
@@ -60,13 +70,25 @@ class ScreenCaptureManager {
60
70
  }
61
71
  return;
62
72
  }
73
+ // Guard: check we're still on the same screen after the async capture
74
+ if (this.currentScreen !== screenName) {
75
+ if (this.debug) {
76
+ console.log(`[Repliqo] Screen capture discarded: user left "${screenName}" during capture`);
77
+ }
78
+ return;
79
+ }
80
+ // Guard: reject oversized images to prevent OOM during upload
81
+ if (result.image.length > MAX_IMAGE_SIZE) {
82
+ if (this.debug) {
83
+ console.warn(`[Repliqo] Screen capture too large (${Math.round(result.image.length / 1024)}KB), skipping "${screenName}"`);
84
+ }
85
+ return;
86
+ }
63
87
  if (this.debug) {
64
88
  console.log(`[Repliqo] Screen captured: "${screenName}" ${result.width}x${result.height} ` +
65
- `(fullContent=${result.isFullContent}, ${Math.round(result.image.length / 1024)}KB)`);
89
+ `(full=${result.isFullContent}, ${Math.round(result.image.length / 1024)}KB)`);
66
90
  }
67
- // Upload to backend (fire-and-forget, errors are swallowed)
68
91
  await this.uploadFn(screenName, result.image, result.width, result.height, result.isFullContent);
69
- // Update local cache
70
92
  this.cache.set(screenName, Date.now());
71
93
  if (this.debug) {
72
94
  console.log(`[Repliqo] Screen capture uploaded: "${screenName}"`);
@@ -81,10 +103,17 @@ class ScreenCaptureManager {
81
103
  this.pending.delete(screenName);
82
104
  }
83
105
  }
84
- /** Reset the cache (e.g. on session end). */
106
+ cancelPending() {
107
+ if (this.pendingTimer !== null) {
108
+ clearTimeout(this.pendingTimer);
109
+ this.pendingTimer = null;
110
+ }
111
+ }
85
112
  reset() {
113
+ this.cancelPending();
86
114
  this.cache.clear();
87
115
  this.pending.clear();
116
+ this.currentScreen = null;
88
117
  }
89
118
  }
90
119
  exports.ScreenCaptureManager = ScreenCaptureManager;
@@ -47,6 +47,8 @@ export interface SDKConfig {
47
47
  snapshotFlushInterval?: number;
48
48
  /** Max snapshots kept in memory if the backend is unreachable. Defaults to 32. */
49
49
  snapshotMaxBufferSize?: number;
50
+ /** Enable full-content screen captures for heatmap backgrounds. Defaults to true when enableSnapshots is true. */
51
+ enableHeatmapCapture?: boolean;
50
52
  enableCrashTracking?: boolean;
51
53
  debug?: boolean;
52
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@repliqo/sdk-react-native",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Session replay & analytics SDK for React Native. Captures screenshots (including modals/alerts), navigation, screen visits, and custom events.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -98,7 +98,8 @@ export class AppAnalytics {
98
98
 
99
99
  // Screen capture manager for heatmap backgrounds.
100
100
  // Captures full ScrollView content on screen enter (24h cache).
101
- if (this.config.enableSnapshots) {
101
+ // Independent of session replay snapshots.
102
+ if (this.config.enableHeatmapCapture) {
102
103
  this.screenCaptureManager = new ScreenCaptureManager(
103
104
  (screenName, image, width, height, isFullContent) =>
104
105
  this.apiClient.uploadScreenCapture(
@@ -18,6 +18,7 @@ export const DEFAULT_CONFIG: Required<Omit<SDKConfig, 'apiKey' | 'appId'>> = {
18
18
  snapshotFlushInterval: 15000,
19
19
  // ~32 * 30KB ≈ 1 MB cap on in-memory buffer if the backend is down.
20
20
  snapshotMaxBufferSize: 32,
21
+ enableHeatmapCapture: true,
21
22
  enableCrashTracking: true,
22
23
  debug: false,
23
24
  };
@@ -1,4 +1,4 @@
1
- import { captureFullContent, type FullContentResult } from './NativeScreenCapture';
1
+ import { captureFullContent } from './NativeScreenCapture';
2
2
 
3
3
  /** 24 hours in milliseconds. */
4
4
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
@@ -6,7 +6,10 @@ const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
6
6
  /** Wait for screen to render before capturing. */
7
7
  const RENDER_DELAY_MS = 800;
8
8
 
9
- type UploadFn = (
9
+ /** Max base64 size to upload (3MB base64 ≈ ~2.2MB binary). */
10
+ const MAX_IMAGE_SIZE = 3 * 1024 * 1024;
11
+
12
+ export type UploadFn = (
10
13
  screenName: string,
11
14
  image: string,
12
15
  width: number,
@@ -19,12 +22,14 @@ type UploadFn = (
19
22
  *
20
23
  * - Captures the full ScrollView content on screen enter
21
24
  * - Local cache with 24h TTL to avoid redundant captures
25
+ * - Cancels pending captures when navigating away (prevents cross-screen bugs)
22
26
  * - Async and non-blocking: never delays navigation or crashes the app
23
- * - Independent of the session replay snapshot system
24
27
  */
25
28
  export class ScreenCaptureManager {
26
29
  private cache = new Map<string, number>();
27
30
  private pending = new Set<string>();
31
+ private pendingTimer: ReturnType<typeof setTimeout> | null = null;
32
+ private currentScreen: string | null = null;
28
33
  private uploadFn: UploadFn;
29
34
  private debug: boolean;
30
35
 
@@ -37,28 +42,37 @@ export class ScreenCaptureManager {
37
42
  * Called when the user enters a screen. Schedules a full-content
38
43
  * capture after a delay if no recent capture exists.
39
44
  *
40
- * Fire-and-forget: never awaited by the caller, never throws.
45
+ * Cancels any pending capture from a previous screen to prevent
46
+ * cross-screen data corruption (capturing screen B but labeling as A).
41
47
  */
42
48
  onScreenEnter(screenName: string): void {
49
+ // Cancel any pending capture from the previous screen
50
+ this.cancelPending();
51
+ this.currentScreen = screenName;
52
+
43
53
  if (!this.shouldCapture(screenName)) return;
44
54
 
45
- // Mark as pending to avoid duplicate captures from rapid navigation
46
55
  this.pending.add(screenName);
47
56
 
48
- // Delay to let the screen render completely (animations, data loading)
49
- setTimeout(() => {
57
+ this.pendingTimer = setTimeout(() => {
58
+ this.pendingTimer = null;
59
+ // Verify the user is STILL on this screen before capturing
60
+ if (this.currentScreen !== screenName) {
61
+ this.pending.delete(screenName);
62
+ if (this.debug) {
63
+ console.log(
64
+ `[Repliqo] Screen capture skipped: navigated away from "${screenName}"`,
65
+ );
66
+ }
67
+ return;
68
+ }
50
69
  this.captureAndUpload(screenName).catch(() => {});
51
70
  }, RENDER_DELAY_MS);
52
71
  }
53
72
 
54
- /**
55
- * Check if a capture is needed for this screen.
56
- */
57
73
  private shouldCapture(screenName: string): boolean {
58
- // Already capturing this screen
59
74
  if (this.pending.has(screenName)) return false;
60
75
 
61
- // Check local cache TTL
62
76
  const lastCapture = this.cache.get(screenName);
63
77
  if (lastCapture && Date.now() - lastCapture < CACHE_TTL_MS) {
64
78
  return false;
@@ -77,14 +91,33 @@ export class ScreenCaptureManager {
77
91
  return;
78
92
  }
79
93
 
94
+ // Guard: check we're still on the same screen after the async capture
95
+ if (this.currentScreen !== screenName) {
96
+ if (this.debug) {
97
+ console.log(
98
+ `[Repliqo] Screen capture discarded: user left "${screenName}" during capture`,
99
+ );
100
+ }
101
+ return;
102
+ }
103
+
104
+ // Guard: reject oversized images to prevent OOM during upload
105
+ if (result.image.length > MAX_IMAGE_SIZE) {
106
+ if (this.debug) {
107
+ console.warn(
108
+ `[Repliqo] Screen capture too large (${Math.round(result.image.length / 1024)}KB), skipping "${screenName}"`,
109
+ );
110
+ }
111
+ return;
112
+ }
113
+
80
114
  if (this.debug) {
81
115
  console.log(
82
116
  `[Repliqo] Screen captured: "${screenName}" ${result.width}x${result.height} ` +
83
- `(fullContent=${result.isFullContent}, ${Math.round(result.image.length / 1024)}KB)`,
117
+ `(full=${result.isFullContent}, ${Math.round(result.image.length / 1024)}KB)`,
84
118
  );
85
119
  }
86
120
 
87
- // Upload to backend (fire-and-forget, errors are swallowed)
88
121
  await this.uploadFn(
89
122
  screenName,
90
123
  result.image,
@@ -93,7 +126,6 @@ export class ScreenCaptureManager {
93
126
  result.isFullContent,
94
127
  );
95
128
 
96
- // Update local cache
97
129
  this.cache.set(screenName, Date.now());
98
130
 
99
131
  if (this.debug) {
@@ -108,9 +140,17 @@ export class ScreenCaptureManager {
108
140
  }
109
141
  }
110
142
 
111
- /** Reset the cache (e.g. on session end). */
143
+ private cancelPending(): void {
144
+ if (this.pendingTimer !== null) {
145
+ clearTimeout(this.pendingTimer);
146
+ this.pendingTimer = null;
147
+ }
148
+ }
149
+
112
150
  reset(): void {
151
+ this.cancelPending();
113
152
  this.cache.clear();
114
153
  this.pending.clear();
154
+ this.currentScreen = null;
115
155
  }
116
156
  }
@@ -53,6 +53,8 @@ export interface SDKConfig {
53
53
  snapshotFlushInterval?: number;
54
54
  /** Max snapshots kept in memory if the backend is unreachable. Defaults to 32. */
55
55
  snapshotMaxBufferSize?: number;
56
+ /** Enable full-content screen captures for heatmap backgrounds. Defaults to true when enableSnapshots is true. */
57
+ enableHeatmapCapture?: boolean;
56
58
  enableCrashTracking?: boolean;
57
59
  debug?: boolean;
58
60
  }