@repliqo/sdk-react-native 0.3.6 → 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/components/RepliqoScrollView.js +7 -1
- package/dist/core/client.d.ts +9 -9
- package/dist/core/client.js +17 -37
- 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/dist/transport/api.client.d.ts +8 -1
- package/dist/transport/api.client.js +25 -11
- package/package.json +1 -1
- package/src/components/RepliqoScrollView.tsx +9 -0
- package/src/core/client.ts +20 -54
- package/src/index.ts +0 -2
- package/src/snapshot/NativeScreenCapture.ts +6 -101
- package/src/transport/api.client.ts +27 -10
- 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
|
}
|
|
@@ -79,6 +79,10 @@ exports.RepliqoScrollView = react_1.default.forwardRef(({ onScroll, onMomentumSc
|
|
|
79
79
|
const scrollViewId = (0, react_1.useRef)(nextScrollViewId());
|
|
80
80
|
const innerRef = (0, react_1.useRef)(null);
|
|
81
81
|
const isMounted = (0, react_1.useRef)(true);
|
|
82
|
+
// Last measured window-relative top Y of the ScrollView — passed to
|
|
83
|
+
// captureScrollTile so the backend can crop each tile to just the
|
|
84
|
+
// scroll viewport area (no fixed header/footer duplication).
|
|
85
|
+
const lastViewportTop = (0, react_1.useRef)(null);
|
|
82
86
|
const scrollState = (0, react_1.useRef)(null);
|
|
83
87
|
// Forward the inner ref to the parent's ref (if provided).
|
|
84
88
|
// IMPORTANT: do NOT clear innerRef on cleanup — RN sometimes calls the
|
|
@@ -112,6 +116,7 @@ exports.RepliqoScrollView = react_1.default.forwardRef(({ onScroll, onMomentumSc
|
|
|
112
116
|
// ignore those, the next onLayout/onScroll will re-measure.
|
|
113
117
|
if (width <= 0 || height <= 0)
|
|
114
118
|
return;
|
|
119
|
+
lastViewportTop.current = y;
|
|
115
120
|
try {
|
|
116
121
|
client_1.AppAnalytics.getInstance().registerScrollView(scrollViewId.current, {
|
|
117
122
|
x, y, width, height,
|
|
@@ -166,7 +171,8 @@ exports.RepliqoScrollView = react_1.default.forwardRef(({ onScroll, onMomentumSc
|
|
|
166
171
|
lastCaptureY.current = state.offsetY;
|
|
167
172
|
initialCaptured.current = true;
|
|
168
173
|
try {
|
|
169
|
-
|
|
174
|
+
const windowHeight = react_native_1.Dimensions.get('window').height;
|
|
175
|
+
client_1.AppAnalytics.getInstance().captureScrollTile(state.offsetY, state.viewportHeight, state.contentHeight, lastViewportTop.current ?? undefined, windowHeight);
|
|
170
176
|
}
|
|
171
177
|
catch { }
|
|
172
178
|
};
|
package/dist/core/client.d.ts
CHANGED
|
@@ -85,16 +85,16 @@ export declare class AppAnalytics {
|
|
|
85
85
|
* Capture the current viewport as a tile for collaborative screen
|
|
86
86
|
* building. Called by RepliqoScrollView on scroll stops.
|
|
87
87
|
*
|
|
88
|
-
* The tile includes
|
|
89
|
-
* in the full content this viewport belongs
|
|
90
|
-
*
|
|
88
|
+
* The tile includes:
|
|
89
|
+
* - `scrollOffsetY`: where in the full content this viewport belongs
|
|
90
|
+
* - `viewportTop`: Y position (window logical px) of the ScrollView's
|
|
91
|
+
* top edge. Used by the backend to crop each tile to just the
|
|
92
|
+
* scrollable area, removing fixed headers/footers that would
|
|
93
|
+
* otherwise duplicate in the composite.
|
|
94
|
+
* - `windowHeight`: window height in logical px, for image-to-logical
|
|
95
|
+
* scale computation during backend cropping.
|
|
91
96
|
*/
|
|
92
|
-
captureScrollTile(scrollOffsetY: number, viewportHeight: number, contentHeight: number): void;
|
|
93
|
-
/**
|
|
94
|
-
* Programmatically scroll-scan the current ScrollView and upload all
|
|
95
|
-
* tiles to the backend. Invisible to the user (~300-500ms).
|
|
96
|
-
*/
|
|
97
|
-
private autoScanScreen;
|
|
97
|
+
captureScrollTile(scrollOffsetY: number, viewportHeight: number, contentHeight: number, viewportTop?: number, windowHeight?: number): void;
|
|
98
98
|
onScreenEnter(screenName: string): void;
|
|
99
99
|
onScreenExit(screenName: string): void;
|
|
100
100
|
flush(): Promise<void>;
|
package/dist/core/client.js
CHANGED
|
@@ -331,11 +331,16 @@ class AppAnalytics {
|
|
|
331
331
|
* Capture the current viewport as a tile for collaborative screen
|
|
332
332
|
* building. Called by RepliqoScrollView on scroll stops.
|
|
333
333
|
*
|
|
334
|
-
* The tile includes
|
|
335
|
-
* in the full content this viewport belongs
|
|
336
|
-
*
|
|
334
|
+
* The tile includes:
|
|
335
|
+
* - `scrollOffsetY`: where in the full content this viewport belongs
|
|
336
|
+
* - `viewportTop`: Y position (window logical px) of the ScrollView's
|
|
337
|
+
* top edge. Used by the backend to crop each tile to just the
|
|
338
|
+
* scrollable area, removing fixed headers/footers that would
|
|
339
|
+
* otherwise duplicate in the composite.
|
|
340
|
+
* - `windowHeight`: window height in logical px, for image-to-logical
|
|
341
|
+
* scale computation during backend cropping.
|
|
337
342
|
*/
|
|
338
|
-
captureScrollTile(scrollOffsetY, viewportHeight, contentHeight) {
|
|
343
|
+
captureScrollTile(scrollOffsetY, viewportHeight, contentHeight, viewportTop, windowHeight) {
|
|
339
344
|
if (!this.sessionId || !this.currentScreen)
|
|
340
345
|
return;
|
|
341
346
|
if (viewportHeight <= 0)
|
|
@@ -350,38 +355,15 @@ class AppAnalytics {
|
|
|
350
355
|
return;
|
|
351
356
|
this.logger.log(`Scroll tile: "${screenName}" Y=${Math.round(scrollOffsetY)} ` +
|
|
352
357
|
`vp=${Math.round(viewportHeight)} content=${Math.round(contentHeight)} ` +
|
|
358
|
+
`top=${viewportTop !== undefined ? Math.round(viewportTop) : '?'} ` +
|
|
353
359
|
`${Math.round(frame.image.length / 1024)}KB`);
|
|
354
|
-
await this.apiClient.uploadScreenTile(sessionId, screenName, frame.image, frame.width, frame.height, scrollOffsetY, viewportHeight, contentHeight);
|
|
360
|
+
await this.apiClient.uploadScreenTile(sessionId, screenName, frame.image, frame.width, frame.height, scrollOffsetY, viewportHeight, contentHeight, viewportTop, windowHeight);
|
|
355
361
|
}
|
|
356
362
|
catch (err) {
|
|
357
363
|
this.logger.warn('Scroll tile failed:', err);
|
|
358
364
|
}
|
|
359
365
|
})();
|
|
360
366
|
}
|
|
361
|
-
/**
|
|
362
|
-
* Programmatically scroll-scan the current ScrollView and upload all
|
|
363
|
-
* tiles to the backend. Invisible to the user (~300-500ms).
|
|
364
|
-
*/
|
|
365
|
-
autoScanScreen(screenName) {
|
|
366
|
-
(async () => {
|
|
367
|
-
try {
|
|
368
|
-
const tiles = await (0, NativeScreenCapture_1.scanFullContent)();
|
|
369
|
-
if (!tiles || tiles.length === 0) {
|
|
370
|
-
this.logger.log(`Auto-scan: no ScrollView found for "${screenName}"`);
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
this.logger.log(`Auto-scan: "${screenName}" captured ${tiles.length} tiles ` +
|
|
374
|
-
`(content=${tiles[0].contentHeight}px)`);
|
|
375
|
-
// Upload all tiles in parallel
|
|
376
|
-
const sessionId = this.sessionId;
|
|
377
|
-
await Promise.all(tiles.map((tile) => this.apiClient.uploadScreenTile(sessionId, screenName, tile.image, tile.width, tile.height, tile.scrollOffsetY, tile.viewportHeight, tile.contentHeight).catch(() => { })));
|
|
378
|
-
this.logger.log(`Auto-scan: "${screenName}" tiles uploaded`);
|
|
379
|
-
}
|
|
380
|
-
catch (err) {
|
|
381
|
-
this.logger.warn('Auto-scan failed:', err);
|
|
382
|
-
}
|
|
383
|
-
})();
|
|
384
|
-
}
|
|
385
367
|
onScreenEnter(screenName) {
|
|
386
368
|
this.currentScreen = screenName;
|
|
387
369
|
this.currentScreenEnteredAt = new Date().toISOString();
|
|
@@ -390,14 +372,12 @@ class AppAnalytics {
|
|
|
390
372
|
this.scrollViewRegistry.clear();
|
|
391
373
|
this.errorTracker?.setCurrentScreen(screenName);
|
|
392
374
|
this.snapshotCapture?.setCurrentScreen(screenName);
|
|
393
|
-
// NOTE:
|
|
394
|
-
// React Native does NOT render off-screen content to
|
|
395
|
-
// tree, so programmatic
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
// Full-content capture relies entirely on RepliqoScrollView capturing
|
|
400
|
-
// 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.
|
|
401
381
|
this.logger.log('Screen entered:', screenName);
|
|
402
382
|
}
|
|
403
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
|
-
}
|
|
@@ -32,7 +32,14 @@ export declare class ApiClient {
|
|
|
32
32
|
*/
|
|
33
33
|
/**
|
|
34
34
|
* Upload a scroll tile for collaborative screen building.
|
|
35
|
+
*
|
|
36
|
+
* @param viewportTop Y position (window logical px) of the ScrollView's
|
|
37
|
+
* top edge. Used by the backend to crop each tile to just the
|
|
38
|
+
* scrollable area, removing fixed headers/footers that would
|
|
39
|
+
* otherwise duplicate in the composite.
|
|
40
|
+
* @param windowHeight Window height (logical px) at capture time.
|
|
41
|
+
* Used by the backend to compute image-to-logical scale for cropping.
|
|
35
42
|
*/
|
|
36
|
-
uploadScreenTile(sessionId: string, screenName: string, image: string, width: number, height: number, scrollOffsetY: number, viewportHeight: number, contentHeight: number): Promise<void>;
|
|
43
|
+
uploadScreenTile(sessionId: string, screenName: string, image: string, width: number, height: number, scrollOffsetY: number, viewportHeight: number, contentHeight: number, viewportTop?: number, windowHeight?: number): Promise<void>;
|
|
37
44
|
uploadScreenCapture(screenName: string, image: string, width: number, height: number, isFullContent: boolean): Promise<void>;
|
|
38
45
|
}
|
|
@@ -158,22 +158,36 @@ class ApiClient {
|
|
|
158
158
|
*/
|
|
159
159
|
/**
|
|
160
160
|
* Upload a scroll tile for collaborative screen building.
|
|
161
|
+
*
|
|
162
|
+
* @param viewportTop Y position (window logical px) of the ScrollView's
|
|
163
|
+
* top edge. Used by the backend to crop each tile to just the
|
|
164
|
+
* scrollable area, removing fixed headers/footers that would
|
|
165
|
+
* otherwise duplicate in the composite.
|
|
166
|
+
* @param windowHeight Window height (logical px) at capture time.
|
|
167
|
+
* Used by the backend to compute image-to-logical scale for cropping.
|
|
161
168
|
*/
|
|
162
|
-
async uploadScreenTile(sessionId, screenName, image, width, height, scrollOffsetY, viewportHeight, contentHeight) {
|
|
169
|
+
async uploadScreenTile(sessionId, screenName, image, width, height, scrollOffsetY, viewportHeight, contentHeight, viewportTop, windowHeight) {
|
|
163
170
|
try {
|
|
171
|
+
const body = {
|
|
172
|
+
sessionId,
|
|
173
|
+
screenName,
|
|
174
|
+
image,
|
|
175
|
+
width,
|
|
176
|
+
height,
|
|
177
|
+
scrollOffsetY: Math.round(scrollOffsetY),
|
|
178
|
+
viewportHeight: Math.round(viewportHeight),
|
|
179
|
+
contentHeight: Math.round(contentHeight),
|
|
180
|
+
};
|
|
181
|
+
if (typeof viewportTop === 'number' && Number.isFinite(viewportTop)) {
|
|
182
|
+
body.viewportTop = Math.round(viewportTop);
|
|
183
|
+
}
|
|
184
|
+
if (typeof windowHeight === 'number' && Number.isFinite(windowHeight)) {
|
|
185
|
+
body.windowHeight = Math.round(windowHeight);
|
|
186
|
+
}
|
|
164
187
|
const response = await fetch(`${this.baseUrl}/api/screen-tiles`, {
|
|
165
188
|
method: 'POST',
|
|
166
189
|
headers: this.getHeaders(),
|
|
167
|
-
body: JSON.stringify(
|
|
168
|
-
sessionId,
|
|
169
|
-
screenName,
|
|
170
|
-
image,
|
|
171
|
-
width,
|
|
172
|
-
height,
|
|
173
|
-
scrollOffsetY: Math.round(scrollOffsetY),
|
|
174
|
-
viewportHeight: Math.round(viewportHeight),
|
|
175
|
-
contentHeight: Math.round(contentHeight),
|
|
176
|
-
}),
|
|
190
|
+
body: JSON.stringify(body),
|
|
177
191
|
});
|
|
178
192
|
if (!response.ok) {
|
|
179
193
|
const text = await response.text();
|
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",
|
|
@@ -2,6 +2,7 @@ import React, { useCallback, useRef, useEffect } from 'react';
|
|
|
2
2
|
import {
|
|
3
3
|
ScrollView,
|
|
4
4
|
Keyboard,
|
|
5
|
+
Dimensions,
|
|
5
6
|
type ScrollViewProps,
|
|
6
7
|
type NativeSyntheticEvent,
|
|
7
8
|
type NativeScrollEvent,
|
|
@@ -54,6 +55,10 @@ export const RepliqoScrollView = React.forwardRef<ScrollView, ScrollViewProps>(
|
|
|
54
55
|
const scrollViewId = useRef<string>(nextScrollViewId());
|
|
55
56
|
const innerRef = useRef<ScrollView | null>(null);
|
|
56
57
|
const isMounted = useRef(true);
|
|
58
|
+
// Last measured window-relative top Y of the ScrollView — passed to
|
|
59
|
+
// captureScrollTile so the backend can crop each tile to just the
|
|
60
|
+
// scroll viewport area (no fixed header/footer duplication).
|
|
61
|
+
const lastViewportTop = useRef<number | null>(null);
|
|
57
62
|
const scrollState = useRef<{
|
|
58
63
|
offsetY: number;
|
|
59
64
|
viewportHeight: number;
|
|
@@ -88,6 +93,7 @@ export const RepliqoScrollView = React.forwardRef<ScrollView, ScrollViewProps>(
|
|
|
88
93
|
// RN sometimes returns 0/0 dims before the view is positioned —
|
|
89
94
|
// ignore those, the next onLayout/onScroll will re-measure.
|
|
90
95
|
if (width <= 0 || height <= 0) return;
|
|
96
|
+
lastViewportTop.current = y;
|
|
91
97
|
try {
|
|
92
98
|
AppAnalytics.getInstance().registerScrollView(scrollViewId.current, {
|
|
93
99
|
x, y, width, height,
|
|
@@ -141,10 +147,13 @@ export const RepliqoScrollView = React.forwardRef<ScrollView, ScrollViewProps>(
|
|
|
141
147
|
initialCaptured.current = true;
|
|
142
148
|
|
|
143
149
|
try {
|
|
150
|
+
const windowHeight = Dimensions.get('window').height;
|
|
144
151
|
AppAnalytics.getInstance().captureScrollTile(
|
|
145
152
|
state.offsetY,
|
|
146
153
|
state.viewportHeight,
|
|
147
154
|
state.contentHeight,
|
|
155
|
+
lastViewportTop.current ?? undefined,
|
|
156
|
+
windowHeight,
|
|
148
157
|
);
|
|
149
158
|
} catch {}
|
|
150
159
|
};
|