@repliqo/sdk-react-native 0.2.0 → 0.2.2
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/android/src/main/java/com/repliqo/screencapture/FullContentCapture.java +15 -0
- package/dist/core/client.js +2 -1
- package/dist/core/config.js +1 -0
- package/dist/snapshot/ScreenCaptureManager.d.ts +7 -8
- package/dist/snapshot/ScreenCaptureManager.js +43 -14
- package/dist/types/events.d.ts +2 -0
- package/package.json +1 -1
- package/src/core/client.ts +2 -1
- package/src/core/config.ts +1 -0
- package/src/snapshot/ScreenCaptureManager.ts +56 -16
- package/src/types/events.ts +2 -0
|
@@ -103,6 +103,17 @@ public class FullContentCapture {
|
|
|
103
103
|
return viewportFallback(activity);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// Force the content view to measure and layout at its full
|
|
107
|
+
// height. React Native's ScrollView optimization may skip
|
|
108
|
+
// laying out off-screen children, causing draw() to only
|
|
109
|
+
// render the visible portion. This forces a full layout pass.
|
|
110
|
+
int widthSpec = View.MeasureSpec.makeMeasureSpec(
|
|
111
|
+
contentWidth, View.MeasureSpec.EXACTLY);
|
|
112
|
+
int heightSpec = View.MeasureSpec.makeMeasureSpec(
|
|
113
|
+
captureHeight, View.MeasureSpec.EXACTLY);
|
|
114
|
+
contentView.measure(widthSpec, heightSpec);
|
|
115
|
+
contentView.layout(0, 0, contentWidth, captureHeight);
|
|
116
|
+
|
|
106
117
|
// Create bitmap — wrapped in OOM catch
|
|
107
118
|
Bitmap bitmap;
|
|
108
119
|
try {
|
|
@@ -120,6 +131,10 @@ public class FullContentCapture {
|
|
|
120
131
|
// Draw the full content
|
|
121
132
|
contentView.draw(canvas);
|
|
122
133
|
|
|
134
|
+
// Restore the original layout so the UI isn't affected.
|
|
135
|
+
// requestLayout triggers an async re-layout on the next frame.
|
|
136
|
+
contentView.requestLayout();
|
|
137
|
+
|
|
123
138
|
// Compress to JPEG
|
|
124
139
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
125
140
|
bitmap.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, baos);
|
package/dist/core/client.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/dist/core/config.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
`(
|
|
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
|
-
|
|
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;
|
package/dist/types/events.d.ts
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.2.2",
|
|
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",
|
package/src/core/client.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
package/src/core/config.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
`(
|
|
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
|
-
|
|
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
|
}
|
package/src/types/events.ts
CHANGED
|
@@ -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
|
}
|