@repliqo/sdk-react-native 0.3.8 → 0.4.1

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 (42) hide show
  1. package/INTEGRATION_GUIDE.md +1384 -1312
  2. package/android/src/main/java/com/repliqo/screencapture/MultiWindowCapture.java +242 -242
  3. package/android/src/main/java/com/repliqo/screencapture/ScreenCaptureModule.java +94 -94
  4. package/dist/components/RepliqoFlatList.d.ts +5 -0
  5. package/dist/components/RepliqoFlatList.js +39 -0
  6. package/dist/components/RepliqoScrollView.d.ts +3 -0
  7. package/dist/components/RepliqoScrollView.js +16 -265
  8. package/dist/components/useRepliqoScrollTracking.d.ts +33 -0
  9. package/dist/components/useRepliqoScrollTracking.js +304 -0
  10. package/dist/core/client.d.ts +52 -0
  11. package/dist/core/client.js +304 -16
  12. package/dist/core/config.d.ts +2 -2
  13. package/dist/core/config.js +8 -2
  14. package/dist/core/storage.d.ts +29 -0
  15. package/dist/core/storage.js +33 -0
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.js +3 -1
  18. package/dist/snapshot/capture.d.ts +7 -0
  19. package/dist/snapshot/capture.js +17 -5
  20. package/dist/trackers/error.tracker.d.ts +25 -0
  21. package/dist/trackers/error.tracker.js +114 -37
  22. package/dist/trackers/navigation.tracker.js +11 -0
  23. package/dist/transport/api.client.d.ts +34 -1
  24. package/dist/transport/api.client.js +102 -19
  25. package/dist/transport/batch-queue.d.ts +53 -1
  26. package/dist/transport/batch-queue.js +126 -19
  27. package/dist/types/events.d.ts +12 -0
  28. package/package.json +68 -64
  29. package/src/components/RepliqoFlatList.tsx +64 -0
  30. package/src/components/RepliqoScrollView.tsx +53 -302
  31. package/src/components/useRepliqoScrollTracking.ts +366 -0
  32. package/src/core/client.ts +953 -628
  33. package/src/core/config.ts +40 -30
  34. package/src/core/storage.ts +51 -0
  35. package/src/index.ts +54 -50
  36. package/src/snapshot/NativeScreenCapture.ts +62 -62
  37. package/src/snapshot/capture.ts +154 -142
  38. package/src/trackers/error.tracker.ts +211 -136
  39. package/src/trackers/navigation.tracker.ts +107 -98
  40. package/src/transport/api.client.ts +430 -327
  41. package/src/transport/batch-queue.ts +212 -95
  42. package/src/types/events.ts +72 -60
