@repliqo/sdk-react-native 0.2.3 → 0.3.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/android/src/main/java/com/repliqo/screencapture/ScreenCaptureModule.java +27 -0
- package/android/src/main/java/com/repliqo/screencapture/ScrollScanCapture.java +248 -0
- package/dist/core/client.d.ts +5 -0
- package/dist/core/client.js +33 -2
- package/dist/snapshot/NativeScreenCapture.d.ts +18 -0
- package/dist/snapshot/NativeScreenCapture.js +32 -0
- package/package.json +1 -1
- package/src/core/client.ts +52 -3
- package/src/snapshot/NativeScreenCapture.ts +51 -0
|
@@ -132,4 +132,31 @@ public class ScreenCaptureModule extends ReactContextBaseJavaModule {
|
|
|
132
132
|
}
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Perform a fast programmatic scroll-scan of the main ScrollView.
|
|
138
|
+
* Captures the entire content as viewport tiles in ~300-500ms.
|
|
139
|
+
* Invisible to the user (scroll is restored to original position).
|
|
140
|
+
*
|
|
141
|
+
* Returns: Array<{ image, width, height, scrollOffsetY, viewportHeight,
|
|
142
|
+
* contentHeight, format }>
|
|
143
|
+
*/
|
|
144
|
+
@ReactMethod
|
|
145
|
+
public void scanFullContent(Promise promise) {
|
|
146
|
+
Activity activity = getCurrentActivity();
|
|
147
|
+
if (activity == null) {
|
|
148
|
+
promise.resolve(null);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
mainHandler.post(() -> {
|
|
153
|
+
try {
|
|
154
|
+
WritableArray tiles = ScrollScanCapture.scan(activity);
|
|
155
|
+
promise.resolve(tiles);
|
|
156
|
+
} catch (Exception e) {
|
|
157
|
+
Log.w(TAG, "scanFullContent failed: " + e.getMessage());
|
|
158
|
+
promise.resolve(null);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
135
162
|
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
package com.repliqo.screencapture;
|
|
2
|
+
|
|
3
|
+
import android.app.Activity;
|
|
4
|
+
import android.graphics.Bitmap;
|
|
5
|
+
import android.graphics.Canvas;
|
|
6
|
+
import android.util.Base64;
|
|
7
|
+
import android.util.Log;
|
|
8
|
+
import android.view.View;
|
|
9
|
+
import android.view.ViewGroup;
|
|
10
|
+
import android.widget.ScrollView;
|
|
11
|
+
|
|
12
|
+
import com.facebook.react.bridge.Arguments;
|
|
13
|
+
import com.facebook.react.bridge.WritableArray;
|
|
14
|
+
import com.facebook.react.bridge.WritableMap;
|
|
15
|
+
|
|
16
|
+
import java.io.ByteArrayOutputStream;
|
|
17
|
+
import java.lang.reflect.Field;
|
|
18
|
+
import java.util.ArrayList;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Performs a fast programmatic scroll-scan of the main ScrollView to
|
|
22
|
+
* capture the ENTIRE content as a series of viewport tiles.
|
|
23
|
+
*
|
|
24
|
+
* The scan is invisible to the user:
|
|
25
|
+
* 1. Save current scroll position
|
|
26
|
+
* 2. scrollTo(0, 0) — instant, no animation
|
|
27
|
+
* 3. Capture viewport at Y=0
|
|
28
|
+
* 4. scrollTo(0, viewportHeight) — instant
|
|
29
|
+
* 5. Capture viewport at Y=viewportHeight
|
|
30
|
+
* 6. ... repeat until end of content
|
|
31
|
+
* 7. scrollTo(0, originalY) — restore position
|
|
32
|
+
*
|
|
33
|
+
* Total time: ~300-500ms for a typical screen. The user sees a brief
|
|
34
|
+
* flicker at most (usually nothing because it happens within one frame).
|
|
35
|
+
*
|
|
36
|
+
* Must run on the UI thread.
|
|
37
|
+
*/
|
|
38
|
+
public class ScrollScanCapture {
|
|
39
|
+
private static final String TAG = "RepliqoCapture";
|
|
40
|
+
private static final int TARGET_WIDTH = 390;
|
|
41
|
+
private static final int JPEG_QUALITY = 40;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Scan the ScrollView and return all viewport tiles.
|
|
45
|
+
*
|
|
46
|
+
* @return WritableArray of tile objects, or null on failure
|
|
47
|
+
*/
|
|
48
|
+
public static WritableArray scan(Activity activity) {
|
|
49
|
+
if (activity == null || activity.isFinishing()) return null;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
View scrollView = findVerticalScrollView(activity);
|
|
53
|
+
if (scrollView == null) {
|
|
54
|
+
Log.d(TAG, "No ScrollView found for scan");
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
View contentView = getContentChild(scrollView);
|
|
59
|
+
if (contentView == null) return null;
|
|
60
|
+
|
|
61
|
+
int viewportWidth = scrollView.getWidth();
|
|
62
|
+
int viewportHeight = scrollView.getHeight();
|
|
63
|
+
int contentHeight = contentView.getHeight();
|
|
64
|
+
|
|
65
|
+
if (viewportWidth <= 0 || viewportHeight <= 0 || contentHeight <= 0) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// If content fits in viewport, just capture once
|
|
70
|
+
if (contentHeight <= viewportHeight) {
|
|
71
|
+
WritableArray tiles = Arguments.createArray();
|
|
72
|
+
WritableMap tile = captureSingleTile(
|
|
73
|
+
scrollView, 0, viewportWidth, viewportHeight, contentHeight
|
|
74
|
+
);
|
|
75
|
+
if (tile != null) tiles.pushMap(tile);
|
|
76
|
+
return tiles;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Save original scroll position
|
|
80
|
+
int originalScrollY = scrollView.getScrollY();
|
|
81
|
+
|
|
82
|
+
WritableArray tiles = Arguments.createArray();
|
|
83
|
+
|
|
84
|
+
// Scan from top to bottom in viewport-sized steps
|
|
85
|
+
// Use 80% overlap to avoid gaps from scroll imprecision
|
|
86
|
+
int step = (int) (viewportHeight * 0.8);
|
|
87
|
+
int y = 0;
|
|
88
|
+
|
|
89
|
+
while (y < contentHeight) {
|
|
90
|
+
// Scroll to position — instant, no animation
|
|
91
|
+
scrollView.scrollTo(0, y);
|
|
92
|
+
|
|
93
|
+
// Force a layout pass so the views at this position render
|
|
94
|
+
scrollView.invalidate();
|
|
95
|
+
contentView.invalidate();
|
|
96
|
+
|
|
97
|
+
// Capture the viewport at this position
|
|
98
|
+
WritableMap tile = captureSingleTile(
|
|
99
|
+
scrollView, y, viewportWidth, viewportHeight, contentHeight
|
|
100
|
+
);
|
|
101
|
+
if (tile != null) {
|
|
102
|
+
tiles.pushMap(tile);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
y += step;
|
|
106
|
+
|
|
107
|
+
// Last step: capture the very bottom
|
|
108
|
+
if (y >= contentHeight && y - step < contentHeight - viewportHeight) {
|
|
109
|
+
y = contentHeight - viewportHeight;
|
|
110
|
+
scrollView.scrollTo(0, y);
|
|
111
|
+
scrollView.invalidate();
|
|
112
|
+
contentView.invalidate();
|
|
113
|
+
|
|
114
|
+
WritableMap lastTile = captureSingleTile(
|
|
115
|
+
scrollView, y, viewportWidth, viewportHeight, contentHeight
|
|
116
|
+
);
|
|
117
|
+
if (lastTile != null) {
|
|
118
|
+
tiles.pushMap(lastTile);
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Restore original scroll position — instant
|
|
125
|
+
scrollView.scrollTo(0, originalScrollY);
|
|
126
|
+
|
|
127
|
+
Log.d(TAG, "Scroll scan complete: " + tiles.size() +
|
|
128
|
+
" tiles, content=" + contentHeight + "px");
|
|
129
|
+
|
|
130
|
+
return tiles;
|
|
131
|
+
|
|
132
|
+
} catch (Exception e) {
|
|
133
|
+
Log.w(TAG, "ScrollScanCapture failed: " + e.getMessage());
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private static WritableMap captureSingleTile(
|
|
139
|
+
View scrollView, int scrollY,
|
|
140
|
+
int viewportWidth, int viewportHeight, int contentHeight) {
|
|
141
|
+
try {
|
|
142
|
+
// Scale to target width
|
|
143
|
+
float scale = (float) TARGET_WIDTH / viewportWidth;
|
|
144
|
+
int outWidth = TARGET_WIDTH;
|
|
145
|
+
int outHeight = Math.round(viewportHeight * scale);
|
|
146
|
+
|
|
147
|
+
if (outWidth <= 0 || outHeight <= 0) return null;
|
|
148
|
+
|
|
149
|
+
Bitmap bitmap = Bitmap.createBitmap(outWidth, outHeight,
|
|
150
|
+
Bitmap.Config.ARGB_8888);
|
|
151
|
+
Canvas canvas = new Canvas(bitmap);
|
|
152
|
+
canvas.scale(scale, scale);
|
|
153
|
+
|
|
154
|
+
// Translate canvas to account for scroll position within the view
|
|
155
|
+
canvas.translate(0, -scrollView.getScrollY());
|
|
156
|
+
|
|
157
|
+
// Draw the scrollview's content
|
|
158
|
+
View content = getContentChild(scrollView);
|
|
159
|
+
if (content != null) {
|
|
160
|
+
content.draw(canvas);
|
|
161
|
+
} else {
|
|
162
|
+
scrollView.draw(canvas);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Compress to JPEG
|
|
166
|
+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
167
|
+
bitmap.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, baos);
|
|
168
|
+
String base64 = Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP);
|
|
169
|
+
bitmap.recycle();
|
|
170
|
+
|
|
171
|
+
WritableMap tile = Arguments.createMap();
|
|
172
|
+
tile.putString("image", base64);
|
|
173
|
+
tile.putInt("width", outWidth);
|
|
174
|
+
tile.putInt("height", outHeight);
|
|
175
|
+
tile.putInt("scrollOffsetY", scrollY);
|
|
176
|
+
tile.putInt("viewportHeight", viewportHeight);
|
|
177
|
+
tile.putInt("contentHeight", contentHeight);
|
|
178
|
+
tile.putString("format", "jpeg");
|
|
179
|
+
return tile;
|
|
180
|
+
|
|
181
|
+
} catch (Exception e) {
|
|
182
|
+
Log.w(TAG, "Tile capture at Y=" + scrollY + " failed: " + e.getMessage());
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@SuppressWarnings("unchecked")
|
|
188
|
+
private static View findVerticalScrollView(Activity activity) {
|
|
189
|
+
View decorView = activity.getWindow().getDecorView();
|
|
190
|
+
View found = findInTree(decorView);
|
|
191
|
+
if (found != null) return found;
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
Class<?> wmgClass = Class.forName("android.view.WindowManagerGlobal");
|
|
195
|
+
Object wmg = wmgClass.getMethod("getInstance").invoke(null);
|
|
196
|
+
Field viewsField = wmgClass.getDeclaredField("mViews");
|
|
197
|
+
viewsField.setAccessible(true);
|
|
198
|
+
ArrayList<View> views = (ArrayList<View>) viewsField.get(wmg);
|
|
199
|
+
if (views != null) {
|
|
200
|
+
for (View root : views) {
|
|
201
|
+
if (root == decorView) continue;
|
|
202
|
+
found = findInTree(root);
|
|
203
|
+
if (found != null) return found;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} catch (Exception e) { }
|
|
207
|
+
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private static View findInTree(View view) {
|
|
212
|
+
if (view == null) return null;
|
|
213
|
+
if (isVerticalScroll(view) && view instanceof ViewGroup
|
|
214
|
+
&& ((ViewGroup) view).getChildCount() > 0) {
|
|
215
|
+
return view;
|
|
216
|
+
}
|
|
217
|
+
if (view instanceof ViewGroup) {
|
|
218
|
+
ViewGroup group = (ViewGroup) view;
|
|
219
|
+
for (int i = 0; i < group.getChildCount(); i++) {
|
|
220
|
+
View found = findInTree(group.getChildAt(i));
|
|
221
|
+
if (found != null) return found;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private static boolean isVerticalScroll(View view) {
|
|
228
|
+
if (view instanceof ScrollView) return true;
|
|
229
|
+
Class<?> clazz = view.getClass();
|
|
230
|
+
while (clazz != null && clazz != View.class) {
|
|
231
|
+
String name = clazz.getName();
|
|
232
|
+
if (name.contains("Horizontal")) return false;
|
|
233
|
+
if (name.contains("NestedScrollView") ||
|
|
234
|
+
name.contains("ReactScrollView") ||
|
|
235
|
+
name.endsWith("ScrollView")) return true;
|
|
236
|
+
clazz = clazz.getSuperclass();
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private static View getContentChild(View scrollView) {
|
|
242
|
+
if (scrollView instanceof ViewGroup) {
|
|
243
|
+
ViewGroup group = (ViewGroup) scrollView;
|
|
244
|
+
if (group.getChildCount() > 0) return group.getChildAt(0);
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
package/dist/core/client.d.ts
CHANGED
|
@@ -40,6 +40,11 @@ export declare class AppAnalytics {
|
|
|
40
40
|
* contribute tiles until the full content is covered.
|
|
41
41
|
*/
|
|
42
42
|
captureScrollTile(scrollOffsetY: number, viewportHeight: number, contentHeight: number): void;
|
|
43
|
+
/**
|
|
44
|
+
* Programmatically scroll-scan the current ScrollView and upload all
|
|
45
|
+
* tiles to the backend. Invisible to the user (~300-500ms).
|
|
46
|
+
*/
|
|
47
|
+
private autoScanScreen;
|
|
43
48
|
onScreenEnter(screenName: string): void;
|
|
44
49
|
onScreenExit(screenName: string): void;
|
|
45
50
|
flush(): Promise<void>;
|
package/dist/core/client.js
CHANGED
|
@@ -254,14 +254,45 @@ class AppAnalytics {
|
|
|
254
254
|
}
|
|
255
255
|
})();
|
|
256
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Programmatically scroll-scan the current ScrollView and upload all
|
|
259
|
+
* tiles to the backend. Invisible to the user (~300-500ms).
|
|
260
|
+
*/
|
|
261
|
+
autoScanScreen(screenName) {
|
|
262
|
+
(async () => {
|
|
263
|
+
try {
|
|
264
|
+
const tiles = await (0, NativeScreenCapture_1.scanFullContent)();
|
|
265
|
+
if (!tiles || tiles.length === 0) {
|
|
266
|
+
this.logger.log(`Auto-scan: no ScrollView found for "${screenName}"`);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
this.logger.log(`Auto-scan: "${screenName}" captured ${tiles.length} tiles ` +
|
|
270
|
+
`(content=${tiles[0].contentHeight}px)`);
|
|
271
|
+
// Upload all tiles in parallel
|
|
272
|
+
const sessionId = this.sessionId;
|
|
273
|
+
await Promise.all(tiles.map((tile) => this.apiClient.uploadScreenTile(sessionId, screenName, tile.image, tile.width, tile.height, tile.scrollOffsetY, tile.viewportHeight, tile.contentHeight).catch(() => { })));
|
|
274
|
+
this.logger.log(`Auto-scan: "${screenName}" tiles uploaded`);
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
this.logger.warn('Auto-scan failed:', err);
|
|
278
|
+
}
|
|
279
|
+
})();
|
|
280
|
+
}
|
|
257
281
|
onScreenEnter(screenName) {
|
|
258
282
|
this.currentScreen = screenName;
|
|
259
283
|
this.currentScreenEnteredAt = new Date().toISOString();
|
|
260
284
|
this.scrollOffsetY = 0;
|
|
261
285
|
this.errorTracker?.setCurrentScreen(screenName);
|
|
262
286
|
this.snapshotCapture?.setCurrentScreen(screenName);
|
|
263
|
-
//
|
|
264
|
-
//
|
|
287
|
+
// Auto-scan: programmatically scroll the ScrollView and capture
|
|
288
|
+
// all viewport tiles in ~300ms. Fire-and-forget, invisible to user.
|
|
289
|
+
if (this.config.enableHeatmapCapture) {
|
|
290
|
+
setTimeout(() => {
|
|
291
|
+
if (this.currentScreen !== screenName || !this.sessionId)
|
|
292
|
+
return;
|
|
293
|
+
this.autoScanScreen(screenName);
|
|
294
|
+
}, 1500); // Wait for screen to fully render
|
|
295
|
+
}
|
|
265
296
|
this.logger.log('Screen entered:', screenName);
|
|
266
297
|
}
|
|
267
298
|
onScreenExit(screenName) {
|
|
@@ -41,3 +41,21 @@ export interface FullContentResult extends NativeFrameResult {
|
|
|
41
41
|
* @returns FullContentResult with isFullContent flag, or null
|
|
42
42
|
*/
|
|
43
43
|
export declare function captureFullContent(): Promise<FullContentResult | null>;
|
|
44
|
+
export interface ScanTile {
|
|
45
|
+
image: string;
|
|
46
|
+
width: number;
|
|
47
|
+
height: number;
|
|
48
|
+
scrollOffsetY: number;
|
|
49
|
+
viewportHeight: number;
|
|
50
|
+
contentHeight: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Perform a fast programmatic scroll-scan of the main ScrollView.
|
|
54
|
+
* Captures the entire content as viewport tiles in ~300-500ms.
|
|
55
|
+
*
|
|
56
|
+
* The scan is invisible to the user — scroll position is saved and
|
|
57
|
+
* restored. Returns an array of tiles covering the full content.
|
|
58
|
+
*
|
|
59
|
+
* @returns Array of tiles, or null if no ScrollView or not available
|
|
60
|
+
*/
|
|
61
|
+
export declare function scanFullContent(): Promise<ScanTile[] | null>;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.isNativeScreenCaptureAvailable = isNativeScreenCaptureAvailable;
|
|
4
4
|
exports.captureNativeFrame = captureNativeFrame;
|
|
5
5
|
exports.captureFullContent = captureFullContent;
|
|
6
|
+
exports.scanFullContent = scanFullContent;
|
|
6
7
|
const react_native_1 = require("react-native");
|
|
7
8
|
const NativeModule = react_native_1.Platform.OS === 'android'
|
|
8
9
|
? react_native_1.NativeModules.ScreenCaptureModule ?? null
|
|
@@ -68,3 +69,34 @@ async function captureFullContent() {
|
|
|
68
69
|
return null;
|
|
69
70
|
}
|
|
70
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Perform a fast programmatic scroll-scan of the main ScrollView.
|
|
74
|
+
* Captures the entire content as viewport tiles in ~300-500ms.
|
|
75
|
+
*
|
|
76
|
+
* The scan is invisible to the user — scroll position is saved and
|
|
77
|
+
* restored. Returns an array of tiles covering the full content.
|
|
78
|
+
*
|
|
79
|
+
* @returns Array of tiles, or null if no ScrollView or not available
|
|
80
|
+
*/
|
|
81
|
+
async function scanFullContent() {
|
|
82
|
+
if (!NativeModule)
|
|
83
|
+
return null;
|
|
84
|
+
try {
|
|
85
|
+
const tiles = await NativeModule.scanFullContent();
|
|
86
|
+
if (!tiles || !Array.isArray(tiles) || tiles.length === 0)
|
|
87
|
+
return null;
|
|
88
|
+
return tiles
|
|
89
|
+
.filter((t) => t && t.image)
|
|
90
|
+
.map((t) => ({
|
|
91
|
+
image: t.image,
|
|
92
|
+
width: t.width,
|
|
93
|
+
height: t.height,
|
|
94
|
+
scrollOffsetY: t.scrollOffsetY,
|
|
95
|
+
viewportHeight: t.viewportHeight,
|
|
96
|
+
contentHeight: t.contentHeight,
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@repliqo/sdk-react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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
|
@@ -12,7 +12,7 @@ import { Logger } from './logger';
|
|
|
12
12
|
import { ApiClient } from '../transport/api.client';
|
|
13
13
|
import { BatchQueue } from '../transport/batch-queue';
|
|
14
14
|
import { ErrorTracker } from '../trackers/error.tracker';
|
|
15
|
-
import { captureNativeFrame } from '../snapshot/NativeScreenCapture';
|
|
15
|
+
import { captureNativeFrame, scanFullContent } from '../snapshot/NativeScreenCapture';
|
|
16
16
|
|
|
17
17
|
export class AppAnalytics {
|
|
18
18
|
private static instance: AppAnalytics | null = null;
|
|
@@ -368,14 +368,63 @@ export class AppAnalytics {
|
|
|
368
368
|
})();
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Programmatically scroll-scan the current ScrollView and upload all
|
|
373
|
+
* tiles to the backend. Invisible to the user (~300-500ms).
|
|
374
|
+
*/
|
|
375
|
+
private autoScanScreen(screenName: string): void {
|
|
376
|
+
(async () => {
|
|
377
|
+
try {
|
|
378
|
+
const tiles = await scanFullContent();
|
|
379
|
+
if (!tiles || tiles.length === 0) {
|
|
380
|
+
this.logger.log(`Auto-scan: no ScrollView found for "${screenName}"`);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
this.logger.log(
|
|
385
|
+
`Auto-scan: "${screenName}" captured ${tiles.length} tiles ` +
|
|
386
|
+
`(content=${tiles[0].contentHeight}px)`,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
// Upload all tiles in parallel
|
|
390
|
+
const sessionId = this.sessionId!;
|
|
391
|
+
await Promise.all(
|
|
392
|
+
tiles.map((tile) =>
|
|
393
|
+
this.apiClient.uploadScreenTile(
|
|
394
|
+
sessionId,
|
|
395
|
+
screenName,
|
|
396
|
+
tile.image,
|
|
397
|
+
tile.width,
|
|
398
|
+
tile.height,
|
|
399
|
+
tile.scrollOffsetY,
|
|
400
|
+
tile.viewportHeight,
|
|
401
|
+
tile.contentHeight,
|
|
402
|
+
).catch(() => {}),
|
|
403
|
+
),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
this.logger.log(`Auto-scan: "${screenName}" tiles uploaded`);
|
|
407
|
+
} catch (err) {
|
|
408
|
+
this.logger.warn('Auto-scan failed:', err);
|
|
409
|
+
}
|
|
410
|
+
})();
|
|
411
|
+
}
|
|
412
|
+
|
|
371
413
|
onScreenEnter(screenName: string): void {
|
|
372
414
|
this.currentScreen = screenName;
|
|
373
415
|
this.currentScreenEnteredAt = new Date().toISOString();
|
|
374
416
|
this.scrollOffsetY = 0;
|
|
375
417
|
this.errorTracker?.setCurrentScreen(screenName);
|
|
376
418
|
this.snapshotCapture?.setCurrentScreen(screenName);
|
|
377
|
-
//
|
|
378
|
-
//
|
|
419
|
+
// Auto-scan: programmatically scroll the ScrollView and capture
|
|
420
|
+
// all viewport tiles in ~300ms. Fire-and-forget, invisible to user.
|
|
421
|
+
if (this.config.enableHeatmapCapture) {
|
|
422
|
+
setTimeout(() => {
|
|
423
|
+
if (this.currentScreen !== screenName || !this.sessionId) return;
|
|
424
|
+
this.autoScanScreen(screenName);
|
|
425
|
+
}, 1500); // Wait for screen to fully render
|
|
426
|
+
}
|
|
427
|
+
|
|
379
428
|
this.logger.log('Screen entered:', screenName);
|
|
380
429
|
}
|
|
381
430
|
|
|
@@ -18,9 +18,20 @@ interface NativeFullContentResult extends NativeCaptureResult {
|
|
|
18
18
|
isFullContent: boolean;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
interface NativeTile {
|
|
22
|
+
image: string;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
scrollOffsetY: number;
|
|
26
|
+
viewportHeight: number;
|
|
27
|
+
contentHeight: number;
|
|
28
|
+
format: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
interface NativeScreenCaptureModule {
|
|
22
32
|
captureFrame(): Promise<NativeCaptureResult | null>;
|
|
23
33
|
captureFullContent(): Promise<NativeFullContentResult | null>;
|
|
34
|
+
scanFullContent(): Promise<NativeTile[] | null>;
|
|
24
35
|
isAvailable(): Promise<boolean>;
|
|
25
36
|
}
|
|
26
37
|
|
|
@@ -104,3 +115,43 @@ export async function captureFullContent(): Promise<FullContentResult | null> {
|
|
|
104
115
|
return null;
|
|
105
116
|
}
|
|
106
117
|
}
|
|
118
|
+
|
|
119
|
+
export interface ScanTile {
|
|
120
|
+
image: string;
|
|
121
|
+
width: number;
|
|
122
|
+
height: number;
|
|
123
|
+
scrollOffsetY: number;
|
|
124
|
+
viewportHeight: number;
|
|
125
|
+
contentHeight: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Perform a fast programmatic scroll-scan of the main ScrollView.
|
|
130
|
+
* Captures the entire content as viewport tiles in ~300-500ms.
|
|
131
|
+
*
|
|
132
|
+
* The scan is invisible to the user — scroll position is saved and
|
|
133
|
+
* restored. Returns an array of tiles covering the full content.
|
|
134
|
+
*
|
|
135
|
+
* @returns Array of tiles, or null if no ScrollView or not available
|
|
136
|
+
*/
|
|
137
|
+
export async function scanFullContent(): Promise<ScanTile[] | null> {
|
|
138
|
+
if (!NativeModule) return null;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const tiles = await NativeModule.scanFullContent();
|
|
142
|
+
if (!tiles || !Array.isArray(tiles) || tiles.length === 0) return null;
|
|
143
|
+
|
|
144
|
+
return tiles
|
|
145
|
+
.filter((t) => t && t.image)
|
|
146
|
+
.map((t) => ({
|
|
147
|
+
image: t.image,
|
|
148
|
+
width: t.width,
|
|
149
|
+
height: t.height,
|
|
150
|
+
scrollOffsetY: t.scrollOffsetY,
|
|
151
|
+
viewportHeight: t.viewportHeight,
|
|
152
|
+
contentHeight: t.contentHeight,
|
|
153
|
+
}));
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|