@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,225 @@
|
|
|
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
|
+
|
|
6
|
+
export interface SessionResponse {
|
|
7
|
+
sessionId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ApiClient {
|
|
11
|
+
private baseUrl: string;
|
|
12
|
+
private apiKey: string;
|
|
13
|
+
private logger: Logger;
|
|
14
|
+
|
|
15
|
+
constructor(baseUrl: string, apiKey: string, logger: Logger) {
|
|
16
|
+
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
17
|
+
this.apiKey = apiKey;
|
|
18
|
+
this.logger = logger;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private getHeaders(): Record<string, string> {
|
|
22
|
+
return {
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
'x-api-key': this.apiKey,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async startSession(
|
|
29
|
+
deviceId: string,
|
|
30
|
+
deviceInfo?: DeviceInfo,
|
|
31
|
+
): Promise<{ id: string }> {
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(`${this.baseUrl}/api/sessions/start`, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: this.getHeaders(),
|
|
36
|
+
body: JSON.stringify({ deviceId, deviceInfo }),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
const text = await response.text();
|
|
41
|
+
this.logger.error('Failed to start session:', response.status, text);
|
|
42
|
+
throw new Error(`Failed to start session: ${response.status}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const data: SessionResponse = await response.json();
|
|
46
|
+
this.logger.log('Session started:', data.sessionId);
|
|
47
|
+
return { id: data.sessionId };
|
|
48
|
+
} catch (error) {
|
|
49
|
+
this.logger.error('Error starting session:', error);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async endSession(sessionId: string): Promise<void> {
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch(
|
|
57
|
+
`${this.baseUrl}/api/sessions/${sessionId}/end`,
|
|
58
|
+
{
|
|
59
|
+
method: 'PATCH',
|
|
60
|
+
headers: this.getHeaders(),
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const text = await response.text();
|
|
66
|
+
this.logger.error('Failed to end session:', response.status, text);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.logger.log('Session ended:', sessionId);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
this.logger.error('Error ending session:', error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async sendEventsBatch(
|
|
77
|
+
sessionId: string,
|
|
78
|
+
events: AnalyticsEvent[],
|
|
79
|
+
): Promise<{ count: number }> {
|
|
80
|
+
try {
|
|
81
|
+
const response = await fetch(`${this.baseUrl}/api/events/batch`, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: this.getHeaders(),
|
|
84
|
+
body: JSON.stringify({ sessionId, events }),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
const text = await response.text();
|
|
89
|
+
this.logger.error(
|
|
90
|
+
'Failed to send events batch:',
|
|
91
|
+
response.status,
|
|
92
|
+
text,
|
|
93
|
+
);
|
|
94
|
+
throw new Error(`Failed to send events batch: ${response.status}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const data = await response.json();
|
|
98
|
+
this.logger.log('Events batch sent, count:', data.count);
|
|
99
|
+
return { count: data.count };
|
|
100
|
+
} catch (error) {
|
|
101
|
+
this.logger.error('Error sending events batch:', error);
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async sendScreenVisitsBatch(visits: ScreenVisit[]): Promise<void> {
|
|
107
|
+
try {
|
|
108
|
+
const response = await fetch(
|
|
109
|
+
`${this.baseUrl}/api/screens/visit/batch`,
|
|
110
|
+
{
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: this.getHeaders(),
|
|
113
|
+
body: JSON.stringify({ visits }),
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
const text = await response.text();
|
|
119
|
+
this.logger.error(
|
|
120
|
+
'Failed to send screen visits batch:',
|
|
121
|
+
response.status,
|
|
122
|
+
text,
|
|
123
|
+
);
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Failed to send screen visits batch: ${response.status}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.logger.log('Screen visits batch sent, count:', visits.length);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
this.logger.error('Error sending screen visits batch:', error);
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async sendSnapshotsBatch(
|
|
137
|
+
snapshots: SnapshotPayload[],
|
|
138
|
+
): Promise<{ count: number }> {
|
|
139
|
+
try {
|
|
140
|
+
const response = await fetch(
|
|
141
|
+
`${this.baseUrl}/api/snapshots/batch`,
|
|
142
|
+
{
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: this.getHeaders(),
|
|
145
|
+
body: JSON.stringify({ snapshots }),
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
const text = await response.text();
|
|
151
|
+
this.logger.error(
|
|
152
|
+
'Failed to send snapshots batch:',
|
|
153
|
+
response.status,
|
|
154
|
+
text,
|
|
155
|
+
);
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Failed to send snapshots batch: ${response.status}`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const data = await response.json();
|
|
162
|
+
this.logger.log('Snapshots batch sent, count:', data.count);
|
|
163
|
+
return { count: data.count };
|
|
164
|
+
} catch (error) {
|
|
165
|
+
this.logger.error('Error sending snapshots batch:', error);
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async sendCrashReport(crash: CrashReport): Promise<void> {
|
|
171
|
+
try {
|
|
172
|
+
const response = await fetch(`${this.baseUrl}/api/crashes`, {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: this.getHeaders(),
|
|
175
|
+
body: JSON.stringify(crash),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
const text = await response.text();
|
|
180
|
+
this.logger.error(
|
|
181
|
+
'Failed to send crash report:',
|
|
182
|
+
response.status,
|
|
183
|
+
text,
|
|
184
|
+
);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.logger.log('Crash report sent:', crash.type, crash.message);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
// Fire-and-forget: catch errors silently, never throw
|
|
191
|
+
this.logger.error('Error sending crash report:', error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async sendCrashesBatch(
|
|
196
|
+
crashes: CrashReport[],
|
|
197
|
+
): Promise<{ count: number }> {
|
|
198
|
+
try {
|
|
199
|
+
const response = await fetch(`${this.baseUrl}/api/crashes/batch`, {
|
|
200
|
+
method: 'POST',
|
|
201
|
+
headers: this.getHeaders(),
|
|
202
|
+
body: JSON.stringify({ crashes }),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
const text = await response.text();
|
|
207
|
+
this.logger.error(
|
|
208
|
+
'Failed to send crashes batch:',
|
|
209
|
+
response.status,
|
|
210
|
+
text,
|
|
211
|
+
);
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Failed to send crashes batch: ${response.status}`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const data = await response.json();
|
|
218
|
+
this.logger.log('Crashes batch sent, count:', data.count);
|
|
219
|
+
return { count: data.count };
|
|
220
|
+
} catch (error) {
|
|
221
|
+
this.logger.error('Error sending crashes batch:', error);
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export class BatchQueue<T> {
|
|
2
|
+
private buffer: T[] = [];
|
|
3
|
+
private maxSize: number;
|
|
4
|
+
private maxBufferSize: number;
|
|
5
|
+
private flushIntervalMs: number;
|
|
6
|
+
private onFlush: (items: T[]) => Promise<void>;
|
|
7
|
+
private intervalId: ReturnType<typeof setInterval> | null = null;
|
|
8
|
+
private flushing: boolean = 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
|
+
private consecutiveFailures: number = 0;
|
|
12
|
+
private ticksToSkip: number = 0;
|
|
13
|
+
private static readonly MAX_BACKOFF_TICKS = 8;
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
maxSize: number = 20,
|
|
17
|
+
flushIntervalMs: number = 30000,
|
|
18
|
+
onFlush: (items: T[]) => Promise<void>,
|
|
19
|
+
maxBufferSize: number = 1000,
|
|
20
|
+
) {
|
|
21
|
+
this.maxSize = maxSize;
|
|
22
|
+
this.flushIntervalMs = flushIntervalMs;
|
|
23
|
+
this.onFlush = onFlush;
|
|
24
|
+
this.maxBufferSize = maxBufferSize;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
add(item: T): void {
|
|
28
|
+
// Evict oldest items if buffer is at max capacity
|
|
29
|
+
if (this.buffer.length >= this.maxBufferSize) {
|
|
30
|
+
this.buffer.splice(0, this.buffer.length - this.maxBufferSize + 1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.buffer.push(item);
|
|
34
|
+
if (this.buffer.length >= this.maxSize) {
|
|
35
|
+
this.flush();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async flush(): Promise<void> {
|
|
40
|
+
if (this.buffer.length === 0 || this.flushing) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.flushing = true;
|
|
45
|
+
const items = [...this.buffer];
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
await this.onFlush(items);
|
|
49
|
+
// Only clear the items that were successfully flushed.
|
|
50
|
+
// New items might have been added during the async flush,
|
|
51
|
+
// so we splice out only the count we sent.
|
|
52
|
+
this.buffer.splice(0, items.length);
|
|
53
|
+
// Successful flush resets the backoff state.
|
|
54
|
+
this.consecutiveFailures = 0;
|
|
55
|
+
this.ticksToSkip = 0;
|
|
56
|
+
} catch {
|
|
57
|
+
// On failure, keep events in buffer so they can be retried.
|
|
58
|
+
// Apply exponential backoff: 1, 2, 4, 8 ticks skipped before
|
|
59
|
+
// the next auto-flush attempt.
|
|
60
|
+
this.consecutiveFailures++;
|
|
61
|
+
this.ticksToSkip = Math.min(
|
|
62
|
+
Math.pow(2, this.consecutiveFailures - 1),
|
|
63
|
+
BatchQueue.MAX_BACKOFF_TICKS,
|
|
64
|
+
);
|
|
65
|
+
} finally {
|
|
66
|
+
this.flushing = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
startAutoFlush(): void {
|
|
71
|
+
if (this.intervalId !== null) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.intervalId = setInterval(() => {
|
|
75
|
+
// Honor backoff: skip this tick if we're still in a cooldown from
|
|
76
|
+
// a previous failure.
|
|
77
|
+
if (this.ticksToSkip > 0) {
|
|
78
|
+
this.ticksToSkip--;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
this.flush();
|
|
82
|
+
}, this.flushIntervalMs);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
stopAutoFlush(): void {
|
|
86
|
+
if (this.intervalId !== null) {
|
|
87
|
+
clearInterval(this.intervalId);
|
|
88
|
+
this.intervalId = null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get pending(): number {
|
|
93
|
+
return this.buffer.length;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type EventType = 'touch' | 'scroll' | 'gesture' | 'navigation' | 'custom';
|
|
2
|
+
|
|
3
|
+
export interface AnalyticsEvent {
|
|
4
|
+
type: EventType;
|
|
5
|
+
screenName?: string;
|
|
6
|
+
data: Record<string, any>;
|
|
7
|
+
timestamp: string; // ISO 8601
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TouchEventData {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
elementTag?: string;
|
|
14
|
+
elementText?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface NavigationEventData {
|
|
18
|
+
fromScreen: string;
|
|
19
|
+
toScreen: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ScreenVisit {
|
|
23
|
+
sessionId: string;
|
|
24
|
+
screenName: string;
|
|
25
|
+
enteredAt: string;
|
|
26
|
+
exitedAt?: string;
|
|
27
|
+
duration?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DeviceInfo {
|
|
31
|
+
os: string;
|
|
32
|
+
osVersion: string;
|
|
33
|
+
model: string;
|
|
34
|
+
brand: string;
|
|
35
|
+
screenWidth: number;
|
|
36
|
+
screenHeight: number;
|
|
37
|
+
appVersion?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SDKConfig {
|
|
41
|
+
apiKey: string;
|
|
42
|
+
appId: string;
|
|
43
|
+
baseUrl: string;
|
|
44
|
+
batchSize?: number;
|
|
45
|
+
flushInterval?: number;
|
|
46
|
+
enableTouchTracking?: boolean;
|
|
47
|
+
enableNavigationTracking?: boolean;
|
|
48
|
+
enableSnapshots?: boolean;
|
|
49
|
+
snapshotInterval?: number;
|
|
50
|
+
maxSnapshotsPerSession?: number;
|
|
51
|
+
/** Max snapshots per network batch. Defaults to 8. */
|
|
52
|
+
snapshotBatchSize?: number;
|
|
53
|
+
/** Auto-flush interval for the snapshot queue (ms). Defaults to 15000. */
|
|
54
|
+
snapshotFlushInterval?: number;
|
|
55
|
+
/** Max snapshots kept in memory if the backend is unreachable. Defaults to 32. */
|
|
56
|
+
snapshotMaxBufferSize?: number;
|
|
57
|
+
enableCrashTracking?: boolean;
|
|
58
|
+
debug?: boolean;
|
|
59
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repliqo snapshot types.
|
|
3
|
+
*
|
|
4
|
+
* The SDK captures JPEG screenshots of the view hierarchy via
|
|
5
|
+
* react-native-view-shot. Each snapshot is stored as a base64 JPEG
|
|
6
|
+
* together with its rendered dimensions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface ScreenshotData {
|
|
10
|
+
image: string;
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
format: 'jpeg';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SnapshotPayload {
|
|
17
|
+
sessionId: string;
|
|
18
|
+
screenName: string;
|
|
19
|
+
timestamp: string;
|
|
20
|
+
sequence: number;
|
|
21
|
+
isDiff: boolean;
|
|
22
|
+
data: ScreenshotData;
|
|
23
|
+
}
|