@repliqo/sdk-react-native 0.3.7 → 0.3.8
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/MultiWindowCapture.java +39 -27
- package/android/src/main/java/com/repliqo/screencapture/ScreenCaptureModule.java +0 -76
- package/dist/core/client.d.ts +0 -5
- package/dist/core/client.js +6 -32
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/snapshot/NativeScreenCapture.d.ts +6 -44
- package/dist/snapshot/NativeScreenCapture.js +6 -64
- package/package.json +1 -1
- package/src/core/client.ts +7 -51
- package/src/index.ts +0 -2
- package/src/snapshot/NativeScreenCapture.ts +6 -101
- package/android/src/main/java/com/repliqo/screencapture/FullContentCapture.java +0 -273
- package/android/src/main/java/com/repliqo/screencapture/PixelCopyScan.java +0 -279
- package/android/src/main/java/com/repliqo/screencapture/ScrollScanCapture.java +0 -248
- package/src/snapshot/ScreenCaptureManager.ts +0 -156
|
@@ -57,6 +57,10 @@ public class MultiWindowCapture {
|
|
|
57
57
|
int screenHeight = decorView.getHeight();
|
|
58
58
|
|
|
59
59
|
if (screenWidth <= 0 || screenHeight <= 0) {
|
|
60
|
+
// Happens during layout transitions / before first layout.
|
|
61
|
+
// Log so a persistent capture failure is diagnosable.
|
|
62
|
+
Log.w(TAG, "capture skipped: invalid decor dimensions "
|
|
63
|
+
+ screenWidth + "x" + screenHeight);
|
|
60
64
|
return null;
|
|
61
65
|
}
|
|
62
66
|
|
|
@@ -77,38 +81,41 @@ public class MultiWindowCapture {
|
|
|
77
81
|
return captureDecorView(decorView, outWidth, outHeight, scale);
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
// Create the composite bitmap
|
|
84
|
+
// Create the composite bitmap. Recycled in finally so it never
|
|
85
|
+
// leaks if compression (bitmapToBase64) or anything else throws.
|
|
81
86
|
Bitmap composite = Bitmap.createBitmap(outWidth, outHeight,
|
|
82
87
|
Bitmap.Config.ARGB_8888);
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
try {
|
|
89
|
+
Canvas canvas = new Canvas(composite);
|
|
90
|
+
canvas.scale(scale, scale);
|
|
85
91
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
// Draw each visible root view in order (bottom to top)
|
|
93
|
+
for (ViewInfo info : views) {
|
|
94
|
+
if (info.view.getVisibility() != View.VISIBLE) continue;
|
|
95
|
+
if (info.view.getWidth() <= 0 || info.view.getHeight() <= 0) continue;
|
|
90
96
|
|
|
91
|
-
|
|
97
|
+
canvas.save();
|
|
92
98
|
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
// Apply the window's position offset
|
|
100
|
+
canvas.translate(info.left, info.top);
|
|
95
101
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
try {
|
|
103
|
+
info.view.draw(canvas);
|
|
104
|
+
} catch (Exception e) {
|
|
105
|
+
Log.w(TAG, "Failed to draw view: " + e.getMessage());
|
|
106
|
+
}
|
|
101
107
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// Compress to JPEG
|
|
106
|
-
String base64 = bitmapToBase64(composite);
|
|
107
|
-
composite.recycle();
|
|
108
|
+
canvas.restore();
|
|
109
|
+
}
|
|
108
110
|
|
|
109
|
-
|
|
111
|
+
// Compress to JPEG
|
|
112
|
+
String base64 = bitmapToBase64(composite);
|
|
113
|
+
if (base64 == null) return null;
|
|
110
114
|
|
|
111
|
-
|
|
115
|
+
return new CaptureResult(base64, outWidth, outHeight);
|
|
116
|
+
} finally {
|
|
117
|
+
composite.recycle();
|
|
118
|
+
}
|
|
112
119
|
|
|
113
120
|
} catch (Exception e) {
|
|
114
121
|
Log.w(TAG, "MultiWindowCapture failed: " + e.getMessage());
|
|
@@ -121,21 +128,23 @@ public class MultiWindowCapture {
|
|
|
121
128
|
*/
|
|
122
129
|
private static CaptureResult captureDecorView(
|
|
123
130
|
View decorView, int outWidth, int outHeight, float scale) {
|
|
131
|
+
Bitmap bitmap = null;
|
|
124
132
|
try {
|
|
125
|
-
|
|
133
|
+
bitmap = Bitmap.createBitmap(outWidth, outHeight,
|
|
126
134
|
Bitmap.Config.ARGB_8888);
|
|
127
135
|
Canvas canvas = new Canvas(bitmap);
|
|
128
136
|
canvas.scale(scale, scale);
|
|
129
137
|
decorView.draw(canvas);
|
|
130
138
|
|
|
131
139
|
String base64 = bitmapToBase64(bitmap);
|
|
132
|
-
bitmap.recycle();
|
|
133
|
-
|
|
134
140
|
if (base64 == null) return null;
|
|
135
141
|
return new CaptureResult(base64, outWidth, outHeight);
|
|
136
142
|
} catch (Exception e) {
|
|
137
143
|
Log.w(TAG, "DecorView capture failed: " + e.getMessage());
|
|
138
144
|
return null;
|
|
145
|
+
} finally {
|
|
146
|
+
// Always recycle, even if bitmapToBase64 / draw threw.
|
|
147
|
+
if (bitmap != null) bitmap.recycle();
|
|
139
148
|
}
|
|
140
149
|
}
|
|
141
150
|
|
|
@@ -197,7 +206,10 @@ public class MultiWindowCapture {
|
|
|
197
206
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
198
207
|
bitmap.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, baos);
|
|
199
208
|
return Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP);
|
|
200
|
-
} catch (
|
|
209
|
+
} catch (Throwable e) {
|
|
210
|
+
// Catch Throwable so OutOfMemoryError (not an Exception) is also
|
|
211
|
+
// surfaced rather than silently producing a null capture.
|
|
212
|
+
Log.w(TAG, "JPEG compression failed: " + e.getMessage());
|
|
201
213
|
return null;
|
|
202
214
|
}
|
|
203
215
|
}
|
|
@@ -12,7 +12,6 @@ import com.facebook.react.bridge.Promise;
|
|
|
12
12
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
13
13
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
14
14
|
import com.facebook.react.bridge.ReactMethod;
|
|
15
|
-
import com.facebook.react.bridge.WritableArray;
|
|
16
15
|
import com.facebook.react.bridge.WritableMap;
|
|
17
16
|
|
|
18
17
|
/**
|
|
@@ -92,79 +91,4 @@ public class ScreenCaptureModule extends ReactContextBaseJavaModule {
|
|
|
92
91
|
}
|
|
93
92
|
});
|
|
94
93
|
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Capture the FULL content of the primary ScrollView on screen,
|
|
98
|
-
* including content that is scrolled off-screen.
|
|
99
|
-
*
|
|
100
|
-
* Used for heatmap backgrounds. Falls back to a viewport capture
|
|
101
|
-
* if no ScrollView is found.
|
|
102
|
-
*
|
|
103
|
-
* Returns: { image: base64, width: int, height: int, format: "jpeg" }
|
|
104
|
-
*/
|
|
105
|
-
@ReactMethod
|
|
106
|
-
public void captureFullContent(Promise promise) {
|
|
107
|
-
Activity activity = getCurrentActivity();
|
|
108
|
-
if (activity == null) {
|
|
109
|
-
promise.resolve(null);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
mainHandler.post(() -> {
|
|
114
|
-
try {
|
|
115
|
-
FullContentCapture.FullCaptureResult result =
|
|
116
|
-
FullContentCapture.capture(activity);
|
|
117
|
-
|
|
118
|
-
if (result == null) {
|
|
119
|
-
promise.resolve(null);
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
WritableMap map = Arguments.createMap();
|
|
124
|
-
map.putString("image", result.image);
|
|
125
|
-
map.putInt("width", result.width);
|
|
126
|
-
map.putInt("height", result.height);
|
|
127
|
-
map.putString("format", "jpeg");
|
|
128
|
-
map.putBoolean("isFullContent", result.isFullContent);
|
|
129
|
-
promise.resolve(map);
|
|
130
|
-
} catch (Exception e) {
|
|
131
|
-
Log.w(TAG, "captureFullContent failed: " + e.getMessage());
|
|
132
|
-
promise.resolve(null);
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Perform a fast programmatic scroll-scan of the main ScrollView.
|
|
139
|
-
* Captures the entire content as viewport tiles in ~300-500ms.
|
|
140
|
-
* Invisible to the user (scroll is restored to original position).
|
|
141
|
-
*
|
|
142
|
-
* Returns: Array<{ image, width, height, scrollOffsetY, viewportHeight,
|
|
143
|
-
* contentHeight, format }>
|
|
144
|
-
*/
|
|
145
|
-
@ReactMethod
|
|
146
|
-
public void scanFullContent(Promise promise) {
|
|
147
|
-
Activity activity = getCurrentActivity();
|
|
148
|
-
if (activity == null) {
|
|
149
|
-
promise.resolve(null);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
mainHandler.post(() -> {
|
|
154
|
-
try {
|
|
155
|
-
// Try PixelCopy (API 26+, captures from GPU framebuffer)
|
|
156
|
-
WritableArray tiles = PixelCopyScan.scan(activity);
|
|
157
|
-
|
|
158
|
-
// Fallback to View.draw scan
|
|
159
|
-
if (tiles == null || tiles.size() == 0) {
|
|
160
|
-
tiles = ScrollScanCapture.scan(activity);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
promise.resolve(tiles);
|
|
164
|
-
} catch (Exception e) {
|
|
165
|
-
Log.w(TAG, "scanFullContent failed: " + e.getMessage());
|
|
166
|
-
promise.resolve(null);
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
94
|
}
|
package/dist/core/client.d.ts
CHANGED
|
@@ -95,11 +95,6 @@ export declare class AppAnalytics {
|
|
|
95
95
|
* scale computation during backend cropping.
|
|
96
96
|
*/
|
|
97
97
|
captureScrollTile(scrollOffsetY: number, viewportHeight: number, contentHeight: number, viewportTop?: number, windowHeight?: number): void;
|
|
98
|
-
/**
|
|
99
|
-
* Programmatically scroll-scan the current ScrollView and upload all
|
|
100
|
-
* tiles to the backend. Invisible to the user (~300-500ms).
|
|
101
|
-
*/
|
|
102
|
-
private autoScanScreen;
|
|
103
98
|
onScreenEnter(screenName: string): void;
|
|
104
99
|
onScreenExit(screenName: string): void;
|
|
105
100
|
flush(): Promise<void>;
|
package/dist/core/client.js
CHANGED
|
@@ -364,30 +364,6 @@ class AppAnalytics {
|
|
|
364
364
|
}
|
|
365
365
|
})();
|
|
366
366
|
}
|
|
367
|
-
/**
|
|
368
|
-
* Programmatically scroll-scan the current ScrollView and upload all
|
|
369
|
-
* tiles to the backend. Invisible to the user (~300-500ms).
|
|
370
|
-
*/
|
|
371
|
-
autoScanScreen(screenName) {
|
|
372
|
-
(async () => {
|
|
373
|
-
try {
|
|
374
|
-
const tiles = await (0, NativeScreenCapture_1.scanFullContent)();
|
|
375
|
-
if (!tiles || tiles.length === 0) {
|
|
376
|
-
this.logger.log(`Auto-scan: no ScrollView found for "${screenName}"`);
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
this.logger.log(`Auto-scan: "${screenName}" captured ${tiles.length} tiles ` +
|
|
380
|
-
`(content=${tiles[0].contentHeight}px)`);
|
|
381
|
-
// Upload all tiles in parallel
|
|
382
|
-
const sessionId = this.sessionId;
|
|
383
|
-
await Promise.all(tiles.map((tile) => this.apiClient.uploadScreenTile(sessionId, screenName, tile.image, tile.width, tile.height, tile.scrollOffsetY, tile.viewportHeight, tile.contentHeight).catch(() => { })));
|
|
384
|
-
this.logger.log(`Auto-scan: "${screenName}" tiles uploaded`);
|
|
385
|
-
}
|
|
386
|
-
catch (err) {
|
|
387
|
-
this.logger.warn('Auto-scan failed:', err);
|
|
388
|
-
}
|
|
389
|
-
})();
|
|
390
|
-
}
|
|
391
367
|
onScreenEnter(screenName) {
|
|
392
368
|
this.currentScreen = screenName;
|
|
393
369
|
this.currentScreenEnteredAt = new Date().toISOString();
|
|
@@ -396,14 +372,12 @@ class AppAnalytics {
|
|
|
396
372
|
this.scrollViewRegistry.clear();
|
|
397
373
|
this.errorTracker?.setCurrentScreen(screenName);
|
|
398
374
|
this.snapshotCapture?.setCurrentScreen(screenName);
|
|
399
|
-
// NOTE:
|
|
400
|
-
// React Native does NOT render off-screen content to
|
|
401
|
-
// tree, so programmatic
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
// Full-content capture relies entirely on RepliqoScrollView capturing
|
|
406
|
-
// viewport tiles as the user naturally scrolls through the content.
|
|
375
|
+
// NOTE: full-content capture is NOT done via programmatic scroll +
|
|
376
|
+
// native capture. React Native does NOT render off-screen content to
|
|
377
|
+
// the GPU or view tree, so any programmatic-scroll approach produces
|
|
378
|
+
// blank tiles. Full-content heatmap backgrounds are built entirely
|
|
379
|
+
// from viewport tiles captured by RepliqoScrollView as the user
|
|
380
|
+
// naturally scrolls through the content.
|
|
407
381
|
this.logger.log('Screen entered:', screenName);
|
|
408
382
|
}
|
|
409
383
|
onScreenExit(screenName) {
|
package/dist/index.d.ts
CHANGED
|
@@ -5,8 +5,7 @@ export { trackScreenEnter, trackScreenExit, } from './trackers/screen.tracker';
|
|
|
5
5
|
export { ErrorTracker } from './trackers/error.tracker';
|
|
6
6
|
export { SnapshotCapture } from './snapshot/capture';
|
|
7
7
|
export { captureScreenshot } from './snapshot/captureScreenshot';
|
|
8
|
-
export { isNativeScreenCaptureAvailable, captureNativeFrame,
|
|
9
|
-
export type { FullContentResult } from './snapshot/NativeScreenCapture';
|
|
8
|
+
export { isNativeScreenCaptureAvailable, captureNativeFrame, } from './snapshot/NativeScreenCapture';
|
|
10
9
|
export type { EventType, AnalyticsEvent, TouchEventData, NavigationEventData, ScreenVisit, DeviceInfo, SDKConfig, } from './types/events';
|
|
11
10
|
export type { ScreenshotData, SnapshotPayload, } from './types/snapshot';
|
|
12
11
|
export type { CrashReport } from './types/crash';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_CONFIG = exports.RepliqoScrollView = exports.
|
|
3
|
+
exports.DEFAULT_CONFIG = exports.RepliqoScrollView = exports.captureNativeFrame = exports.isNativeScreenCaptureAvailable = exports.captureScreenshot = exports.SnapshotCapture = exports.ErrorTracker = exports.trackScreenExit = exports.trackScreenEnter = exports.trackScreenChange = exports.useNavigationTracker = exports.TouchTracker = exports.AppAnalytics = void 0;
|
|
4
4
|
// Main class
|
|
5
5
|
var client_1 = require("./core/client");
|
|
6
6
|
Object.defineProperty(exports, "AppAnalytics", { enumerable: true, get: function () { return client_1.AppAnalytics; } });
|
|
@@ -23,7 +23,6 @@ Object.defineProperty(exports, "captureScreenshot", { enumerable: true, get: fun
|
|
|
23
23
|
var NativeScreenCapture_1 = require("./snapshot/NativeScreenCapture");
|
|
24
24
|
Object.defineProperty(exports, "isNativeScreenCaptureAvailable", { enumerable: true, get: function () { return NativeScreenCapture_1.isNativeScreenCaptureAvailable; } });
|
|
25
25
|
Object.defineProperty(exports, "captureNativeFrame", { enumerable: true, get: function () { return NativeScreenCapture_1.captureNativeFrame; } });
|
|
26
|
-
Object.defineProperty(exports, "captureFullContent", { enumerable: true, get: function () { return NativeScreenCapture_1.captureFullContent; } });
|
|
27
26
|
// Components
|
|
28
27
|
var RepliqoScrollView_1 = require("./components/RepliqoScrollView");
|
|
29
28
|
Object.defineProperty(exports, "RepliqoScrollView", { enumerable: true, get: function () { return RepliqoScrollView_1.RepliqoScrollView; } });
|
|
@@ -12,50 +12,12 @@ export declare function isNativeScreenCaptureAvailable(): boolean;
|
|
|
12
12
|
*
|
|
13
13
|
* No permissions required. No dialog. No foreground service.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
* including content scrolled off-screen. Used for heatmap backgrounds.
|
|
21
|
-
*
|
|
22
|
-
* Falls back to a viewport capture if no ScrollView is found.
|
|
23
|
-
* Max height: 5000px. JPEG quality: 60%. Width: ~500px.
|
|
15
|
+
* This is the ONLY native capture path. Full-content capture of
|
|
16
|
+
* off-screen ScrollView content is NOT possible natively (React Native
|
|
17
|
+
* doesn't render off-screen content to the GPU/view tree) — full-content
|
|
18
|
+
* heatmap backgrounds are instead built collaboratively from viewport
|
|
19
|
+
* tiles captured as the user naturally scrolls (see RepliqoScrollView).
|
|
24
20
|
*
|
|
25
21
|
* @returns NativeFrameResult or null if capture failed / not available
|
|
26
22
|
*/
|
|
27
|
-
export
|
|
28
|
-
/** True if the capture includes full scrollable content. False if viewport-only fallback. */
|
|
29
|
-
isFullContent: boolean;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Capture the FULL content of the primary ScrollView on screen,
|
|
33
|
-
* including content scrolled off-screen. Used for heatmap backgrounds.
|
|
34
|
-
*
|
|
35
|
-
* Falls back to a viewport capture if no ScrollView is found.
|
|
36
|
-
* Max height: 5000px. JPEG quality: 60%. Width: ~500px.
|
|
37
|
-
*
|
|
38
|
-
* Known limitation: FlatList/SectionList use RecyclerView which only
|
|
39
|
-
* renders visible items — full-content capture falls back to viewport.
|
|
40
|
-
*
|
|
41
|
-
* @returns FullContentResult with isFullContent flag, or null
|
|
42
|
-
*/
|
|
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>;
|
|
23
|
+
export declare function captureNativeFrame(): Promise<NativeFrameResult | null>;
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isNativeScreenCaptureAvailable = isNativeScreenCaptureAvailable;
|
|
4
4
|
exports.captureNativeFrame = captureNativeFrame;
|
|
5
|
-
exports.captureFullContent = captureFullContent;
|
|
6
|
-
exports.scanFullContent = scanFullContent;
|
|
7
5
|
const react_native_1 = require("react-native");
|
|
8
6
|
const NativeModule = react_native_1.Platform.OS === 'android'
|
|
9
7
|
? react_native_1.NativeModules.ScreenCaptureModule ?? null
|
|
@@ -18,6 +16,12 @@ function isNativeScreenCaptureAvailable() {
|
|
|
18
16
|
*
|
|
19
17
|
* No permissions required. No dialog. No foreground service.
|
|
20
18
|
*
|
|
19
|
+
* This is the ONLY native capture path. Full-content capture of
|
|
20
|
+
* off-screen ScrollView content is NOT possible natively (React Native
|
|
21
|
+
* doesn't render off-screen content to the GPU/view tree) — full-content
|
|
22
|
+
* heatmap backgrounds are instead built collaboratively from viewport
|
|
23
|
+
* tiles captured as the user naturally scrolls (see RepliqoScrollView).
|
|
24
|
+
*
|
|
21
25
|
* @returns NativeFrameResult or null if capture failed / not available
|
|
22
26
|
*/
|
|
23
27
|
async function captureNativeFrame() {
|
|
@@ -38,65 +42,3 @@ async function captureNativeFrame() {
|
|
|
38
42
|
return null;
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Capture the FULL content of the primary ScrollView on screen,
|
|
43
|
-
* including content scrolled off-screen. Used for heatmap backgrounds.
|
|
44
|
-
*
|
|
45
|
-
* Falls back to a viewport capture if no ScrollView is found.
|
|
46
|
-
* Max height: 5000px. JPEG quality: 60%. Width: ~500px.
|
|
47
|
-
*
|
|
48
|
-
* Known limitation: FlatList/SectionList use RecyclerView which only
|
|
49
|
-
* renders visible items — full-content capture falls back to viewport.
|
|
50
|
-
*
|
|
51
|
-
* @returns FullContentResult with isFullContent flag, or null
|
|
52
|
-
*/
|
|
53
|
-
async function captureFullContent() {
|
|
54
|
-
if (!NativeModule)
|
|
55
|
-
return null;
|
|
56
|
-
try {
|
|
57
|
-
const result = await NativeModule.captureFullContent();
|
|
58
|
-
if (!result || !result.image)
|
|
59
|
-
return null;
|
|
60
|
-
return {
|
|
61
|
-
image: result.image,
|
|
62
|
-
width: result.width,
|
|
63
|
-
height: result.height,
|
|
64
|
-
format: 'jpeg',
|
|
65
|
-
isFullContent: !!result.isFullContent,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return null;
|
|
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.
|
|
3
|
+
"version": "0.3.8",
|
|
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
|
|
15
|
+
import { captureNativeFrame } from '../snapshot/NativeScreenCapture';
|
|
16
16
|
|
|
17
17
|
export class AppAnalytics {
|
|
18
18
|
private static instance: AppAnalytics | null = null;
|
|
@@ -499,48 +499,6 @@ export class AppAnalytics {
|
|
|
499
499
|
})();
|
|
500
500
|
}
|
|
501
501
|
|
|
502
|
-
/**
|
|
503
|
-
* Programmatically scroll-scan the current ScrollView and upload all
|
|
504
|
-
* tiles to the backend. Invisible to the user (~300-500ms).
|
|
505
|
-
*/
|
|
506
|
-
private autoScanScreen(screenName: string): void {
|
|
507
|
-
(async () => {
|
|
508
|
-
try {
|
|
509
|
-
const tiles = await scanFullContent();
|
|
510
|
-
if (!tiles || tiles.length === 0) {
|
|
511
|
-
this.logger.log(`Auto-scan: no ScrollView found for "${screenName}"`);
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
this.logger.log(
|
|
516
|
-
`Auto-scan: "${screenName}" captured ${tiles.length} tiles ` +
|
|
517
|
-
`(content=${tiles[0].contentHeight}px)`,
|
|
518
|
-
);
|
|
519
|
-
|
|
520
|
-
// Upload all tiles in parallel
|
|
521
|
-
const sessionId = this.sessionId!;
|
|
522
|
-
await Promise.all(
|
|
523
|
-
tiles.map((tile) =>
|
|
524
|
-
this.apiClient.uploadScreenTile(
|
|
525
|
-
sessionId,
|
|
526
|
-
screenName,
|
|
527
|
-
tile.image,
|
|
528
|
-
tile.width,
|
|
529
|
-
tile.height,
|
|
530
|
-
tile.scrollOffsetY,
|
|
531
|
-
tile.viewportHeight,
|
|
532
|
-
tile.contentHeight,
|
|
533
|
-
).catch(() => {}),
|
|
534
|
-
),
|
|
535
|
-
);
|
|
536
|
-
|
|
537
|
-
this.logger.log(`Auto-scan: "${screenName}" tiles uploaded`);
|
|
538
|
-
} catch (err) {
|
|
539
|
-
this.logger.warn('Auto-scan failed:', err);
|
|
540
|
-
}
|
|
541
|
-
})();
|
|
542
|
-
}
|
|
543
|
-
|
|
544
502
|
onScreenEnter(screenName: string): void {
|
|
545
503
|
this.currentScreen = screenName;
|
|
546
504
|
this.currentScreenEnteredAt = new Date().toISOString();
|
|
@@ -549,14 +507,12 @@ export class AppAnalytics {
|
|
|
549
507
|
this.scrollViewRegistry.clear();
|
|
550
508
|
this.errorTracker?.setCurrentScreen(screenName);
|
|
551
509
|
this.snapshotCapture?.setCurrentScreen(screenName);
|
|
552
|
-
// NOTE:
|
|
553
|
-
// React Native does NOT render off-screen content to
|
|
554
|
-
// tree, so programmatic
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
// Full-content capture relies entirely on RepliqoScrollView capturing
|
|
559
|
-
// viewport tiles as the user naturally scrolls through the content.
|
|
510
|
+
// NOTE: full-content capture is NOT done via programmatic scroll +
|
|
511
|
+
// native capture. React Native does NOT render off-screen content to
|
|
512
|
+
// the GPU or view tree, so any programmatic-scroll approach produces
|
|
513
|
+
// blank tiles. Full-content heatmap backgrounds are built entirely
|
|
514
|
+
// from viewport tiles captured by RepliqoScrollView as the user
|
|
515
|
+
// naturally scrolls through the content.
|
|
560
516
|
|
|
561
517
|
this.logger.log('Screen entered:', screenName);
|
|
562
518
|
}
|
package/src/index.ts
CHANGED
|
@@ -19,9 +19,7 @@ export { captureScreenshot } from './snapshot/captureScreenshot';
|
|
|
19
19
|
export {
|
|
20
20
|
isNativeScreenCaptureAvailable,
|
|
21
21
|
captureNativeFrame,
|
|
22
|
-
captureFullContent,
|
|
23
22
|
} from './snapshot/NativeScreenCapture';
|
|
24
|
-
export type { FullContentResult } from './snapshot/NativeScreenCapture';
|
|
25
23
|
|
|
26
24
|
// Types
|
|
27
25
|
export type {
|
|
@@ -14,24 +14,8 @@ interface NativeCaptureResult {
|
|
|
14
14
|
format: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
interface NativeFullContentResult extends NativeCaptureResult {
|
|
18
|
-
isFullContent: boolean;
|
|
19
|
-
}
|
|
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
|
-
|
|
31
17
|
interface NativeScreenCaptureModule {
|
|
32
18
|
captureFrame(): Promise<NativeCaptureResult | null>;
|
|
33
|
-
captureFullContent(): Promise<NativeFullContentResult | null>;
|
|
34
|
-
scanFullContent(): Promise<NativeTile[] | null>;
|
|
35
19
|
isAvailable(): Promise<boolean>;
|
|
36
20
|
}
|
|
37
21
|
|
|
@@ -51,6 +35,12 @@ export function isNativeScreenCaptureAvailable(): boolean {
|
|
|
51
35
|
*
|
|
52
36
|
* No permissions required. No dialog. No foreground service.
|
|
53
37
|
*
|
|
38
|
+
* This is the ONLY native capture path. Full-content capture of
|
|
39
|
+
* off-screen ScrollView content is NOT possible natively (React Native
|
|
40
|
+
* doesn't render off-screen content to the GPU/view tree) — full-content
|
|
41
|
+
* heatmap backgrounds are instead built collaboratively from viewport
|
|
42
|
+
* tiles captured as the user naturally scrolls (see RepliqoScrollView).
|
|
43
|
+
*
|
|
54
44
|
* @returns NativeFrameResult or null if capture failed / not available
|
|
55
45
|
*/
|
|
56
46
|
export async function captureNativeFrame(): Promise<NativeFrameResult | null> {
|
|
@@ -70,88 +60,3 @@ export async function captureNativeFrame(): Promise<NativeFrameResult | null> {
|
|
|
70
60
|
return null;
|
|
71
61
|
}
|
|
72
62
|
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Capture the FULL content of the primary ScrollView on screen,
|
|
76
|
-
* including content scrolled off-screen. Used for heatmap backgrounds.
|
|
77
|
-
*
|
|
78
|
-
* Falls back to a viewport capture if no ScrollView is found.
|
|
79
|
-
* Max height: 5000px. JPEG quality: 60%. Width: ~500px.
|
|
80
|
-
*
|
|
81
|
-
* @returns NativeFrameResult or null if capture failed / not available
|
|
82
|
-
*/
|
|
83
|
-
export interface FullContentResult extends NativeFrameResult {
|
|
84
|
-
/** True if the capture includes full scrollable content. False if viewport-only fallback. */
|
|
85
|
-
isFullContent: boolean;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Capture the FULL content of the primary ScrollView on screen,
|
|
90
|
-
* including content scrolled off-screen. Used for heatmap backgrounds.
|
|
91
|
-
*
|
|
92
|
-
* Falls back to a viewport capture if no ScrollView is found.
|
|
93
|
-
* Max height: 5000px. JPEG quality: 60%. Width: ~500px.
|
|
94
|
-
*
|
|
95
|
-
* Known limitation: FlatList/SectionList use RecyclerView which only
|
|
96
|
-
* renders visible items — full-content capture falls back to viewport.
|
|
97
|
-
*
|
|
98
|
-
* @returns FullContentResult with isFullContent flag, or null
|
|
99
|
-
*/
|
|
100
|
-
export async function captureFullContent(): Promise<FullContentResult | null> {
|
|
101
|
-
if (!NativeModule) return null;
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
const result = await NativeModule.captureFullContent();
|
|
105
|
-
if (!result || !result.image) return null;
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
image: result.image,
|
|
109
|
-
width: result.width,
|
|
110
|
-
height: result.height,
|
|
111
|
-
format: 'jpeg',
|
|
112
|
-
isFullContent: !!result.isFullContent,
|
|
113
|
-
};
|
|
114
|
-
} catch {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
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
|
-
}
|