@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.
- package/INTEGRATION_GUIDE.md +1384 -1312
- package/android/src/main/java/com/repliqo/screencapture/MultiWindowCapture.java +242 -242
- package/android/src/main/java/com/repliqo/screencapture/ScreenCaptureModule.java +94 -94
- package/dist/components/RepliqoFlatList.d.ts +5 -0
- package/dist/components/RepliqoFlatList.js +39 -0
- package/dist/components/RepliqoScrollView.d.ts +3 -0
- package/dist/components/RepliqoScrollView.js +16 -265
- package/dist/components/useRepliqoScrollTracking.d.ts +33 -0
- package/dist/components/useRepliqoScrollTracking.js +304 -0
- package/dist/core/client.d.ts +52 -0
- package/dist/core/client.js +304 -16
- package/dist/core/config.d.ts +2 -2
- package/dist/core/config.js +8 -2
- package/dist/core/storage.d.ts +29 -0
- package/dist/core/storage.js +33 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/snapshot/capture.d.ts +7 -0
- package/dist/snapshot/capture.js +17 -5
- package/dist/trackers/error.tracker.d.ts +25 -0
- package/dist/trackers/error.tracker.js +114 -37
- package/dist/trackers/navigation.tracker.js +11 -0
- package/dist/transport/api.client.d.ts +34 -1
- package/dist/transport/api.client.js +102 -19
- package/dist/transport/batch-queue.d.ts +53 -1
- package/dist/transport/batch-queue.js +126 -19
- package/dist/types/events.d.ts +12 -0
- package/package.json +68 -64
- package/src/components/RepliqoFlatList.tsx +64 -0
- package/src/components/RepliqoScrollView.tsx +53 -302
- package/src/components/useRepliqoScrollTracking.ts +366 -0
- package/src/core/client.ts +953 -628
- package/src/core/config.ts +40 -30
- package/src/core/storage.ts +51 -0
- package/src/index.ts +54 -50
- package/src/snapshot/NativeScreenCapture.ts +62 -62
- package/src/snapshot/capture.ts +154 -142
- package/src/trackers/error.tracker.ts +211 -136
- package/src/trackers/navigation.tracker.ts +107 -98
- package/src/transport/api.client.ts +430 -327
- package/src/transport/batch-queue.ts +212 -95
- package/src/types/events.ts +72 -60
|
@@ -1,327 +1,430 @@
|
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
this.logger.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
this.logger.log('
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
if (!response.ok) {
|
|
280
|
-
const text = await response.text();
|
|
281
|
-
this.logger.
|
|
282
|
-
'
|
|
283
|
-
response.status,
|
|
284
|
-
text
|
|
285
|
-
);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
} catch (error) {
|
|
324
|
-
this.logger.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
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
|
+
import { NonRetryableFlushError } from './batch-queue';
|
|
6
|
+
|
|
7
|
+
export interface SessionResponse {
|
|
8
|
+
sessionId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Abort any request that hasn't completed within this window. */
|
|
12
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
13
|
+
|
|
14
|
+
export class ApiClient {
|
|
15
|
+
private baseUrl: string;
|
|
16
|
+
private apiKey: string;
|
|
17
|
+
private logger: Logger;
|
|
18
|
+
|
|
19
|
+
constructor(baseUrl: string, apiKey: string, logger: Logger) {
|
|
20
|
+
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
21
|
+
this.apiKey = apiKey;
|
|
22
|
+
this.logger = logger;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private getHeaders(): Record<string, string> {
|
|
26
|
+
return {
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
'x-api-key': this.apiKey,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* fetch with a hard timeout. Without it, a single stalled request keeps
|
|
34
|
+
* the BatchQueue's in-flight lock held indefinitely — no flush can run,
|
|
35
|
+
* the buffer fills up and starts evicting events (silent data loss).
|
|
36
|
+
*/
|
|
37
|
+
private async fetchWithTimeout(
|
|
38
|
+
url: string,
|
|
39
|
+
init: RequestInit,
|
|
40
|
+
): Promise<Response> {
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
43
|
+
try {
|
|
44
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
45
|
+
} finally {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Classify a non-ok batch response. Permanent PAYLOAD errors (the batch
|
|
52
|
+
* itself will never be accepted: 400 malformed, 413 too large, 422
|
|
53
|
+
* unprocessable, 404 gone) surface as NonRetryableFlushError so the
|
|
54
|
+
* queue drops the poison batch instead of retrying it forever.
|
|
55
|
+
*
|
|
56
|
+
* 401/403 are NOT the payload's fault — they mean the API key is wrong
|
|
57
|
+
* or was rotated. Treat them as transient (retry with backoff) so a key
|
|
58
|
+
* rotation doesn't silently destroy every live SDK's buffered data.
|
|
59
|
+
* 408/429 are transient by definition.
|
|
60
|
+
*/
|
|
61
|
+
private batchError(context: string, status: number): Error {
|
|
62
|
+
const message = `${context}: ${status}`;
|
|
63
|
+
const isPermanent =
|
|
64
|
+
status >= 400 &&
|
|
65
|
+
status < 500 &&
|
|
66
|
+
status !== 401 &&
|
|
67
|
+
status !== 403 &&
|
|
68
|
+
status !== 408 &&
|
|
69
|
+
status !== 429;
|
|
70
|
+
return isPermanent
|
|
71
|
+
? new NonRetryableFlushError(message)
|
|
72
|
+
: new Error(message);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parse a 2xx response body, tolerating malformed JSON. The server
|
|
77
|
+
* already accepted the data — throwing here would make the queue retry
|
|
78
|
+
* an already-persisted batch and create duplicates.
|
|
79
|
+
*/
|
|
80
|
+
private async safeJson<T>(response: Response, fallback: T): Promise<T> {
|
|
81
|
+
try {
|
|
82
|
+
return (await response.json()) as T;
|
|
83
|
+
} catch {
|
|
84
|
+
this.logger.warn('Response body was not valid JSON; treating as success');
|
|
85
|
+
return fallback;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async startSession(
|
|
90
|
+
deviceId: string,
|
|
91
|
+
deviceInfo?: DeviceInfo,
|
|
92
|
+
): Promise<{ id: string }> {
|
|
93
|
+
try {
|
|
94
|
+
const response = await this.fetchWithTimeout(`${this.baseUrl}/api/sessions/start`, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: this.getHeaders(),
|
|
97
|
+
body: JSON.stringify({ deviceId, deviceInfo }),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
const text = await response.text();
|
|
102
|
+
this.logger.error('Failed to start session:', response.status, text);
|
|
103
|
+
throw new Error(`Failed to start session: ${response.status}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const data: SessionResponse = await response.json();
|
|
107
|
+
this.logger.log('Session started:', data.sessionId);
|
|
108
|
+
return { id: data.sessionId };
|
|
109
|
+
} catch (error) {
|
|
110
|
+
this.logger.error('Error starting session:', error);
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async endSession(sessionId: string): Promise<void> {
|
|
116
|
+
try {
|
|
117
|
+
const response = await this.fetchWithTimeout(
|
|
118
|
+
`${this.baseUrl}/api/sessions/${sessionId}/end`,
|
|
119
|
+
{
|
|
120
|
+
method: 'PATCH',
|
|
121
|
+
headers: this.getHeaders(),
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
const text = await response.text();
|
|
127
|
+
this.logger.error('Failed to end session:', response.status, text);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.logger.log('Session ended:', sessionId);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
this.logger.error('Error ending session:', error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Attach the host app's user identity to a session.
|
|
139
|
+
* Fire-and-forget: errors are logged but never thrown.
|
|
140
|
+
*/
|
|
141
|
+
async identifySession(
|
|
142
|
+
sessionId: string,
|
|
143
|
+
userId: string,
|
|
144
|
+
traits?: Record<string, unknown>,
|
|
145
|
+
): Promise<void> {
|
|
146
|
+
try {
|
|
147
|
+
const response = await this.fetchWithTimeout(
|
|
148
|
+
`${this.baseUrl}/api/sessions/${sessionId}/identify`,
|
|
149
|
+
{
|
|
150
|
+
method: 'PATCH',
|
|
151
|
+
headers: this.getHeaders(),
|
|
152
|
+
body: JSON.stringify({ userId, traits }),
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
const text = await response.text();
|
|
158
|
+
this.logger.warn('Identify failed:', response.status, text?.substring(0, 200));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
this.logger.log('Session identified as user:', userId);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
this.logger.warn('Error identifying session:', error);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async sendEventsBatch(
|
|
169
|
+
sessionId: string,
|
|
170
|
+
events: AnalyticsEvent[],
|
|
171
|
+
): Promise<{ count: number }> {
|
|
172
|
+
try {
|
|
173
|
+
const response = await this.fetchWithTimeout(
|
|
174
|
+
`${this.baseUrl}/api/events/batch`,
|
|
175
|
+
{
|
|
176
|
+
method: 'POST',
|
|
177
|
+
headers: this.getHeaders(),
|
|
178
|
+
body: JSON.stringify({ sessionId, events }),
|
|
179
|
+
},
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (!response.ok) {
|
|
183
|
+
const text = await response.text().catch(() => '');
|
|
184
|
+
this.logger.error(
|
|
185
|
+
'Failed to send events batch:',
|
|
186
|
+
response.status,
|
|
187
|
+
text,
|
|
188
|
+
);
|
|
189
|
+
throw this.batchError('Failed to send events batch', response.status);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const data = await this.safeJson(response, { count: events.length });
|
|
193
|
+
this.logger.log('Events batch sent, count:', data.count);
|
|
194
|
+
return { count: data.count };
|
|
195
|
+
} catch (error) {
|
|
196
|
+
this.logger.error('Error sending events batch:', error);
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async sendScreenVisitsBatch(visits: ScreenVisit[]): Promise<void> {
|
|
202
|
+
try {
|
|
203
|
+
const response = await this.fetchWithTimeout(
|
|
204
|
+
`${this.baseUrl}/api/screens/visit/batch`,
|
|
205
|
+
{
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: this.getHeaders(),
|
|
208
|
+
body: JSON.stringify({ visits }),
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
if (!response.ok) {
|
|
213
|
+
const text = await response.text().catch(() => '');
|
|
214
|
+
this.logger.error(
|
|
215
|
+
'Failed to send screen visits batch:',
|
|
216
|
+
response.status,
|
|
217
|
+
text,
|
|
218
|
+
);
|
|
219
|
+
throw this.batchError(
|
|
220
|
+
'Failed to send screen visits batch',
|
|
221
|
+
response.status,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.logger.log('Screen visits batch sent, count:', visits.length);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
this.logger.error('Error sending screen visits batch:', error);
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async sendSnapshotsBatch(
|
|
233
|
+
snapshots: SnapshotPayload[],
|
|
234
|
+
): Promise<{ count: number }> {
|
|
235
|
+
try {
|
|
236
|
+
const response = await this.fetchWithTimeout(
|
|
237
|
+
`${this.baseUrl}/api/snapshots/batch`,
|
|
238
|
+
{
|
|
239
|
+
method: 'POST',
|
|
240
|
+
headers: this.getHeaders(),
|
|
241
|
+
body: JSON.stringify({ snapshots }),
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (!response.ok) {
|
|
246
|
+
const text = await response.text().catch(() => '');
|
|
247
|
+
this.logger.error(
|
|
248
|
+
'Failed to send snapshots batch:',
|
|
249
|
+
response.status,
|
|
250
|
+
text,
|
|
251
|
+
);
|
|
252
|
+
throw this.batchError(
|
|
253
|
+
'Failed to send snapshots batch',
|
|
254
|
+
response.status,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const data = await this.safeJson(response, { count: snapshots.length });
|
|
259
|
+
this.logger.log('Snapshots batch sent, count:', data.count);
|
|
260
|
+
return { count: data.count };
|
|
261
|
+
} catch (error) {
|
|
262
|
+
this.logger.error('Error sending snapshots batch:', error);
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Send a single crash report. Never throws; returns true when the
|
|
269
|
+
* server accepted it (used to clear the persisted pending copy).
|
|
270
|
+
*/
|
|
271
|
+
async sendCrashReport(crash: CrashReport): Promise<boolean> {
|
|
272
|
+
try {
|
|
273
|
+
const response = await this.fetchWithTimeout(`${this.baseUrl}/api/crashes`, {
|
|
274
|
+
method: 'POST',
|
|
275
|
+
headers: this.getHeaders(),
|
|
276
|
+
body: JSON.stringify(crash),
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (!response.ok) {
|
|
280
|
+
const text = await response.text().catch(() => '');
|
|
281
|
+
this.logger.error(
|
|
282
|
+
'Failed to send crash report:',
|
|
283
|
+
response.status,
|
|
284
|
+
text,
|
|
285
|
+
);
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
this.logger.log('Crash report sent:', crash.type, crash.message);
|
|
290
|
+
return true;
|
|
291
|
+
} catch (error) {
|
|
292
|
+
// Fire-and-forget: catch errors silently, never throw
|
|
293
|
+
this.logger.error('Error sending crash report:', error);
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async sendCrashesBatch(
|
|
299
|
+
crashes: CrashReport[],
|
|
300
|
+
): Promise<{ count: number }> {
|
|
301
|
+
try {
|
|
302
|
+
const response = await this.fetchWithTimeout(`${this.baseUrl}/api/crashes/batch`, {
|
|
303
|
+
method: 'POST',
|
|
304
|
+
headers: this.getHeaders(),
|
|
305
|
+
body: JSON.stringify({ crashes }),
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (!response.ok) {
|
|
309
|
+
const text = await response.text();
|
|
310
|
+
this.logger.error(
|
|
311
|
+
'Failed to send crashes batch:',
|
|
312
|
+
response.status,
|
|
313
|
+
text,
|
|
314
|
+
);
|
|
315
|
+
throw new Error(
|
|
316
|
+
`Failed to send crashes batch: ${response.status}`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const data = await response.json();
|
|
321
|
+
this.logger.log('Crashes batch sent, count:', data.count);
|
|
322
|
+
return { count: data.count };
|
|
323
|
+
} catch (error) {
|
|
324
|
+
this.logger.error('Error sending crashes batch:', error);
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Upload a full-content screen capture for heatmap backgrounds.
|
|
331
|
+
* Fire-and-forget: errors are logged but never thrown.
|
|
332
|
+
*/
|
|
333
|
+
/**
|
|
334
|
+
* Upload a scroll tile for collaborative screen building.
|
|
335
|
+
*
|
|
336
|
+
* @param 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
|
+
* @param windowHeight Window height (logical px) at capture time.
|
|
341
|
+
* Used by the backend to compute image-to-logical scale for cropping.
|
|
342
|
+
*/
|
|
343
|
+
async uploadScreenTile(
|
|
344
|
+
sessionId: string,
|
|
345
|
+
screenName: string,
|
|
346
|
+
image: string,
|
|
347
|
+
width: number,
|
|
348
|
+
height: number,
|
|
349
|
+
scrollOffsetY: number,
|
|
350
|
+
viewportHeight: number,
|
|
351
|
+
contentHeight: number,
|
|
352
|
+
viewportTop?: number,
|
|
353
|
+
windowHeight?: number,
|
|
354
|
+
): Promise<void> {
|
|
355
|
+
try {
|
|
356
|
+
const body: Record<string, unknown> = {
|
|
357
|
+
sessionId,
|
|
358
|
+
screenName,
|
|
359
|
+
image,
|
|
360
|
+
width,
|
|
361
|
+
height,
|
|
362
|
+
scrollOffsetY: Math.round(scrollOffsetY),
|
|
363
|
+
viewportHeight: Math.round(viewportHeight),
|
|
364
|
+
contentHeight: Math.round(contentHeight),
|
|
365
|
+
};
|
|
366
|
+
if (typeof viewportTop === 'number' && Number.isFinite(viewportTop)) {
|
|
367
|
+
body.viewportTop = Math.round(viewportTop);
|
|
368
|
+
}
|
|
369
|
+
if (typeof windowHeight === 'number' && Number.isFinite(windowHeight)) {
|
|
370
|
+
body.windowHeight = Math.round(windowHeight);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const response = await this.fetchWithTimeout(
|
|
374
|
+
`${this.baseUrl}/api/screen-tiles`,
|
|
375
|
+
{
|
|
376
|
+
method: 'POST',
|
|
377
|
+
headers: this.getHeaders(),
|
|
378
|
+
body: JSON.stringify(body),
|
|
379
|
+
},
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
if (!response.ok) {
|
|
383
|
+
const text = await response.text();
|
|
384
|
+
this.logger.warn(
|
|
385
|
+
'Screen tile upload failed:',
|
|
386
|
+
response.status,
|
|
387
|
+
text?.substring(0, 200),
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
} catch (error) {
|
|
391
|
+
this.logger.warn('Error uploading screen tile:', error);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async uploadScreenCapture(
|
|
396
|
+
screenName: string,
|
|
397
|
+
image: string,
|
|
398
|
+
width: number,
|
|
399
|
+
height: number,
|
|
400
|
+
isFullContent: boolean,
|
|
401
|
+
): Promise<void> {
|
|
402
|
+
try {
|
|
403
|
+
const response = await this.fetchWithTimeout(
|
|
404
|
+
`${this.baseUrl}/api/screen-captures`,
|
|
405
|
+
{
|
|
406
|
+
method: 'POST',
|
|
407
|
+
headers: this.getHeaders(),
|
|
408
|
+
body: JSON.stringify({
|
|
409
|
+
screenName,
|
|
410
|
+
image,
|
|
411
|
+
width,
|
|
412
|
+
height,
|
|
413
|
+
isFullContent,
|
|
414
|
+
}),
|
|
415
|
+
},
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
if (!response.ok) {
|
|
419
|
+
const text = await response.text();
|
|
420
|
+
this.logger.warn(
|
|
421
|
+
'Screen capture upload failed:',
|
|
422
|
+
response.status,
|
|
423
|
+
text?.substring(0, 200),
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
} catch (error) {
|
|
427
|
+
this.logger.warn('Error uploading screen capture:', error);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|