@@ -1,136 +1,211 @@
1
- import { CrashReport } from '../types/crash';
2
-
3
- type CrashCallback = (crash: CrashReport) => void;
4
-
5
- export class ErrorTracker {
6
- private onCrash: CrashCallback;
7
- private sessionId: string | null = null;
8
- private currentScreen: string | null = null;
9
- private originalHandler:
10
- | ((error: Error, isFatal?: boolean) => void)
11
- | null = null;
12
- private isActive = false;
13
-
14
- constructor(onCrash: CrashCallback) {
15
- this.onCrash = onCrash;
16
- }
17
-
18
- setSessionId(id: string | null): void {
19
- this.sessionId = id;
20
- }
21
-
22
- setCurrentScreen(screen: string | null): void {
23
- this.currentScreen = screen;
24
- }
25
-
26
- start(): void {
27
- if (this.isActive) return;
28
- this.isActive = true;
29
- this.setupGlobalErrorHandler();
30
- this.setupUnhandledRejectionHandler();
31
- }
32
-
33
- stop(): void {
34
- if (!this.isActive) return;
35
- this.isActive = false;
36
- this.restoreGlobalErrorHandler();
37
- }
38
-
39
- /**
40
- * Manual error reporting - allows consumers to report caught errors.
41
- */
42
- reportError(error: Error, metadata?: Record<string, any>): void {
43
- try {
44
- const crash: CrashReport = {
45
- sessionId: this.sessionId || undefined,
46
- type: 'js_error',
47
- message: error.message,
48
- stackTrace: error.stack || undefined,
49
- screenName: this.currentScreen || undefined,
50
- metadata,
51
- timestamp: new Date().toISOString(),
52
- };
53
- this.onCrash(crash);
54
- } catch (_e) {
55
- // Never crash the app due to error reporting
56
- }
57
- }
58
-
59
- private setupGlobalErrorHandler(): void {
60
- try {
61
- const ErrorUtils = (global as any).ErrorUtils;
62
- if (ErrorUtils) {
63
- this.originalHandler = ErrorUtils.getGlobalHandler();
64
- ErrorUtils.setGlobalHandler(
65
- (error: Error, isFatal?: boolean) => {
66
- try {
67
- const crash: CrashReport = {
68
- sessionId: this.sessionId || undefined,
69
- type: isFatal ? 'native_crash' : 'js_error',
70
- message: error.message,
71
- stackTrace: error.stack || undefined,
72
- screenName: this.currentScreen || undefined,
73
- metadata: { isFatal },
74
- timestamp: new Date().toISOString(),
75
- };
76
- this.onCrash(crash);
77
- } catch (_e) {
78
- // Never crash the app due to error reporting
79
- }
80
-
81
- // Always call original handler so RN can handle the error
82
- if (this.originalHandler) {
83
- this.originalHandler(error, isFatal);
84
- }
85
- },
86
- );
87
- }
88
- } catch (_e) {
89
- // Never crash the app due to error handler setup
90
- }
91
- }
92
-
93
- private setupUnhandledRejectionHandler(): void {
94
- try {
95
- if (typeof global !== 'undefined') {
96
- (global as any).__analyticsUnhandledRejection = (
97
- _id: number,
98
- error: any,
99
- ) => {
100
- try {
101
- const message =
102
- error instanceof Error ? error.message : String(error);
103
- const stack =
104
- error instanceof Error ? error.stack : undefined;
105
-
106
- const crash: CrashReport = {
107
- sessionId: this.sessionId || undefined,
108
- type: 'unhandled_rejection',
109
- message,
110
- stackTrace: stack || undefined,
111
- screenName: this.currentScreen || undefined,
112
- timestamp: new Date().toISOString(),
113
- };
114
- this.onCrash(crash);
115
- } catch (_e) {
116
- // Never crash the app due to error reporting
117
- }
118
- };
119
- }
120
- } catch (_e) {
121
- // Never crash the app due to rejection handler setup
122
- }
123
- }
124
-
125
- private restoreGlobalErrorHandler(): void {
126
- try {
127
- const ErrorUtils = (global as any).ErrorUtils;
128
- if (ErrorUtils && this.originalHandler) {
129
- ErrorUtils.setGlobalHandler(this.originalHandler);
130
- this.originalHandler = null;
131
- }
132
- } catch (_e) {
133
- // Never crash the app due to handler restoration
134
- }
135
- }
136
- }
1
+ import { CrashReport } from '../types/crash';
2
+
3
+ type CrashCallback = (crash: CrashReport) => void;
4
+
5
+ /* Dynamic require for React Native's optional promise internals. */
6
+ declare const require: any;
7
+
8
+ export class ErrorTracker {
9
+ private onCrash: CrashCallback;
10
+ private sessionId: string | null = null;
11
+ private currentScreen: string | null = null;
12
+ private originalHandler:
13
+ | ((error: Error, isFatal?: boolean) => void)
14
+ | null = null;
15
+ private wrapperHandler:
16
+ | ((error: Error, isFatal?: boolean) => void)
17
+ | null = null;
18
+ private isActive = false;
19
+
20
+ constructor(onCrash: CrashCallback) {
21
+ this.onCrash = onCrash;
22
+ }
23
+
24
+ setSessionId(id: string | null): void {
25
+ this.sessionId = id;
26
+ }
27
+
28
+ setCurrentScreen(screen: string | null): void {
29
+ this.currentScreen = screen;
30
+ }
31
+
32
+ start(): void {
33
+ if (this.isActive) return;
34
+ this.isActive = true;
35
+ this.setupGlobalErrorHandler();
36
+ this.setupUnhandledRejectionHandler();
37
+ }
38
+
39
+ stop(): void {
40
+ if (!this.isActive) return;
41
+ // Flip the flag FIRST — the rejection trackers can't always be
42
+ // unregistered (Hermes), so their callbacks guard on isActive.
43
+ this.isActive = false;
44
+ this.restoreGlobalErrorHandler();
45
+ this.teardownUnhandledRejectionHandler();
46
+ }
47
+
48
+ /**
49
+ * Manual error reporting - allows consumers to report caught errors.
50
+ */
51
+ reportError(error: Error, metadata?: Record<string, any>): void {
52
+ try {
53
+ const crash: CrashReport = {
54
+ sessionId: this.sessionId || undefined,
55
+ type: 'js_error',
56
+ message: error.message,
57
+ stackTrace: error.stack || undefined,
58
+ screenName: this.currentScreen || undefined,
59
+ metadata,
60
+ timestamp: new Date().toISOString(),
61
+ };
62
+ this.onCrash(crash);
63
+ } catch (_e) {
64
+ // Never crash the app due to error reporting
65
+ }
66
+ }
67
+
68
+ private setupGlobalErrorHandler(): void {
69
+ try {
70
+ const ErrorUtils = (global as any).ErrorUtils;
71
+ if (ErrorUtils) {
72
+ this.originalHandler = ErrorUtils.getGlobalHandler();
73
+ this.wrapperHandler = (error: Error, isFatal?: boolean) => {
74
+ if (this.isActive) {
75
+ try {
76
+ const crash: CrashReport = {
77
+ sessionId: this.sessionId || undefined,
78
+ type: isFatal ? 'native_crash' : 'js_error',
79
+ message: error.message,
80
+ stackTrace: error.stack || undefined,
81
+ screenName: this.currentScreen || undefined,
82
+ metadata: { isFatal },
83
+ timestamp: new Date().toISOString(),
84
+ };
85
+ this.onCrash(crash);
86
+ } catch (_e) {
87
+ // Never crash the app due to error reporting
88
+ }
89
+ }
90
+
91
+ // Always call original handler so RN can handle the error
92
+ if (this.originalHandler) {
93
+ this.originalHandler(error, isFatal);
94
+ }
95
+ };
96
+ ErrorUtils.setGlobalHandler(this.wrapperHandler);
97
+ }
98
+ } catch (_e) {
99
+ // Never crash the app due to error handler setup
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Handle an unhandled promise rejection. Guarded on isActive because the
105
+ * underlying trackers can't always be unregistered.
106
+ */
107
+ private handleUnhandledRejection = (_id: number, error: any): void => {
108
+ if (!this.isActive) return;
109
+ try {
110
+ const message = error instanceof Error ? error.message : String(error);
111
+ const stack = error instanceof Error ? error.stack : undefined;
112
+
113
+ const crash: CrashReport = {
114
+ sessionId: this.sessionId || undefined,
115
+ type: 'unhandled_rejection',
116
+ message,
117
+ stackTrace: stack || undefined,
118
+ screenName: this.currentScreen || undefined,
119
+ timestamp: new Date().toISOString(),
120
+ };
121
+ this.onCrash(crash);
122
+ } catch (_e) {
123
+ // Never crash the app due to error reporting
124
+ }
125
+ };
126
+
127
+ /**
128
+ * Register with the mechanisms React Native ACTUALLY uses to surface
129
+ * unhandled promise rejections:
130
+ *
131
+ * 1. Hermes' native promise rejection tracker (Hermes engine).
132
+ * 2. The `promise` polyfill's rejection-tracking module (JSC and older
133
+ * RN versions).
134
+ *
135
+ * A bare global callback (the old approach) is never invoked by RN — it
136
+ * silently captured nothing.
137
+ *
138
+ * KNOWN LIMITATION: these trackers hold a single handler — registering
139
+ * REPLACES whatever was installed before (RN's dev LogBox warning, or
140
+ * another SDK like Sentry if it initialized first), and there is no
141
+ * unregister API, so after stop() an inert handler remains installed.
142
+ * If you run multiple error-tracking SDKs, initialize Repliqo FIRST so
143
+ * the other SDK's handler wins, or disable enableCrashTracking.
144
+ */
145
+ private setupUnhandledRejectionHandler(): void {
146
+ // Kept for manual invocation / test hooks.
147
+ try {
148
+ (global as any).__analyticsUnhandledRejection =
149
+ this.handleUnhandledRejection;
150
+ } catch (_e) {
151
+ /* ignore */
152
+ }
153
+
154
+ // Hermes: native promise rejection tracking.
155
+ try {
156
+ const hermes = (global as any).HermesInternal;
157
+ if (hermes?.hasPromise?.() && hermes?.enablePromiseRejectionTracker) {
158
+ hermes.enablePromiseRejectionTracker({
159
+ allRejections: true,
160
+ onUnhandled: this.handleUnhandledRejection,
161
+ onHandled: () => {},
162
+ });
163
+ return;
164
+ }
165
+ } catch (_e) {
166
+ /* ignore — fall through to the polyfill */
167
+ }
168
+
169
+ // JSC / promise polyfill path.
170
+ try {
171
+ const rejectionTracking = require('promise/setimmediate/rejection-tracking');
172
+ rejectionTracking.enable({
173
+ allRejections: true,
174
+ onUnhandled: this.handleUnhandledRejection,
175
+ onHandled: () => {},
176
+ });
177
+ } catch (_e) {
178
+ // Module unavailable (e.g. jest without the polyfill) — the global
179
+ // hook above still allows manual reporting.
180
+ }
181
+ }
182
+
183
+ private teardownUnhandledRejectionHandler(): void {
184
+ try {
185
+ delete (global as any).__analyticsUnhandledRejection;
186
+ } catch (_e) {
187
+ /* ignore */
188
+ }
189
+ // Hermes offers no unregister API and disabling the polyfill tracker
190
+ // could clobber another library's handler — the isActive guard in
191
+ // handleUnhandledRejection makes lingering registrations inert.
192
+ }
193
+
194
+ private restoreGlobalErrorHandler(): void {
195
+ try {
196
+ const ErrorUtils = (global as any).ErrorUtils;
197
+ if (ErrorUtils && this.originalHandler) {
198
+ // Only restore if WE are still the installed handler — another
199
+ // library may have wrapped us after start(), and restoring would
200
+ // clobber its handler.
201
+ if (ErrorUtils.getGlobalHandler() === this.wrapperHandler) {
202
+ ErrorUtils.setGlobalHandler(this.originalHandler);
203
+ }
204
+ this.originalHandler = null;
205
+ this.wrapperHandler = null;
206
+ }
207
+ } catch (_e) {
208
+ // Never crash the app due to handler restoration
209
+ }
210
+ }
211
+ }
@@ -1,98 +1,107 @@
1
- import { useRef, useCallback } from 'react';
2
- import { AppAnalytics } from '../core/client';
3
-
4
- interface NavigationState {
5
- routes: Array<{ name: string; key: string; state?: NavigationState }>;
6
- index: number;
7
- }
8
-
9
- function getActiveRouteName(state: NavigationState | undefined): string | null {
10
- if (!state) {
11
- return null;
12
- }
13
-
14
- const route = state.routes[state.index];
15
- if (!route) {
16
- return null;
17
- }
18
-
19
- // Dive into nested navigators
20
- if (route.state) {
21
- return getActiveRouteName(route.state as NavigationState);
22
- }
23
-
24
- return route.name;
25
- }
26
-
27
- export function useNavigationTracker() {
28
- const routeNameRef = useRef<string | null>(null);
29
- const navigationRef = useRef<any>(null);
30
-
31
- const onReady = useCallback(() => {
32
- if (navigationRef.current) {
33
- const state = navigationRef.current.getRootState() as
34
- | NavigationState
35
- | undefined;
36
- const currentRouteName = getActiveRouteName(state);
37
- routeNameRef.current = currentRouteName;
38
-
39
- if (currentRouteName) {
40
- try {
41
- const analytics = AppAnalytics.getInstance();
42
- analytics.onScreenEnter(currentRouteName);
43
- } catch {
44
- // AppAnalytics not initialized, silently ignore
45
- }
46
- }
47
- }
48
- }, []);
49
-
50
- const onStateChange = useCallback(() => {
51
- if (!navigationRef.current) {
52
- return;
53
- }
54
-
55
- const state = navigationRef.current.getRootState() as
56
- | NavigationState
57
- | undefined;
58
- const currentRouteName = getActiveRouteName(state);
59
- const previousRouteName = routeNameRef.current;
60
-
61
- if (
62
- currentRouteName &&
63
- previousRouteName &&
64
- currentRouteName !== previousRouteName
65
- ) {
66
- try {
67
- const analytics = AppAnalytics.getInstance();
68
- analytics.trackNavigation(previousRouteName, currentRouteName);
69
- analytics.onScreenExit(previousRouteName);
70
- analytics.onScreenEnter(currentRouteName);
71
- } catch {
72
- // AppAnalytics not initialized, silently ignore
73
- }
74
- }
75
-
76
- routeNameRef.current = currentRouteName;
77
- }, []);
78
-
79
- return {
80
- navigationRef,
81
- onStateChange,
82
- onReady,
83
- };
84
- }
85
-
86
- export function trackScreenChange(
87
- fromScreen: string,
88
- toScreen: string,
89
- ): void {
90
- try {
91
- const analytics = AppAnalytics.getInstance();
92
- analytics.trackNavigation(fromScreen, toScreen);
93
- analytics.onScreenExit(fromScreen);
94
- analytics.onScreenEnter(toScreen);
95
- } catch {
96
- // AppAnalytics not initialized, silently ignore
97
- }
98
- }
1
+ import { useRef, useCallback } from 'react';
2
+ import { AppAnalytics } from '../core/client';
3
+
4
+ interface NavigationState {
5
+ routes: Array<{ name: string; key: string; state?: NavigationState }>;
6
+ index: number;
7
+ }
8
+
9
+ function getActiveRouteName(state: NavigationState | undefined): string | null {
10
+ if (!state) {
11
+ return null;
12
+ }
13
+
14
+ const route = state.routes[state.index];
15
+ if (!route) {
16
+ return null;
17
+ }
18
+
19
+ // Dive into nested navigators
20
+ if (route.state) {
21
+ return getActiveRouteName(route.state as NavigationState);
22
+ }
23
+
24
+ return route.name;
25
+ }
26
+
27
+ export function useNavigationTracker() {
28
+ const routeNameRef = useRef<string | null>(null);
29
+ const navigationRef = useRef<any>(null);
30
+
31
+ const onReady = useCallback(() => {
32
+ if (navigationRef.current) {
33
+ const state = navigationRef.current.getRootState() as
34
+ | NavigationState
35
+ | undefined;
36
+ const currentRouteName = getActiveRouteName(state);
37
+ routeNameRef.current = currentRouteName;
38
+
39
+ if (currentRouteName) {
40
+ try {
41
+ const analytics = AppAnalytics.getInstance();
42
+ analytics.onScreenEnter(currentRouteName);
43
+ } catch {
44
+ // AppAnalytics not initialized, silently ignore
45
+ }
46
+ }
47
+ }
48
+ }, []);
49
+
50
+ const onStateChange = useCallback(() => {
51
+ if (!navigationRef.current) {
52
+ return;
53
+ }
54
+
55
+ const state = navigationRef.current.getRootState() as
56
+ | NavigationState
57
+ | undefined;
58
+ const currentRouteName = getActiveRouteName(state);
59
+ const previousRouteName = routeNameRef.current;
60
+
61
+ if (
62
+ currentRouteName &&
63
+ previousRouteName &&
64
+ currentRouteName !== previousRouteName
65
+ ) {
66
+ try {
67
+ const analytics = AppAnalytics.getInstance();
68
+ analytics.trackNavigation(previousRouteName, currentRouteName);
69
+ analytics.onScreenExit(previousRouteName);
70
+ analytics.onScreenEnter(currentRouteName);
71
+ } catch {
72
+ // AppAnalytics not initialized, silently ignore
73
+ }
74
+ } else if (currentRouteName && !previousRouteName) {
75
+ // First route seen without onReady having fired (some apps only wire
76
+ // onStateChange). Without this, currentScreen stays null until the
77
+ // SECOND navigation and early touches/visits go unattributed.
78
+ try {
79
+ AppAnalytics.getInstance().onScreenEnter(currentRouteName);
80
+ } catch {
81
+ // AppAnalytics not initialized, silently ignore
82
+ }
83
+ }
84
+
85
+ routeNameRef.current = currentRouteName;
86
+ }, []);
87
+
88
+ return {
89
+ navigationRef,
90
+ onStateChange,
91
+ onReady,
92
+ };
93
+ }
94
+
95
+ export function trackScreenChange(
96
+ fromScreen: string,
97
+ toScreen: string,
98
+ ): void {
99
+ try {
100
+ const analytics = AppAnalytics.getInstance();
101
+ analytics.trackNavigation(fromScreen, toScreen);
102
+ analytics.onScreenExit(fromScreen);
103
+ analytics.onScreenEnter(toScreen);
104
+ } catch {
105
+ // AppAnalytics not initialized, silently ignore
106
+ }
107
+ }