@switchlabs/verify-ai-react-native 2.4.21 → 2.4.23
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/lib/client/index.d.ts +1 -1
- package/lib/client/index.js +50 -10
- package/lib/components/VerifyAIScanner.js +15 -2
- package/lib/telemetry/TelemetryReporter.d.ts +6 -0
- package/lib/telemetry/TelemetryReporter.js +30 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +66 -18
- package/src/components/VerifyAIScanner.tsx +18 -2
- package/src/telemetry/TelemetryReporter.ts +34 -1
- package/src/version.ts +1 -1
package/lib/client/index.d.ts
CHANGED
|
@@ -93,7 +93,7 @@ export declare class VerifyAIClient {
|
|
|
93
93
|
export declare class VerifyAIRequestError extends Error {
|
|
94
94
|
status: number;
|
|
95
95
|
body: VerifyAIError;
|
|
96
|
-
constructor(message:
|
|
96
|
+
constructor(message: unknown, status: number, body: VerifyAIError);
|
|
97
97
|
get isRateLimited(): boolean;
|
|
98
98
|
get isUnauthorized(): boolean;
|
|
99
99
|
get isServerError(): boolean;
|
package/lib/client/index.js
CHANGED
|
@@ -19,6 +19,39 @@ function enrichMetadata(metadata) {
|
|
|
19
19
|
}
|
|
20
20
|
return enriched;
|
|
21
21
|
}
|
|
22
|
+
function stringifyErrorMessage(value, fallback) {
|
|
23
|
+
if (typeof value === 'string' && value.trim()) {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
if (value instanceof Error && value.message) {
|
|
27
|
+
return value.message;
|
|
28
|
+
}
|
|
29
|
+
if (value && typeof value === 'object') {
|
|
30
|
+
const record = value;
|
|
31
|
+
for (const key of ['message', 'error', 'detail', 'code']) {
|
|
32
|
+
const candidate = record[key];
|
|
33
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
34
|
+
return candidate;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const json = JSON.stringify(value);
|
|
39
|
+
if (json && json !== '{}') {
|
|
40
|
+
return json;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Fall through to String(value) below.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (value != null) {
|
|
48
|
+
const text = String(value);
|
|
49
|
+
if (text && text !== '[object Object]') {
|
|
50
|
+
return text;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return fallback;
|
|
54
|
+
}
|
|
22
55
|
export class VerifyAIClient {
|
|
23
56
|
constructor(config) {
|
|
24
57
|
if (!config.apiKey) {
|
|
@@ -43,8 +76,9 @@ export class VerifyAIClient {
|
|
|
43
76
|
}
|
|
44
77
|
}
|
|
45
78
|
buildRequestError(message, status, context, body = {}) {
|
|
79
|
+
const safeMessage = stringifyErrorMessage(message, 'VerifyAI request failed');
|
|
46
80
|
const error = {
|
|
47
|
-
error:
|
|
81
|
+
error: safeMessage,
|
|
48
82
|
status,
|
|
49
83
|
code: status === 408 ? 'timeout' : status === 0 ? 'network_error' : 'request_error',
|
|
50
84
|
path: context.path,
|
|
@@ -52,7 +86,7 @@ export class VerifyAIClient {
|
|
|
52
86
|
method: context.method,
|
|
53
87
|
...body,
|
|
54
88
|
};
|
|
55
|
-
return new VerifyAIRequestError(
|
|
89
|
+
return new VerifyAIRequestError(safeMessage, status, error);
|
|
56
90
|
}
|
|
57
91
|
normalizeRequestError(error, context) {
|
|
58
92
|
if (error instanceof VerifyAIRequestError) {
|
|
@@ -64,7 +98,7 @@ export class VerifyAIClient {
|
|
|
64
98
|
if (error instanceof TypeError) {
|
|
65
99
|
return this.buildRequestError('Network request failed', 0, context, { code: 'network_error' });
|
|
66
100
|
}
|
|
67
|
-
const message = error instanceof Error ? error.message : 'VerifyAI request failed';
|
|
101
|
+
const message = error instanceof Error ? error.message : stringifyErrorMessage(error, 'VerifyAI request failed');
|
|
68
102
|
return this.buildRequestError(message, 0, context);
|
|
69
103
|
}
|
|
70
104
|
async executeRequest(path, options = {}) {
|
|
@@ -86,7 +120,7 @@ export class VerifyAIClient {
|
|
|
86
120
|
const body = this.parseResponseBody(rawBody);
|
|
87
121
|
if (!response.ok) {
|
|
88
122
|
const errorBody = (body && typeof body === 'object' ? body : null);
|
|
89
|
-
throw this.buildRequestError(errorBody?.error
|
|
123
|
+
throw this.buildRequestError(stringifyErrorMessage(errorBody?.error, `Request failed with status ${response.status}`), response.status, context, {
|
|
90
124
|
current_usage: errorBody?.current_usage,
|
|
91
125
|
limit: errorBody?.limit,
|
|
92
126
|
upgrade_url: errorBody?.upgrade_url,
|
|
@@ -220,9 +254,11 @@ export class VerifyAIClient {
|
|
|
220
254
|
});
|
|
221
255
|
}
|
|
222
256
|
catch (error) {
|
|
223
|
-
//
|
|
224
|
-
// Vercel
|
|
225
|
-
//
|
|
257
|
+
// 5xx during multipart can mean the server crashed before reservation
|
|
258
|
+
// (e.g. Vercel multipart-parse failure) OR after the verification was
|
|
259
|
+
// saved (e.g. signed-URL generation throw). Retry as base64 JSON and
|
|
260
|
+
// forward the original Idempotency-Key so the server can dedupe instead
|
|
261
|
+
// of inserting a second verify_ai_verifications row.
|
|
226
262
|
if (error instanceof VerifyAIRequestError && error.status >= 500 && error.isServerError) {
|
|
227
263
|
try {
|
|
228
264
|
const { Buffer } = await import('buffer');
|
|
@@ -235,7 +271,7 @@ export class VerifyAIClient {
|
|
|
235
271
|
metadata: request.metadata,
|
|
236
272
|
provider: request.provider,
|
|
237
273
|
include_image_data: request.include_image_data,
|
|
238
|
-
});
|
|
274
|
+
}, options?.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : undefined);
|
|
239
275
|
}
|
|
240
276
|
catch {
|
|
241
277
|
// If the retry itself fails, throw the original error
|
|
@@ -316,10 +352,14 @@ export class VerifyAIClient {
|
|
|
316
352
|
}
|
|
317
353
|
export class VerifyAIRequestError extends Error {
|
|
318
354
|
constructor(message, status, body) {
|
|
319
|
-
|
|
355
|
+
const safeMessage = stringifyErrorMessage(message, 'VerifyAI request failed');
|
|
356
|
+
super(safeMessage);
|
|
320
357
|
this.name = 'VerifyAIRequestError';
|
|
321
358
|
this.status = status;
|
|
322
|
-
this.body =
|
|
359
|
+
this.body = {
|
|
360
|
+
...body,
|
|
361
|
+
error: stringifyErrorMessage(body.error, safeMessage),
|
|
362
|
+
};
|
|
323
363
|
}
|
|
324
364
|
get isRateLimited() {
|
|
325
365
|
return this.status === 429;
|
|
@@ -4,6 +4,7 @@ import { View, Text, TouchableOpacity, StyleSheet, ActivityIndicator, AppState,
|
|
|
4
4
|
import { CameraView, useCameraPermissions, } from 'expo-camera';
|
|
5
5
|
import { VerifyAIRequestError } from '../client';
|
|
6
6
|
import { useTelemetry } from '../telemetry/TelemetryContext';
|
|
7
|
+
import { SDK_VERSION } from '../version';
|
|
7
8
|
import { BikeOverlay } from './BikeOverlay';
|
|
8
9
|
import { ScooterOverlay } from './ScooterOverlay';
|
|
9
10
|
import { ANDROID_ACCELEROMETER_AXIS_DOMINANCE_THRESHOLD, classifyAndroidAccelerometerOrientation, getOverlayRotationDeg, } from './scannerOrientation';
|
|
@@ -326,11 +327,23 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
326
327
|
telemetryRef.current = telemetry;
|
|
327
328
|
buildScannerTelemetryMetadataRef.current = buildScannerTelemetryMetadata;
|
|
328
329
|
useEffect(() => {
|
|
329
|
-
telemetryRef.current
|
|
330
|
+
const reporter = telemetryRef.current;
|
|
331
|
+
if (reporter) {
|
|
332
|
+
console.log(`VerifyAI[${SDK_VERSION}]: scanner telemetry attached ` +
|
|
333
|
+
`baseUrl=${reporter.getBaseUrl()} apiKeyPrefix=${reporter.getApiKeyPrefix()}…`);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
console.warn(`VerifyAI[${SDK_VERSION}]: scanner telemetry is not attached; field ` +
|
|
337
|
+
'diagnostics will only appear in local device logs. Pass a ' +
|
|
338
|
+
'`TelemetryReporter` into `<VerifyAIScanner telemetry={…} />` or ' +
|
|
339
|
+
'wrap the tree in `<TelemetryContext.Provider value={reporter}>` ' +
|
|
340
|
+
'to enable server-side diagnostics.');
|
|
341
|
+
}
|
|
342
|
+
reporter?.track('camera_scanner_mounted', {
|
|
330
343
|
component: 'scanner',
|
|
331
344
|
error: 'scanner_mounted',
|
|
332
345
|
metadata: buildScannerTelemetryMetadataRef.current?.({
|
|
333
|
-
scanner_telemetry_attached:
|
|
346
|
+
scanner_telemetry_attached: reporter ? 1 : 0,
|
|
334
347
|
}),
|
|
335
348
|
});
|
|
336
349
|
return () => {
|
|
@@ -9,6 +9,12 @@ export declare class TelemetryReporter {
|
|
|
9
9
|
private loadedPersisted;
|
|
10
10
|
private loadPersistedPromise;
|
|
11
11
|
constructor(apiKey: string, baseUrl: string);
|
|
12
|
+
/** Telemetry POST destination — exposed so the scanner can log an init banner
|
|
13
|
+
* that helps field debugging confirm where events are being sent. */
|
|
14
|
+
getBaseUrl(): string;
|
|
15
|
+
/** First 6 characters of the API key, for log banners. Never returns the full key. */
|
|
16
|
+
getApiKeyPrefix(): string;
|
|
17
|
+
private logDeliveryFailure;
|
|
12
18
|
/** Track an error event. Fire-and-forget — never throws. */
|
|
13
19
|
track(eventType: string, opts?: {
|
|
14
20
|
component?: string;
|
|
@@ -40,6 +40,20 @@ export class TelemetryReporter {
|
|
|
40
40
|
// Load any persisted events left behind by a previous session
|
|
41
41
|
this.loadPersistedBuffer();
|
|
42
42
|
}
|
|
43
|
+
/** Telemetry POST destination — exposed so the scanner can log an init banner
|
|
44
|
+
* that helps field debugging confirm where events are being sent. */
|
|
45
|
+
getBaseUrl() {
|
|
46
|
+
return this.baseUrl;
|
|
47
|
+
}
|
|
48
|
+
/** First 6 characters of the API key, for log banners. Never returns the full key. */
|
|
49
|
+
getApiKeyPrefix() {
|
|
50
|
+
return this.apiKey.length >= 6 ? this.apiKey.slice(0, 6) : this.apiKey;
|
|
51
|
+
}
|
|
52
|
+
logDeliveryFailure(kind, detail, eventCount) {
|
|
53
|
+
const trimmed = detail.length > 200 ? detail.slice(0, 200) : detail;
|
|
54
|
+
console.warn(`VerifyAI[${SDK_VERSION}]: telemetry POST failed kind=${kind} ` +
|
|
55
|
+
`events=${eventCount} url=${this.baseUrl}/telemetry detail=${trimmed}`);
|
|
56
|
+
}
|
|
43
57
|
/** Track an error event. Fire-and-forget — never throws. */
|
|
44
58
|
track(eventType, opts = {}) {
|
|
45
59
|
if (this.disposed)
|
|
@@ -113,6 +127,7 @@ export class TelemetryReporter {
|
|
|
113
127
|
this.clearFlushTimer();
|
|
114
128
|
const controller = new AbortController();
|
|
115
129
|
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
130
|
+
let loggedDeliveryFailure = false;
|
|
116
131
|
try {
|
|
117
132
|
const response = await fetch(`${this.baseUrl}/telemetry`, {
|
|
118
133
|
method: 'POST',
|
|
@@ -124,12 +139,26 @@ export class TelemetryReporter {
|
|
|
124
139
|
signal: controller.signal,
|
|
125
140
|
});
|
|
126
141
|
if (!response.ok) {
|
|
142
|
+
let body = '';
|
|
143
|
+
try {
|
|
144
|
+
body = await response.text();
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// ignore
|
|
148
|
+
}
|
|
149
|
+
this.logDeliveryFailure(`http_${response.status}`, body, events.length);
|
|
150
|
+
loggedDeliveryFailure = true;
|
|
127
151
|
throw new Error(`Telemetry request failed with status ${response.status}`);
|
|
128
152
|
}
|
|
129
153
|
// Success — clear persisted buffer since events are now server-side
|
|
130
154
|
this.persistBuffer(); // buffer is empty at this point, so this clears the key
|
|
131
155
|
}
|
|
132
|
-
catch {
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (!loggedDeliveryFailure) {
|
|
158
|
+
const kind = error instanceof Error ? error.name : typeof error;
|
|
159
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
160
|
+
this.logDeliveryFailure(kind, detail, events.length);
|
|
161
|
+
}
|
|
133
162
|
for (const [dedupKey, event] of bufferedEntries) {
|
|
134
163
|
const existing = this.buffer.get(dedupKey);
|
|
135
164
|
if (existing) {
|
package/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "2.4.
|
|
1
|
+
export declare const SDK_VERSION = "2.4.23";
|
package/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = '2.4.
|
|
1
|
+
export const SDK_VERSION = '2.4.23';
|
package/package.json
CHANGED
package/src/client/index.ts
CHANGED
|
@@ -34,6 +34,44 @@ function enrichMetadata(metadata?: Record<string, unknown>): Record<string, unkn
|
|
|
34
34
|
return enriched;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function stringifyErrorMessage(value: unknown, fallback: string): string {
|
|
38
|
+
if (typeof value === 'string' && value.trim()) {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (value instanceof Error && value.message) {
|
|
43
|
+
return value.message;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (value && typeof value === 'object') {
|
|
47
|
+
const record = value as Record<string, unknown>;
|
|
48
|
+
for (const key of ['message', 'error', 'detail', 'code']) {
|
|
49
|
+
const candidate = record[key];
|
|
50
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
51
|
+
return candidate;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const json = JSON.stringify(value);
|
|
57
|
+
if (json && json !== '{}') {
|
|
58
|
+
return json;
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// Fall through to String(value) below.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (value != null) {
|
|
66
|
+
const text = String(value);
|
|
67
|
+
if (text && text !== '[object Object]') {
|
|
68
|
+
return text;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return fallback;
|
|
73
|
+
}
|
|
74
|
+
|
|
37
75
|
interface RequestContext {
|
|
38
76
|
path: string;
|
|
39
77
|
url: string;
|
|
@@ -71,13 +109,14 @@ export class VerifyAIClient {
|
|
|
71
109
|
}
|
|
72
110
|
|
|
73
111
|
private buildRequestError(
|
|
74
|
-
message:
|
|
112
|
+
message: unknown,
|
|
75
113
|
status: number,
|
|
76
114
|
context: RequestContext,
|
|
77
115
|
body: Partial<VerifyAIError> = {}
|
|
78
116
|
): VerifyAIRequestError {
|
|
117
|
+
const safeMessage = stringifyErrorMessage(message, 'VerifyAI request failed');
|
|
79
118
|
const error: VerifyAIError = {
|
|
80
|
-
error:
|
|
119
|
+
error: safeMessage,
|
|
81
120
|
status,
|
|
82
121
|
code: status === 408 ? 'timeout' : status === 0 ? 'network_error' : 'request_error',
|
|
83
122
|
path: context.path,
|
|
@@ -86,7 +125,7 @@ export class VerifyAIClient {
|
|
|
86
125
|
...body,
|
|
87
126
|
};
|
|
88
127
|
|
|
89
|
-
return new VerifyAIRequestError(
|
|
128
|
+
return new VerifyAIRequestError(safeMessage, status, error);
|
|
90
129
|
}
|
|
91
130
|
|
|
92
131
|
private normalizeRequestError(error: unknown, context: RequestContext): VerifyAIRequestError {
|
|
@@ -102,7 +141,7 @@ export class VerifyAIClient {
|
|
|
102
141
|
return this.buildRequestError('Network request failed', 0, context, { code: 'network_error' });
|
|
103
142
|
}
|
|
104
143
|
|
|
105
|
-
const message = error instanceof Error ? error.message : 'VerifyAI request failed';
|
|
144
|
+
const message = error instanceof Error ? error.message : stringifyErrorMessage(error, 'VerifyAI request failed');
|
|
106
145
|
return this.buildRequestError(message, 0, context);
|
|
107
146
|
}
|
|
108
147
|
|
|
@@ -132,7 +171,7 @@ export class VerifyAIClient {
|
|
|
132
171
|
if (!response.ok) {
|
|
133
172
|
const errorBody = (body && typeof body === 'object' ? body : null) as VerifyAIError | null;
|
|
134
173
|
throw this.buildRequestError(
|
|
135
|
-
errorBody?.error
|
|
174
|
+
stringifyErrorMessage(errorBody?.error, `Request failed with status ${response.status}`),
|
|
136
175
|
response.status,
|
|
137
176
|
context,
|
|
138
177
|
{
|
|
@@ -284,22 +323,27 @@ export class VerifyAIClient {
|
|
|
284
323
|
body: formData,
|
|
285
324
|
});
|
|
286
325
|
} catch (error) {
|
|
287
|
-
//
|
|
288
|
-
// Vercel
|
|
289
|
-
//
|
|
326
|
+
// 5xx during multipart can mean the server crashed before reservation
|
|
327
|
+
// (e.g. Vercel multipart-parse failure) OR after the verification was
|
|
328
|
+
// saved (e.g. signed-URL generation throw). Retry as base64 JSON and
|
|
329
|
+
// forward the original Idempotency-Key so the server can dedupe instead
|
|
330
|
+
// of inserting a second verify_ai_verifications row.
|
|
290
331
|
if (error instanceof VerifyAIRequestError && error.status >= 500 && error.isServerError) {
|
|
291
332
|
try {
|
|
292
333
|
const { Buffer } = await import('buffer');
|
|
293
334
|
const fileResponse = await fetch(request.imageUri);
|
|
294
335
|
const arrayBuffer = await fileResponse.arrayBuffer();
|
|
295
336
|
const base64 = Buffer.from(arrayBuffer).toString('base64');
|
|
296
|
-
return this.verify(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
337
|
+
return this.verify(
|
|
338
|
+
{
|
|
339
|
+
image: base64,
|
|
340
|
+
policy: request.policy,
|
|
341
|
+
metadata: request.metadata,
|
|
342
|
+
provider: request.provider,
|
|
343
|
+
include_image_data: request.include_image_data,
|
|
344
|
+
},
|
|
345
|
+
options?.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : undefined,
|
|
346
|
+
);
|
|
303
347
|
} catch {
|
|
304
348
|
// If the retry itself fails, throw the original error
|
|
305
349
|
throw error;
|
|
@@ -384,11 +428,15 @@ export class VerifyAIRequestError extends Error {
|
|
|
384
428
|
status: number;
|
|
385
429
|
body: VerifyAIError;
|
|
386
430
|
|
|
387
|
-
constructor(message:
|
|
388
|
-
|
|
431
|
+
constructor(message: unknown, status: number, body: VerifyAIError) {
|
|
432
|
+
const safeMessage = stringifyErrorMessage(message, 'VerifyAI request failed');
|
|
433
|
+
super(safeMessage);
|
|
389
434
|
this.name = 'VerifyAIRequestError';
|
|
390
435
|
this.status = status;
|
|
391
|
-
this.body =
|
|
436
|
+
this.body = {
|
|
437
|
+
...body,
|
|
438
|
+
error: stringifyErrorMessage(body.error, safeMessage),
|
|
439
|
+
};
|
|
392
440
|
}
|
|
393
441
|
|
|
394
442
|
get isRateLimited(): boolean {
|
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
import { VerifyAIRequestError } from '../client';
|
|
24
24
|
import { useTelemetry } from '../telemetry/TelemetryContext';
|
|
25
25
|
import type { TelemetryReporter } from '../telemetry/TelemetryReporter';
|
|
26
|
+
import { SDK_VERSION } from '../version';
|
|
26
27
|
import { BikeOverlay } from './BikeOverlay';
|
|
27
28
|
import { ScooterOverlay } from './ScooterOverlay';
|
|
28
29
|
import {
|
|
@@ -439,11 +440,26 @@ export function VerifyAIScanner({
|
|
|
439
440
|
buildScannerTelemetryMetadataRef.current = buildScannerTelemetryMetadata;
|
|
440
441
|
|
|
441
442
|
useEffect(() => {
|
|
442
|
-
telemetryRef.current
|
|
443
|
+
const reporter = telemetryRef.current;
|
|
444
|
+
if (reporter) {
|
|
445
|
+
console.log(
|
|
446
|
+
`VerifyAI[${SDK_VERSION}]: scanner telemetry attached ` +
|
|
447
|
+
`baseUrl=${reporter.getBaseUrl()} apiKeyPrefix=${reporter.getApiKeyPrefix()}…`,
|
|
448
|
+
);
|
|
449
|
+
} else {
|
|
450
|
+
console.warn(
|
|
451
|
+
`VerifyAI[${SDK_VERSION}]: scanner telemetry is not attached; field ` +
|
|
452
|
+
'diagnostics will only appear in local device logs. Pass a ' +
|
|
453
|
+
'`TelemetryReporter` into `<VerifyAIScanner telemetry={…} />` or ' +
|
|
454
|
+
'wrap the tree in `<TelemetryContext.Provider value={reporter}>` ' +
|
|
455
|
+
'to enable server-side diagnostics.',
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
reporter?.track('camera_scanner_mounted', {
|
|
443
459
|
component: 'scanner',
|
|
444
460
|
error: 'scanner_mounted',
|
|
445
461
|
metadata: buildScannerTelemetryMetadataRef.current?.({
|
|
446
|
-
scanner_telemetry_attached:
|
|
462
|
+
scanner_telemetry_attached: reporter ? 1 : 0,
|
|
447
463
|
}),
|
|
448
464
|
});
|
|
449
465
|
|
|
@@ -71,6 +71,25 @@ export class TelemetryReporter {
|
|
|
71
71
|
this.loadPersistedBuffer();
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/** Telemetry POST destination — exposed so the scanner can log an init banner
|
|
75
|
+
* that helps field debugging confirm where events are being sent. */
|
|
76
|
+
getBaseUrl(): string {
|
|
77
|
+
return this.baseUrl;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** First 6 characters of the API key, for log banners. Never returns the full key. */
|
|
81
|
+
getApiKeyPrefix(): string {
|
|
82
|
+
return this.apiKey.length >= 6 ? this.apiKey.slice(0, 6) : this.apiKey;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private logDeliveryFailure(kind: string, detail: string, eventCount: number): void {
|
|
86
|
+
const trimmed = detail.length > 200 ? detail.slice(0, 200) : detail;
|
|
87
|
+
console.warn(
|
|
88
|
+
`VerifyAI[${SDK_VERSION}]: telemetry POST failed kind=${kind} ` +
|
|
89
|
+
`events=${eventCount} url=${this.baseUrl}/telemetry detail=${trimmed}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
74
93
|
/** Track an error event. Fire-and-forget — never throws. */
|
|
75
94
|
track(
|
|
76
95
|
eventType: string,
|
|
@@ -158,6 +177,7 @@ export class TelemetryReporter {
|
|
|
158
177
|
const controller = new AbortController();
|
|
159
178
|
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
160
179
|
|
|
180
|
+
let loggedDeliveryFailure = false;
|
|
161
181
|
try {
|
|
162
182
|
const response = await fetch(`${this.baseUrl}/telemetry`, {
|
|
163
183
|
method: 'POST',
|
|
@@ -170,12 +190,25 @@ export class TelemetryReporter {
|
|
|
170
190
|
});
|
|
171
191
|
|
|
172
192
|
if (!response.ok) {
|
|
193
|
+
let body = '';
|
|
194
|
+
try {
|
|
195
|
+
body = await response.text();
|
|
196
|
+
} catch {
|
|
197
|
+
// ignore
|
|
198
|
+
}
|
|
199
|
+
this.logDeliveryFailure(`http_${response.status}`, body, events.length);
|
|
200
|
+
loggedDeliveryFailure = true;
|
|
173
201
|
throw new Error(`Telemetry request failed with status ${response.status}`);
|
|
174
202
|
}
|
|
175
203
|
|
|
176
204
|
// Success — clear persisted buffer since events are now server-side
|
|
177
205
|
this.persistBuffer(); // buffer is empty at this point, so this clears the key
|
|
178
|
-
} catch {
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (!loggedDeliveryFailure) {
|
|
208
|
+
const kind = error instanceof Error ? error.name : typeof error;
|
|
209
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
210
|
+
this.logDeliveryFailure(kind, detail, events.length);
|
|
211
|
+
}
|
|
179
212
|
for (const [dedupKey, event] of bufferedEntries) {
|
|
180
213
|
const existing = this.buffer.get(dedupKey);
|
|
181
214
|
if (existing) {
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = '2.4.
|
|
1
|
+
export const SDK_VERSION = '2.4.23';
|