@switchlabs/verify-ai-react-native 2.4.0 → 2.4.2
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.js +4 -1
- package/lib/components/VerifyAIScanner.js +40 -1
- package/lib/telemetry/TelemetryReporter.d.ts +6 -5
- package/lib/telemetry/TelemetryReporter.js +14 -13
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +5 -2
- package/src/components/VerifyAIScanner.tsx +47 -0
- package/src/telemetry/TelemetryReporter.ts +14 -13
- package/src/version.ts +1 -1
package/lib/client/index.js
CHANGED
|
@@ -118,9 +118,12 @@ export class VerifyAIClient {
|
|
|
118
118
|
: normalized.status >= 400 && normalized.status < 500
|
|
119
119
|
? 'request_error'
|
|
120
120
|
: 'server_error';
|
|
121
|
+
const telemetryError = eventType === 'request_timeout'
|
|
122
|
+
? `${normalized.message} [${context.method} ${context.path}; timeout=${this.timeout}ms]`
|
|
123
|
+
: normalized;
|
|
121
124
|
this.telemetry.track(eventType, {
|
|
122
125
|
component: 'client',
|
|
123
|
-
error:
|
|
126
|
+
error: telemetryError,
|
|
124
127
|
errorCode,
|
|
125
128
|
});
|
|
126
129
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useRef, useState, useCallback, useEffect } from 'react';
|
|
3
|
-
import { View, Text, TouchableOpacity, StyleSheet, ActivityIndicator, } from 'react-native';
|
|
3
|
+
import { View, Text, TouchableOpacity, StyleSheet, ActivityIndicator, AppState, useWindowDimensions, } from 'react-native';
|
|
4
4
|
import { CameraView, useCameraPermissions, } from 'expo-camera';
|
|
5
5
|
import { useTelemetry } from '../telemetry/TelemetryContext';
|
|
6
6
|
/** Quality used when expo-image-manipulator is not available (lower = smaller). */
|
|
@@ -89,6 +89,45 @@ export function VerifyAIScanner({ onCapture, onResult, onError, overlay, style,
|
|
|
89
89
|
const cameraReadyRef = useRef(false);
|
|
90
90
|
const cameraInitFailedRef = useRef(false);
|
|
91
91
|
const permissionDeniedTrackedRef = useRef(false);
|
|
92
|
+
// Track dimensions to detect orientation changes and remount camera
|
|
93
|
+
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
|
94
|
+
const prevDimensionsRef = useRef({ width: windowWidth, height: windowHeight });
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const prev = prevDimensionsRef.current;
|
|
97
|
+
const orientationChanged = (prev.width > prev.height) !== (windowWidth > windowHeight);
|
|
98
|
+
prevDimensionsRef.current = { width: windowWidth, height: windowHeight };
|
|
99
|
+
if (orientationChanged && !terminated) {
|
|
100
|
+
// Force camera remount to fix preview distortion on iOS
|
|
101
|
+
telemetry?.track('camera_orientation_remount', {
|
|
102
|
+
component: 'scanner',
|
|
103
|
+
metadata: {
|
|
104
|
+
from: prev.width > prev.height ? 'landscape' : 'portrait',
|
|
105
|
+
to: windowWidth > windowHeight ? 'landscape' : 'portrait',
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
setCameraReady(false);
|
|
109
|
+
cameraReadyRef.current = false;
|
|
110
|
+
setCameraKey((k) => k + 1);
|
|
111
|
+
}
|
|
112
|
+
}, [windowWidth, windowHeight, terminated]);
|
|
113
|
+
// Resume camera when app returns from background/inactive (e.g. notification bar)
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (terminated)
|
|
116
|
+
return;
|
|
117
|
+
const subscription = AppState.addEventListener('change', (nextState) => {
|
|
118
|
+
if (nextState === 'active') {
|
|
119
|
+
// Force camera remount — on iOS, AVCaptureSession often fails to resume
|
|
120
|
+
// its preview layer after returning from the notification bar or control center.
|
|
121
|
+
telemetry?.track('camera_appstate_remount', {
|
|
122
|
+
component: 'scanner',
|
|
123
|
+
});
|
|
124
|
+
setCameraReady(false);
|
|
125
|
+
cameraReadyRef.current = false;
|
|
126
|
+
setCameraKey((k) => k + 1);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return () => subscription.remove();
|
|
130
|
+
}, [terminated]);
|
|
92
131
|
const pausePreview = useCallback(() => {
|
|
93
132
|
cameraRef.current?.pausePreview?.().catch(() => { });
|
|
94
133
|
}, []);
|
|
@@ -24,17 +24,18 @@ export declare class TelemetryReporter {
|
|
|
24
24
|
private flushNow;
|
|
25
25
|
private clearFlushTimer;
|
|
26
26
|
/**
|
|
27
|
-
* Persist the current buffer to AsyncStorage so events survive app
|
|
27
|
+
* Persist the current buffer to AsyncStorage so events survive abrupt app exits.
|
|
28
28
|
* Fire-and-forget — never throws or blocks.
|
|
29
29
|
*
|
|
30
30
|
* IMPORTANT: skips persist if loadPersistedBuffer() hasn't completed yet,
|
|
31
|
-
* to avoid overwriting orphaned events from a
|
|
31
|
+
* to avoid overwriting orphaned events from a previous session before recovery.
|
|
32
32
|
*/
|
|
33
33
|
private persistBuffer;
|
|
34
34
|
/**
|
|
35
|
-
* Load
|
|
36
|
-
* Runs once per instance. If orphaned events are found, emits an
|
|
37
|
-
* so
|
|
35
|
+
* Load persisted events from a previous session and merge into current buffer.
|
|
36
|
+
* Runs once per instance. If orphaned events are found, emits an
|
|
37
|
+
* `sdk_session_recovered` event so monitoring can flag that the prior
|
|
38
|
+
* session ended without a clean telemetry flush.
|
|
38
39
|
*/
|
|
39
40
|
private loadPersistedBuffer;
|
|
40
41
|
}
|
|
@@ -26,7 +26,7 @@ export class TelemetryReporter {
|
|
|
26
26
|
this.apiKey = apiKey;
|
|
27
27
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
28
28
|
this.sessionId = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
29
|
-
// Load any
|
|
29
|
+
// Load any persisted events left behind by a previous session
|
|
30
30
|
this.loadPersistedBuffer();
|
|
31
31
|
}
|
|
32
32
|
/** Track an error event. Fire-and-forget — never throws. */
|
|
@@ -72,7 +72,7 @@ export class TelemetryReporter {
|
|
|
72
72
|
else {
|
|
73
73
|
this.scheduleFlush();
|
|
74
74
|
}
|
|
75
|
-
// Persist immediately once
|
|
75
|
+
// Persist immediately once prior-session recovery has completed. If startup
|
|
76
76
|
// recovery is still in flight, write the merged buffer back afterwards.
|
|
77
77
|
if (this.loadedPersisted) {
|
|
78
78
|
this.persistBuffer();
|
|
@@ -91,7 +91,7 @@ export class TelemetryReporter {
|
|
|
91
91
|
}
|
|
92
92
|
/** Flush all buffered events immediately. Returns a promise but never rejects. */
|
|
93
93
|
async flush() {
|
|
94
|
-
// Merge any
|
|
94
|
+
// Merge any persisted events from a previous session before flushing
|
|
95
95
|
await this.loadPersistedBuffer();
|
|
96
96
|
if (this.buffer.size === 0 || this.flushing)
|
|
97
97
|
return;
|
|
@@ -168,15 +168,15 @@ export class TelemetryReporter {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
/**
|
|
171
|
-
* Persist the current buffer to AsyncStorage so events survive app
|
|
171
|
+
* Persist the current buffer to AsyncStorage so events survive abrupt app exits.
|
|
172
172
|
* Fire-and-forget — never throws or blocks.
|
|
173
173
|
*
|
|
174
174
|
* IMPORTANT: skips persist if loadPersistedBuffer() hasn't completed yet,
|
|
175
|
-
* to avoid overwriting orphaned events from a
|
|
175
|
+
* to avoid overwriting orphaned events from a previous session before recovery.
|
|
176
176
|
*/
|
|
177
177
|
persistBuffer() {
|
|
178
178
|
if (!this.loadedPersisted)
|
|
179
|
-
return; // Don't overwrite orphaned
|
|
179
|
+
return; // Don't overwrite orphaned prior-session data before it's loaded
|
|
180
180
|
try {
|
|
181
181
|
const entries = {};
|
|
182
182
|
for (const [key, event] of this.buffer) {
|
|
@@ -196,9 +196,10 @@ export class TelemetryReporter {
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
/**
|
|
199
|
-
* Load
|
|
200
|
-
* Runs once per instance. If orphaned events are found, emits an
|
|
201
|
-
* so
|
|
199
|
+
* Load persisted events from a previous session and merge into current buffer.
|
|
200
|
+
* Runs once per instance. If orphaned events are found, emits an
|
|
201
|
+
* `sdk_session_recovered` event so monitoring can flag that the prior
|
|
202
|
+
* session ended without a clean telemetry flush.
|
|
202
203
|
*/
|
|
203
204
|
async loadPersistedBuffer() {
|
|
204
205
|
if (this.loadedPersisted)
|
|
@@ -236,14 +237,14 @@ export class TelemetryReporter {
|
|
|
236
237
|
this.buffer.set(key, event);
|
|
237
238
|
}
|
|
238
239
|
}
|
|
239
|
-
// Emit
|
|
240
|
+
// Emit sdk_session_recovered if we recovered orphaned events from a previous session
|
|
240
241
|
if (orphanedCount > 0) {
|
|
241
242
|
const now = new Date().toISOString();
|
|
242
243
|
const uniqueTypes = [...new Set(orphanedTypes)];
|
|
243
244
|
const crashEvent = {
|
|
244
|
-
event_type: '
|
|
245
|
+
event_type: 'sdk_session_recovered',
|
|
245
246
|
component: 'TelemetryReporter',
|
|
246
|
-
error_message: `Recovered ${orphanedCount} unflushed event(s) from
|
|
247
|
+
error_message: `Recovered ${orphanedCount} unflushed event(s) from a previous session that ended without a clean telemetry flush: ${uniqueTypes.join(', ')}`,
|
|
247
248
|
sdk_platform: Platform.OS,
|
|
248
249
|
sdk_version: SDK_VERSION,
|
|
249
250
|
os_name: Platform.OS,
|
|
@@ -253,7 +254,7 @@ export class TelemetryReporter {
|
|
|
253
254
|
first_occurred_at: now,
|
|
254
255
|
last_occurred_at: now,
|
|
255
256
|
};
|
|
256
|
-
this.buffer.set(`
|
|
257
|
+
this.buffer.set(`sdk_session_recovered|recovered|${this.sessionId}`, crashEvent);
|
|
257
258
|
}
|
|
258
259
|
// Clear persisted data now that it's loaded into memory. The merged
|
|
259
260
|
// buffer will be re-persisted below until a flush succeeds.
|
package/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "2.4.
|
|
1
|
+
export declare const SDK_VERSION = "2.4.2";
|
package/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = '2.4.
|
|
1
|
+
export const SDK_VERSION = '2.4.2';
|
package/package.json
CHANGED
package/src/client/index.ts
CHANGED
|
@@ -173,13 +173,16 @@ export class VerifyAIClient {
|
|
|
173
173
|
: normalized.status === 429
|
|
174
174
|
? 'rate_limited'
|
|
175
175
|
: normalized.status >= 400 && normalized.status < 500
|
|
176
|
-
|
|
176
|
+
? 'request_error'
|
|
177
177
|
: 'server_error';
|
|
178
|
+
const telemetryError = eventType === 'request_timeout'
|
|
179
|
+
? `${normalized.message} [${context.method} ${context.path}; timeout=${this.timeout}ms]`
|
|
180
|
+
: normalized;
|
|
178
181
|
this.telemetry.track(
|
|
179
182
|
eventType,
|
|
180
183
|
{
|
|
181
184
|
component: 'client',
|
|
182
|
-
error:
|
|
185
|
+
error: telemetryError,
|
|
183
186
|
errorCode,
|
|
184
187
|
},
|
|
185
188
|
);
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
TouchableOpacity,
|
|
6
6
|
StyleSheet,
|
|
7
7
|
ActivityIndicator,
|
|
8
|
+
AppState,
|
|
9
|
+
useWindowDimensions,
|
|
8
10
|
type ViewStyle,
|
|
9
11
|
} from 'react-native';
|
|
10
12
|
import {
|
|
@@ -152,6 +154,51 @@ export function VerifyAIScanner({
|
|
|
152
154
|
const cameraInitFailedRef = useRef(false);
|
|
153
155
|
const permissionDeniedTrackedRef = useRef(false);
|
|
154
156
|
|
|
157
|
+
// Track dimensions to detect orientation changes and remount camera
|
|
158
|
+
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
|
159
|
+
const prevDimensionsRef = useRef({ width: windowWidth, height: windowHeight });
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
const prev = prevDimensionsRef.current;
|
|
163
|
+
const orientationChanged =
|
|
164
|
+
(prev.width > prev.height) !== (windowWidth > windowHeight);
|
|
165
|
+
prevDimensionsRef.current = { width: windowWidth, height: windowHeight };
|
|
166
|
+
|
|
167
|
+
if (orientationChanged && !terminated) {
|
|
168
|
+
// Force camera remount to fix preview distortion on iOS
|
|
169
|
+
telemetry?.track('camera_orientation_remount', {
|
|
170
|
+
component: 'scanner',
|
|
171
|
+
metadata: {
|
|
172
|
+
from: prev.width > prev.height ? 'landscape' : 'portrait',
|
|
173
|
+
to: windowWidth > windowHeight ? 'landscape' : 'portrait',
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
setCameraReady(false);
|
|
177
|
+
cameraReadyRef.current = false;
|
|
178
|
+
setCameraKey((k) => k + 1);
|
|
179
|
+
}
|
|
180
|
+
}, [windowWidth, windowHeight, terminated]);
|
|
181
|
+
|
|
182
|
+
// Resume camera when app returns from background/inactive (e.g. notification bar)
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (terminated) return;
|
|
185
|
+
|
|
186
|
+
const subscription = AppState.addEventListener('change', (nextState) => {
|
|
187
|
+
if (nextState === 'active') {
|
|
188
|
+
// Force camera remount — on iOS, AVCaptureSession often fails to resume
|
|
189
|
+
// its preview layer after returning from the notification bar or control center.
|
|
190
|
+
telemetry?.track('camera_appstate_remount', {
|
|
191
|
+
component: 'scanner',
|
|
192
|
+
});
|
|
193
|
+
setCameraReady(false);
|
|
194
|
+
cameraReadyRef.current = false;
|
|
195
|
+
setCameraKey((k) => k + 1);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return () => subscription.remove();
|
|
200
|
+
}, [terminated]);
|
|
201
|
+
|
|
155
202
|
const pausePreview = useCallback(() => {
|
|
156
203
|
cameraRef.current?.pausePreview?.().catch(() => {});
|
|
157
204
|
}, []);
|
|
@@ -56,7 +56,7 @@ export class TelemetryReporter {
|
|
|
56
56
|
this.apiKey = apiKey;
|
|
57
57
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
58
58
|
this.sessionId = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
59
|
-
// Load any
|
|
59
|
+
// Load any persisted events left behind by a previous session
|
|
60
60
|
this.loadPersistedBuffer();
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -116,7 +116,7 @@ export class TelemetryReporter {
|
|
|
116
116
|
this.scheduleFlush();
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
// Persist immediately once
|
|
119
|
+
// Persist immediately once prior-session recovery has completed. If startup
|
|
120
120
|
// recovery is still in flight, write the merged buffer back afterwards.
|
|
121
121
|
if (this.loadedPersisted) {
|
|
122
122
|
this.persistBuffer();
|
|
@@ -134,7 +134,7 @@ export class TelemetryReporter {
|
|
|
134
134
|
|
|
135
135
|
/** Flush all buffered events immediately. Returns a promise but never rejects. */
|
|
136
136
|
async flush(): Promise<void> {
|
|
137
|
-
// Merge any
|
|
137
|
+
// Merge any persisted events from a previous session before flushing
|
|
138
138
|
await this.loadPersistedBuffer();
|
|
139
139
|
|
|
140
140
|
if (this.buffer.size === 0 || this.flushing) return;
|
|
@@ -216,14 +216,14 @@ export class TelemetryReporter {
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
/**
|
|
219
|
-
* Persist the current buffer to AsyncStorage so events survive app
|
|
219
|
+
* Persist the current buffer to AsyncStorage so events survive abrupt app exits.
|
|
220
220
|
* Fire-and-forget — never throws or blocks.
|
|
221
221
|
*
|
|
222
222
|
* IMPORTANT: skips persist if loadPersistedBuffer() hasn't completed yet,
|
|
223
|
-
* to avoid overwriting orphaned events from a
|
|
223
|
+
* to avoid overwriting orphaned events from a previous session before recovery.
|
|
224
224
|
*/
|
|
225
225
|
private persistBuffer(): void {
|
|
226
|
-
if (!this.loadedPersisted) return; // Don't overwrite orphaned
|
|
226
|
+
if (!this.loadedPersisted) return; // Don't overwrite orphaned prior-session data before it's loaded
|
|
227
227
|
try {
|
|
228
228
|
const entries: Record<string, TelemetryEvent> = {};
|
|
229
229
|
for (const [key, event] of this.buffer) {
|
|
@@ -242,9 +242,10 @@ export class TelemetryReporter {
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
/**
|
|
245
|
-
* Load
|
|
246
|
-
* Runs once per instance. If orphaned events are found, emits an
|
|
247
|
-
* so
|
|
245
|
+
* Load persisted events from a previous session and merge into current buffer.
|
|
246
|
+
* Runs once per instance. If orphaned events are found, emits an
|
|
247
|
+
* `sdk_session_recovered` event so monitoring can flag that the prior
|
|
248
|
+
* session ended without a clean telemetry flush.
|
|
248
249
|
*/
|
|
249
250
|
private async loadPersistedBuffer(): Promise<void> {
|
|
250
251
|
if (this.loadedPersisted) return;
|
|
@@ -282,14 +283,14 @@ export class TelemetryReporter {
|
|
|
282
283
|
}
|
|
283
284
|
}
|
|
284
285
|
|
|
285
|
-
// Emit
|
|
286
|
+
// Emit sdk_session_recovered if we recovered orphaned events from a previous session
|
|
286
287
|
if (orphanedCount > 0) {
|
|
287
288
|
const now = new Date().toISOString();
|
|
288
289
|
const uniqueTypes = [...new Set(orphanedTypes)];
|
|
289
290
|
const crashEvent: TelemetryEvent = {
|
|
290
|
-
event_type: '
|
|
291
|
+
event_type: 'sdk_session_recovered',
|
|
291
292
|
component: 'TelemetryReporter',
|
|
292
|
-
error_message: `Recovered ${orphanedCount} unflushed event(s) from
|
|
293
|
+
error_message: `Recovered ${orphanedCount} unflushed event(s) from a previous session that ended without a clean telemetry flush: ${uniqueTypes.join(', ')}`,
|
|
293
294
|
sdk_platform: Platform.OS,
|
|
294
295
|
sdk_version: SDK_VERSION,
|
|
295
296
|
os_name: Platform.OS,
|
|
@@ -299,7 +300,7 @@ export class TelemetryReporter {
|
|
|
299
300
|
first_occurred_at: now,
|
|
300
301
|
last_occurred_at: now,
|
|
301
302
|
};
|
|
302
|
-
this.buffer.set(`
|
|
303
|
+
this.buffer.set(`sdk_session_recovered|recovered|${this.sessionId}`, crashEvent);
|
|
303
304
|
}
|
|
304
305
|
|
|
305
306
|
// Clear persisted data now that it's loaded into memory. The merged
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = '2.4.
|
|
1
|
+
export const SDK_VERSION = '2.4.2';
|