@repliqo/sdk-react-native 0.1.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/INTEGRATION_GUIDE.md +1312 -0
- package/android/build.gradle +24 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/com/repliqo/screencapture/MultiWindowCapture.java +230 -0
- package/android/src/main/java/com/repliqo/screencapture/ScreenCaptureModule.java +94 -0
- package/android/src/main/java/com/repliqo/screencapture/ScreenCapturePackage.java +38 -0
- package/dist/components/ScreenCapturePermission.d.ts +55 -0
- package/dist/components/ScreenCapturePermission.js +112 -0
- package/dist/core/client.d.ts +41 -0
- package/dist/core/client.js +309 -0
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.js +26 -0
- package/dist/core/logger.d.ts +8 -0
- package/dist/core/logger.js +25 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +28 -0
- package/dist/snapshot/NativeScreenCapture.d.ts +17 -0
- package/dist/snapshot/NativeScreenCapture.js +38 -0
- package/dist/snapshot/capture.d.ts +36 -0
- package/dist/snapshot/capture.js +109 -0
- package/dist/snapshot/captureScreenshot.d.ts +26 -0
- package/dist/snapshot/captureScreenshot.js +89 -0
- package/dist/trackers/error.tracker.d.ts +22 -0
- package/dist/trackers/error.tracker.js +123 -0
- package/dist/trackers/navigation.tracker.d.ts +6 -0
- package/dist/trackers/navigation.tracker.js +78 -0
- package/dist/trackers/screen.tracker.d.ts +2 -0
- package/dist/trackers/screen.tracker.js +23 -0
- package/dist/trackers/touch.tracker.d.ts +8 -0
- package/dist/trackers/touch.tracker.js +30 -0
- package/dist/transport/api.client.d.ts +29 -0
- package/dist/transport/api.client.js +156 -0
- package/dist/transport/batch-queue.d.ts +18 -0
- package/dist/transport/batch-queue.js +80 -0
- package/dist/types/crash.d.ts +10 -0
- package/dist/types/crash.js +2 -0
- package/dist/types/events.d.ts +53 -0
- package/dist/types/events.js +2 -0
- package/dist/types/snapshot.d.ts +21 -0
- package/dist/types/snapshot.js +9 -0
- package/package.json +64 -0
- package/src/components/ScreenCapturePermission.tsx +160 -0
- package/src/core/client.ts +425 -0
- package/src/core/config.ts +27 -0
- package/src/core/logger.ts +27 -0
- package/src/index.ts +47 -0
- package/src/snapshot/NativeScreenCapture.ts +54 -0
- package/src/snapshot/capture.ts +142 -0
- package/src/snapshot/captureScreenshot.ts +109 -0
- package/src/trackers/error.tracker.ts +136 -0
- package/src/trackers/navigation.tracker.ts +98 -0
- package/src/trackers/screen.tracker.ts +19 -0
- package/src/trackers/touch.tracker.tsx +44 -0
- package/src/transport/api.client.ts +225 -0
- package/src/transport/batch-queue.ts +95 -0
- package/src/types/crash.ts +10 -0
- package/src/types/events.ts +59 -0
- package/src/types/snapshot.ts +23 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.captureScreenshot = captureScreenshot;
|
|
4
|
+
exports.__resetCaptureRefForTests = __resetCaptureRefForTests;
|
|
5
|
+
const react_native_1 = require("react-native");
|
|
6
|
+
const NativeScreenCapture_1 = require("./NativeScreenCapture");
|
|
7
|
+
const TARGET_WIDTH = 390;
|
|
8
|
+
const JPEG_QUALITY = 0.4;
|
|
9
|
+
// --- view-shot fallback (lazy-loaded) ---
|
|
10
|
+
let viewShotCaptureScreen = null;
|
|
11
|
+
let viewShotResolved = false;
|
|
12
|
+
function resolveViewShot() {
|
|
13
|
+
if (viewShotResolved)
|
|
14
|
+
return viewShotCaptureScreen;
|
|
15
|
+
viewShotResolved = true;
|
|
16
|
+
try {
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
18
|
+
const viewShot = require('react-native-view-shot');
|
|
19
|
+
if (typeof viewShot.captureScreen === 'function') {
|
|
20
|
+
viewShotCaptureScreen = viewShot.captureScreen;
|
|
21
|
+
}
|
|
22
|
+
else if (typeof viewShot.captureRef === 'function') {
|
|
23
|
+
viewShotCaptureScreen = (opts) => viewShot.captureRef('window', opts);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// view-shot not installed - native capture only
|
|
28
|
+
}
|
|
29
|
+
return viewShotCaptureScreen;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Capture a JPEG screenshot using the best available method:
|
|
33
|
+
*
|
|
34
|
+
* 1. Native multi-window capture (Android) — uses View.draw() on all
|
|
35
|
+
* app windows via WindowManagerGlobal reflection. Captures modals,
|
|
36
|
+
* alerts, dialogs, overlays. No permissions needed.
|
|
37
|
+
* 2. react-native-view-shot fallback — captures the main RN view only
|
|
38
|
+
* (no modals or native dialogs)
|
|
39
|
+
* 3. null — if both are unavailable
|
|
40
|
+
*
|
|
41
|
+
* Never throws. Returns null on any failure.
|
|
42
|
+
*/
|
|
43
|
+
async function captureScreenshot() {
|
|
44
|
+
// Strategy 1: Native multi-window capture (preferred on Android)
|
|
45
|
+
if ((0, NativeScreenCapture_1.isNativeScreenCaptureAvailable)()) {
|
|
46
|
+
const native = await (0, NativeScreenCapture_1.captureNativeFrame)();
|
|
47
|
+
if (native)
|
|
48
|
+
return { ...native, source: 'native' };
|
|
49
|
+
// If native returns null (e.g. first frame not ready), fall through
|
|
50
|
+
// to view-shot for this single frame rather than showing nothing.
|
|
51
|
+
}
|
|
52
|
+
// Strategy 2: react-native-view-shot fallback
|
|
53
|
+
const captureScreen = resolveViewShot();
|
|
54
|
+
if (!captureScreen)
|
|
55
|
+
return null;
|
|
56
|
+
try {
|
|
57
|
+
const { width: screenWidth, height: screenHeight } = react_native_1.Dimensions.get('window');
|
|
58
|
+
const aspectRatio = screenHeight / screenWidth;
|
|
59
|
+
const outputWidth = Math.min(TARGET_WIDTH, screenWidth);
|
|
60
|
+
const outputHeight = Math.round(outputWidth * aspectRatio);
|
|
61
|
+
const base64 = await captureScreen({
|
|
62
|
+
format: 'jpg',
|
|
63
|
+
quality: JPEG_QUALITY,
|
|
64
|
+
width: outputWidth,
|
|
65
|
+
height: outputHeight,
|
|
66
|
+
result: 'base64',
|
|
67
|
+
});
|
|
68
|
+
if (typeof base64 !== 'string' || base64.length === 0) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
image: base64,
|
|
73
|
+
width: outputWidth,
|
|
74
|
+
height: outputHeight,
|
|
75
|
+
format: 'jpeg',
|
|
76
|
+
source: 'viewshot',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Test-only: reset cached view-shot resolution.
|
|
85
|
+
*/
|
|
86
|
+
function __resetCaptureRefForTests() {
|
|
87
|
+
viewShotCaptureScreen = null;
|
|
88
|
+
viewShotResolved = false;
|
|
89
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { CrashReport } from '../types/crash';
|
|
2
|
+
type CrashCallback = (crash: CrashReport) => void;
|
|
3
|
+
export declare class ErrorTracker {
|
|
4
|
+
private onCrash;
|
|
5
|
+
private sessionId;
|
|
6
|
+
private currentScreen;
|
|
7
|
+
private originalHandler;
|
|
8
|
+
private isActive;
|
|
9
|
+
constructor(onCrash: CrashCallback);
|
|
10
|
+
setSessionId(id: string | null): void;
|
|
11
|
+
setCurrentScreen(screen: string | null): void;
|
|
12
|
+
start(): void;
|
|
13
|
+
stop(): void;
|
|
14
|
+
/**
|
|
15
|
+
* Manual error reporting - allows consumers to report caught errors.
|
|
16
|
+
*/
|
|
17
|
+
reportError(error: Error, metadata?: Record<string, any>): void;
|
|
18
|
+
private setupGlobalErrorHandler;
|
|
19
|
+
private setupUnhandledRejectionHandler;
|
|
20
|
+
private restoreGlobalErrorHandler;
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ErrorTracker = void 0;
|
|
4
|
+
class ErrorTracker {
|
|
5
|
+
constructor(onCrash) {
|
|
6
|
+
this.sessionId = null;
|
|
7
|
+
this.currentScreen = null;
|
|
8
|
+
this.originalHandler = null;
|
|
9
|
+
this.isActive = false;
|
|
10
|
+
this.onCrash = onCrash;
|
|
11
|
+
}
|
|
12
|
+
setSessionId(id) {
|
|
13
|
+
this.sessionId = id;
|
|
14
|
+
}
|
|
15
|
+
setCurrentScreen(screen) {
|
|
16
|
+
this.currentScreen = screen;
|
|
17
|
+
}
|
|
18
|
+
start() {
|
|
19
|
+
if (this.isActive)
|
|
20
|
+
return;
|
|
21
|
+
this.isActive = true;
|
|
22
|
+
this.setupGlobalErrorHandler();
|
|
23
|
+
this.setupUnhandledRejectionHandler();
|
|
24
|
+
}
|
|
25
|
+
stop() {
|
|
26
|
+
if (!this.isActive)
|
|
27
|
+
return;
|
|
28
|
+
this.isActive = false;
|
|
29
|
+
this.restoreGlobalErrorHandler();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Manual error reporting - allows consumers to report caught errors.
|
|
33
|
+
*/
|
|
34
|
+
reportError(error, metadata) {
|
|
35
|
+
try {
|
|
36
|
+
const crash = {
|
|
37
|
+
sessionId: this.sessionId || undefined,
|
|
38
|
+
type: 'js_error',
|
|
39
|
+
message: error.message,
|
|
40
|
+
stackTrace: error.stack || undefined,
|
|
41
|
+
screenName: this.currentScreen || undefined,
|
|
42
|
+
metadata,
|
|
43
|
+
timestamp: new Date().toISOString(),
|
|
44
|
+
};
|
|
45
|
+
this.onCrash(crash);
|
|
46
|
+
}
|
|
47
|
+
catch (_e) {
|
|
48
|
+
// Never crash the app due to error reporting
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
setupGlobalErrorHandler() {
|
|
52
|
+
try {
|
|
53
|
+
const ErrorUtils = global.ErrorUtils;
|
|
54
|
+
if (ErrorUtils) {
|
|
55
|
+
this.originalHandler = ErrorUtils.getGlobalHandler();
|
|
56
|
+
ErrorUtils.setGlobalHandler((error, isFatal) => {
|
|
57
|
+
try {
|
|
58
|
+
const crash = {
|
|
59
|
+
sessionId: this.sessionId || undefined,
|
|
60
|
+
type: isFatal ? 'native_crash' : 'js_error',
|
|
61
|
+
message: error.message,
|
|
62
|
+
stackTrace: error.stack || undefined,
|
|
63
|
+
screenName: this.currentScreen || undefined,
|
|
64
|
+
metadata: { isFatal },
|
|
65
|
+
timestamp: new Date().toISOString(),
|
|
66
|
+
};
|
|
67
|
+
this.onCrash(crash);
|
|
68
|
+
}
|
|
69
|
+
catch (_e) {
|
|
70
|
+
// Never crash the app due to error reporting
|
|
71
|
+
}
|
|
72
|
+
// Always call original handler so RN can handle the error
|
|
73
|
+
if (this.originalHandler) {
|
|
74
|
+
this.originalHandler(error, isFatal);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (_e) {
|
|
80
|
+
// Never crash the app due to error handler setup
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
setupUnhandledRejectionHandler() {
|
|
84
|
+
try {
|
|
85
|
+
if (typeof global !== 'undefined') {
|
|
86
|
+
global.__analyticsUnhandledRejection = (_id, error) => {
|
|
87
|
+
try {
|
|
88
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
89
|
+
const stack = error instanceof Error ? error.stack : undefined;
|
|
90
|
+
const crash = {
|
|
91
|
+
sessionId: this.sessionId || undefined,
|
|
92
|
+
type: 'unhandled_rejection',
|
|
93
|
+
message,
|
|
94
|
+
stackTrace: stack || undefined,
|
|
95
|
+
screenName: this.currentScreen || undefined,
|
|
96
|
+
timestamp: new Date().toISOString(),
|
|
97
|
+
};
|
|
98
|
+
this.onCrash(crash);
|
|
99
|
+
}
|
|
100
|
+
catch (_e) {
|
|
101
|
+
// Never crash the app due to error reporting
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (_e) {
|
|
107
|
+
// Never crash the app due to rejection handler setup
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
restoreGlobalErrorHandler() {
|
|
111
|
+
try {
|
|
112
|
+
const ErrorUtils = global.ErrorUtils;
|
|
113
|
+
if (ErrorUtils && this.originalHandler) {
|
|
114
|
+
ErrorUtils.setGlobalHandler(this.originalHandler);
|
|
115
|
+
this.originalHandler = null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (_e) {
|
|
119
|
+
// Never crash the app due to handler restoration
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.ErrorTracker = ErrorTracker;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useNavigationTracker = useNavigationTracker;
|
|
4
|
+
exports.trackScreenChange = trackScreenChange;
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const client_1 = require("../core/client");
|
|
7
|
+
function getActiveRouteName(state) {
|
|
8
|
+
if (!state) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const route = state.routes[state.index];
|
|
12
|
+
if (!route) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
// Dive into nested navigators
|
|
16
|
+
if (route.state) {
|
|
17
|
+
return getActiveRouteName(route.state);
|
|
18
|
+
}
|
|
19
|
+
return route.name;
|
|
20
|
+
}
|
|
21
|
+
function useNavigationTracker() {
|
|
22
|
+
const routeNameRef = (0, react_1.useRef)(null);
|
|
23
|
+
const navigationRef = (0, react_1.useRef)(null);
|
|
24
|
+
const onReady = (0, react_1.useCallback)(() => {
|
|
25
|
+
if (navigationRef.current) {
|
|
26
|
+
const state = navigationRef.current.getRootState();
|
|
27
|
+
const currentRouteName = getActiveRouteName(state);
|
|
28
|
+
routeNameRef.current = currentRouteName;
|
|
29
|
+
if (currentRouteName) {
|
|
30
|
+
try {
|
|
31
|
+
const analytics = client_1.AppAnalytics.getInstance();
|
|
32
|
+
analytics.onScreenEnter(currentRouteName);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// AppAnalytics not initialized, silently ignore
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}, []);
|
|
40
|
+
const onStateChange = (0, react_1.useCallback)(() => {
|
|
41
|
+
if (!navigationRef.current) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const state = navigationRef.current.getRootState();
|
|
45
|
+
const currentRouteName = getActiveRouteName(state);
|
|
46
|
+
const previousRouteName = routeNameRef.current;
|
|
47
|
+
if (currentRouteName &&
|
|
48
|
+
previousRouteName &&
|
|
49
|
+
currentRouteName !== previousRouteName) {
|
|
50
|
+
try {
|
|
51
|
+
const analytics = client_1.AppAnalytics.getInstance();
|
|
52
|
+
analytics.trackNavigation(previousRouteName, currentRouteName);
|
|
53
|
+
analytics.onScreenExit(previousRouteName);
|
|
54
|
+
analytics.onScreenEnter(currentRouteName);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// AppAnalytics not initialized, silently ignore
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
routeNameRef.current = currentRouteName;
|
|
61
|
+
}, []);
|
|
62
|
+
return {
|
|
63
|
+
navigationRef,
|
|
64
|
+
onStateChange,
|
|
65
|
+
onReady,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function trackScreenChange(fromScreen, toScreen) {
|
|
69
|
+
try {
|
|
70
|
+
const analytics = client_1.AppAnalytics.getInstance();
|
|
71
|
+
analytics.trackNavigation(fromScreen, toScreen);
|
|
72
|
+
analytics.onScreenExit(fromScreen);
|
|
73
|
+
analytics.onScreenEnter(toScreen);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// AppAnalytics not initialized, silently ignore
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.trackScreenEnter = trackScreenEnter;
|
|
4
|
+
exports.trackScreenExit = trackScreenExit;
|
|
5
|
+
const client_1 = require("../core/client");
|
|
6
|
+
function trackScreenEnter(screenName) {
|
|
7
|
+
try {
|
|
8
|
+
const analytics = client_1.AppAnalytics.getInstance();
|
|
9
|
+
analytics.onScreenEnter(screenName);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// AppAnalytics not initialized, silently ignore
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function trackScreenExit(screenName) {
|
|
16
|
+
try {
|
|
17
|
+
const analytics = client_1.AppAnalytics.getInstance();
|
|
18
|
+
analytics.onScreenExit(screenName);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// AppAnalytics not initialized, silently ignore
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TouchTracker = void 0;
|
|
7
|
+
const react_1 = __importDefault(require("react"));
|
|
8
|
+
const react_native_1 = require("react-native");
|
|
9
|
+
const client_1 = require("../core/client");
|
|
10
|
+
const TouchTracker = ({ children, screenName, enabled = true, }) => {
|
|
11
|
+
const handleTouchCapture = react_1.default.useCallback((event) => {
|
|
12
|
+
if (!enabled) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const analytics = client_1.AppAnalytics.getInstance();
|
|
17
|
+
const { pageX, pageY } = event.nativeEvent;
|
|
18
|
+
analytics.trackTouch(pageX, pageY, screenName);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// AppAnalytics not initialized, silently ignore
|
|
22
|
+
}
|
|
23
|
+
// Return false so touches pass through to children
|
|
24
|
+
return false;
|
|
25
|
+
}, [enabled, screenName]);
|
|
26
|
+
return (<react_native_1.View style={{ flex: 1 }} onStartShouldSetResponderCapture={handleTouchCapture}>
|
|
27
|
+
{children}
|
|
28
|
+
</react_native_1.View>);
|
|
29
|
+
};
|
|
30
|
+
exports.TouchTracker = TouchTracker;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AnalyticsEvent, DeviceInfo, ScreenVisit } from '../types/events';
|
|
2
|
+
import { SnapshotPayload } from '../types/snapshot';
|
|
3
|
+
import { CrashReport } from '../types/crash';
|
|
4
|
+
import { Logger } from '../core/logger';
|
|
5
|
+
export interface SessionResponse {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class ApiClient {
|
|
9
|
+
private baseUrl;
|
|
10
|
+
private apiKey;
|
|
11
|
+
private logger;
|
|
12
|
+
constructor(baseUrl: string, apiKey: string, logger: Logger);
|
|
13
|
+
private getHeaders;
|
|
14
|
+
startSession(deviceId: string, deviceInfo?: DeviceInfo): Promise<{
|
|
15
|
+
id: string;
|
|
16
|
+
}>;
|
|
17
|
+
endSession(sessionId: string): Promise<void>;
|
|
18
|
+
sendEventsBatch(sessionId: string, events: AnalyticsEvent[]): Promise<{
|
|
19
|
+
count: number;
|
|
20
|
+
}>;
|
|
21
|
+
sendScreenVisitsBatch(visits: ScreenVisit[]): Promise<void>;
|
|
22
|
+
sendSnapshotsBatch(snapshots: SnapshotPayload[]): Promise<{
|
|
23
|
+
count: number;
|
|
24
|
+
}>;
|
|
25
|
+
sendCrashReport(crash: CrashReport): Promise<void>;
|
|
26
|
+
sendCrashesBatch(crashes: CrashReport[]): Promise<{
|
|
27
|
+
count: number;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiClient = void 0;
|
|
4
|
+
class ApiClient {
|
|
5
|
+
constructor(baseUrl, apiKey, logger) {
|
|
6
|
+
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
7
|
+
this.apiKey = apiKey;
|
|
8
|
+
this.logger = logger;
|
|
9
|
+
}
|
|
10
|
+
getHeaders() {
|
|
11
|
+
return {
|
|
12
|
+
'Content-Type': 'application/json',
|
|
13
|
+
'x-api-key': this.apiKey,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
async startSession(deviceId, deviceInfo) {
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch(`${this.baseUrl}/api/sessions/start`, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: this.getHeaders(),
|
|
21
|
+
body: JSON.stringify({ deviceId, deviceInfo }),
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
const text = await response.text();
|
|
25
|
+
this.logger.error('Failed to start session:', response.status, text);
|
|
26
|
+
throw new Error(`Failed to start session: ${response.status}`);
|
|
27
|
+
}
|
|
28
|
+
const data = await response.json();
|
|
29
|
+
this.logger.log('Session started:', data.sessionId);
|
|
30
|
+
return { id: data.sessionId };
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
this.logger.error('Error starting session:', error);
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async endSession(sessionId) {
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetch(`${this.baseUrl}/api/sessions/${sessionId}/end`, {
|
|
40
|
+
method: 'PATCH',
|
|
41
|
+
headers: this.getHeaders(),
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
const text = await response.text();
|
|
45
|
+
this.logger.error('Failed to end session:', response.status, text);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.logger.log('Session ended:', sessionId);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
this.logger.error('Error ending session:', error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async sendEventsBatch(sessionId, events) {
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch(`${this.baseUrl}/api/events/batch`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: this.getHeaders(),
|
|
59
|
+
body: JSON.stringify({ sessionId, events }),
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
const text = await response.text();
|
|
63
|
+
this.logger.error('Failed to send events batch:', response.status, text);
|
|
64
|
+
throw new Error(`Failed to send events batch: ${response.status}`);
|
|
65
|
+
}
|
|
66
|
+
const data = await response.json();
|
|
67
|
+
this.logger.log('Events batch sent, count:', data.count);
|
|
68
|
+
return { count: data.count };
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
this.logger.error('Error sending events batch:', error);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async sendScreenVisitsBatch(visits) {
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetch(`${this.baseUrl}/api/screens/visit/batch`, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: this.getHeaders(),
|
|
80
|
+
body: JSON.stringify({ visits }),
|
|
81
|
+
});
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
const text = await response.text();
|
|
84
|
+
this.logger.error('Failed to send screen visits batch:', response.status, text);
|
|
85
|
+
throw new Error(`Failed to send screen visits batch: ${response.status}`);
|
|
86
|
+
}
|
|
87
|
+
this.logger.log('Screen visits batch sent, count:', visits.length);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
this.logger.error('Error sending screen visits batch:', error);
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async sendSnapshotsBatch(snapshots) {
|
|
95
|
+
try {
|
|
96
|
+
const response = await fetch(`${this.baseUrl}/api/snapshots/batch`, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: this.getHeaders(),
|
|
99
|
+
body: JSON.stringify({ snapshots }),
|
|
100
|
+
});
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
const text = await response.text();
|
|
103
|
+
this.logger.error('Failed to send snapshots batch:', response.status, text);
|
|
104
|
+
throw new Error(`Failed to send snapshots batch: ${response.status}`);
|
|
105
|
+
}
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
this.logger.log('Snapshots batch sent, count:', data.count);
|
|
108
|
+
return { count: data.count };
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
this.logger.error('Error sending snapshots batch:', error);
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async sendCrashReport(crash) {
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch(`${this.baseUrl}/api/crashes`, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: this.getHeaders(),
|
|
120
|
+
body: JSON.stringify(crash),
|
|
121
|
+
});
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
const text = await response.text();
|
|
124
|
+
this.logger.error('Failed to send crash report:', response.status, text);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.logger.log('Crash report sent:', crash.type, crash.message);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
// Fire-and-forget: catch errors silently, never throw
|
|
131
|
+
this.logger.error('Error sending crash report:', error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async sendCrashesBatch(crashes) {
|
|
135
|
+
try {
|
|
136
|
+
const response = await fetch(`${this.baseUrl}/api/crashes/batch`, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: this.getHeaders(),
|
|
139
|
+
body: JSON.stringify({ crashes }),
|
|
140
|
+
});
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
const text = await response.text();
|
|
143
|
+
this.logger.error('Failed to send crashes batch:', response.status, text);
|
|
144
|
+
throw new Error(`Failed to send crashes batch: ${response.status}`);
|
|
145
|
+
}
|
|
146
|
+
const data = await response.json();
|
|
147
|
+
this.logger.log('Crashes batch sent, count:', data.count);
|
|
148
|
+
return { count: data.count };
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
this.logger.error('Error sending crashes batch:', error);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
exports.ApiClient = ApiClient;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare class BatchQueue<T> {
|
|
2
|
+
private buffer;
|
|
3
|
+
private maxSize;
|
|
4
|
+
private maxBufferSize;
|
|
5
|
+
private flushIntervalMs;
|
|
6
|
+
private onFlush;
|
|
7
|
+
private intervalId;
|
|
8
|
+
private flushing;
|
|
9
|
+
private consecutiveFailures;
|
|
10
|
+
private ticksToSkip;
|
|
11
|
+
private static readonly MAX_BACKOFF_TICKS;
|
|
12
|
+
constructor(maxSize: number | undefined, flushIntervalMs: number | undefined, onFlush: (items: T[]) => Promise<void>, maxBufferSize?: number);
|
|
13
|
+
add(item: T): void;
|
|
14
|
+
flush(): Promise<void>;
|
|
15
|
+
startAutoFlush(): void;
|
|
16
|
+
stopAutoFlush(): void;
|
|
17
|
+
get pending(): number;
|
|
18
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BatchQueue = void 0;
|
|
4
|
+
class BatchQueue {
|
|
5
|
+
constructor(maxSize = 20, flushIntervalMs = 30000, onFlush, maxBufferSize = 1000) {
|
|
6
|
+
this.buffer = [];
|
|
7
|
+
this.intervalId = null;
|
|
8
|
+
this.flushing = false;
|
|
9
|
+
// Exponential backoff state: on each failure we double the skip count,
|
|
10
|
+
// capped at 8 ticks. A successful flush resets it to 0.
|
|
11
|
+
this.consecutiveFailures = 0;
|
|
12
|
+
this.ticksToSkip = 0;
|
|
13
|
+
this.maxSize = maxSize;
|
|
14
|
+
this.flushIntervalMs = flushIntervalMs;
|
|
15
|
+
this.onFlush = onFlush;
|
|
16
|
+
this.maxBufferSize = maxBufferSize;
|
|
17
|
+
}
|
|
18
|
+
add(item) {
|
|
19
|
+
// Evict oldest items if buffer is at max capacity
|
|
20
|
+
if (this.buffer.length >= this.maxBufferSize) {
|
|
21
|
+
this.buffer.splice(0, this.buffer.length - this.maxBufferSize + 1);
|
|
22
|
+
}
|
|
23
|
+
this.buffer.push(item);
|
|
24
|
+
if (this.buffer.length >= this.maxSize) {
|
|
25
|
+
this.flush();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async flush() {
|
|
29
|
+
if (this.buffer.length === 0 || this.flushing) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.flushing = true;
|
|
33
|
+
const items = [...this.buffer];
|
|
34
|
+
try {
|
|
35
|
+
await this.onFlush(items);
|
|
36
|
+
// Only clear the items that were successfully flushed.
|
|
37
|
+
// New items might have been added during the async flush,
|
|
38
|
+
// so we splice out only the count we sent.
|
|
39
|
+
this.buffer.splice(0, items.length);
|
|
40
|
+
// Successful flush resets the backoff state.
|
|
41
|
+
this.consecutiveFailures = 0;
|
|
42
|
+
this.ticksToSkip = 0;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// On failure, keep events in buffer so they can be retried.
|
|
46
|
+
// Apply exponential backoff: 1, 2, 4, 8 ticks skipped before
|
|
47
|
+
// the next auto-flush attempt.
|
|
48
|
+
this.consecutiveFailures++;
|
|
49
|
+
this.ticksToSkip = Math.min(Math.pow(2, this.consecutiveFailures - 1), BatchQueue.MAX_BACKOFF_TICKS);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
this.flushing = false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
startAutoFlush() {
|
|
56
|
+
if (this.intervalId !== null) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
this.intervalId = setInterval(() => {
|
|
60
|
+
// Honor backoff: skip this tick if we're still in a cooldown from
|
|
61
|
+
// a previous failure.
|
|
62
|
+
if (this.ticksToSkip > 0) {
|
|
63
|
+
this.ticksToSkip--;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
this.flush();
|
|
67
|
+
}, this.flushIntervalMs);
|
|
68
|
+
}
|
|
69
|
+
stopAutoFlush() {
|
|
70
|
+
if (this.intervalId !== null) {
|
|
71
|
+
clearInterval(this.intervalId);
|
|
72
|
+
this.intervalId = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
get pending() {
|
|
76
|
+
return this.buffer.length;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.BatchQueue = BatchQueue;
|
|
80
|
+
BatchQueue.MAX_BACKOFF_TICKS = 8;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface CrashReport {
|
|
2
|
+
sessionId?: string;
|
|
3
|
+
type: 'js_error' | 'native_crash' | 'unhandled_rejection';
|
|
4
|
+
message: string;
|
|
5
|
+
stackTrace?: string;
|
|
6
|
+
screenName?: string;
|
|
7
|
+
deviceInfo?: Record<string, any>;
|
|
8
|
+
metadata?: Record<string, any>;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
}
|