@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.
Files changed (58) hide show
  1. package/INTEGRATION_GUIDE.md +1312 -0
  2. package/android/build.gradle +24 -0
  3. package/android/src/main/AndroidManifest.xml +4 -0
  4. package/android/src/main/java/com/repliqo/screencapture/MultiWindowCapture.java +230 -0
  5. package/android/src/main/java/com/repliqo/screencapture/ScreenCaptureModule.java +94 -0
  6. package/android/src/main/java/com/repliqo/screencapture/ScreenCapturePackage.java +38 -0
  7. package/dist/components/ScreenCapturePermission.d.ts +55 -0
  8. package/dist/components/ScreenCapturePermission.js +112 -0
  9. package/dist/core/client.d.ts +41 -0
  10. package/dist/core/client.js +309 -0
  11. package/dist/core/config.d.ts +4 -0
  12. package/dist/core/config.js +26 -0
  13. package/dist/core/logger.d.ts +8 -0
  14. package/dist/core/logger.js +25 -0
  15. package/dist/index.d.ts +15 -0
  16. package/dist/index.js +28 -0
  17. package/dist/snapshot/NativeScreenCapture.d.ts +17 -0
  18. package/dist/snapshot/NativeScreenCapture.js +38 -0
  19. package/dist/snapshot/capture.d.ts +36 -0
  20. package/dist/snapshot/capture.js +109 -0
  21. package/dist/snapshot/captureScreenshot.d.ts +26 -0
  22. package/dist/snapshot/captureScreenshot.js +89 -0
  23. package/dist/trackers/error.tracker.d.ts +22 -0
  24. package/dist/trackers/error.tracker.js +123 -0
  25. package/dist/trackers/navigation.tracker.d.ts +6 -0
  26. package/dist/trackers/navigation.tracker.js +78 -0
  27. package/dist/trackers/screen.tracker.d.ts +2 -0
  28. package/dist/trackers/screen.tracker.js +23 -0
  29. package/dist/trackers/touch.tracker.d.ts +8 -0
  30. package/dist/trackers/touch.tracker.js +30 -0
  31. package/dist/transport/api.client.d.ts +29 -0
  32. package/dist/transport/api.client.js +156 -0
  33. package/dist/transport/batch-queue.d.ts +18 -0
  34. package/dist/transport/batch-queue.js +80 -0
  35. package/dist/types/crash.d.ts +10 -0
  36. package/dist/types/crash.js +2 -0
  37. package/dist/types/events.d.ts +53 -0
  38. package/dist/types/events.js +2 -0
  39. package/dist/types/snapshot.d.ts +21 -0
  40. package/dist/types/snapshot.js +9 -0
  41. package/package.json +64 -0
  42. package/src/components/ScreenCapturePermission.tsx +160 -0
  43. package/src/core/client.ts +425 -0
  44. package/src/core/config.ts +27 -0
  45. package/src/core/logger.ts +27 -0
  46. package/src/index.ts +47 -0
  47. package/src/snapshot/NativeScreenCapture.ts +54 -0
  48. package/src/snapshot/capture.ts +142 -0
  49. package/src/snapshot/captureScreenshot.ts +109 -0
  50. package/src/trackers/error.tracker.ts +136 -0
  51. package/src/trackers/navigation.tracker.ts +98 -0
  52. package/src/trackers/screen.tracker.ts +19 -0
  53. package/src/trackers/touch.tracker.tsx +44 -0
  54. package/src/transport/api.client.ts +225 -0
  55. package/src/transport/batch-queue.ts +95 -0
  56. package/src/types/crash.ts +10 -0
  57. package/src/types/events.ts +59 -0
  58. 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,6 @@
1
+ export declare function useNavigationTracker(): {
2
+ navigationRef: import("react").MutableRefObject<any>;
3
+ onStateChange: () => void;
4
+ onReady: () => void;
5
+ };
6
+ export declare function trackScreenChange(fromScreen: string, toScreen: string): void;
@@ -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,2 @@
1
+ export declare function trackScreenEnter(screenName: string): void;
2
+ export declare function trackScreenExit(screenName: string): void;
@@ -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,8 @@
1
+ import React from 'react';
2
+ interface TouchTrackerProps {
3
+ children: React.ReactNode;
4
+ screenName?: string;
5
+ enabled?: boolean;
6
+ }
7
+ export declare const TouchTracker: React.FC<TouchTrackerProps>;
8
+ export {};
@@ -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
+ }