Skip to content

Commit f5a76a3

Browse files
authored
Merge pull request Expensify#83275 from Expensify/marcaaron-initializeVisionCameraMetrics
[CP Staging] Add Scan flow Camera init telemetry
2 parents b4f5c07 + 6a1ca24 commit f5a76a3

5 files changed

Lines changed: 93 additions & 1 deletion

File tree

src/CONST/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1785,6 +1785,8 @@ const CONST = {
17851785
SPAN_OD_ND_TRANSITION_LOGGED_OUT: 'ManualOdNdTransitionLoggedOut',
17861786
SPAN_OPEN_SEARCH_ROUTER: 'ManualOpenSearchRouter',
17871787
SPAN_OPEN_CREATE_EXPENSE: 'ManualOpenCreateExpense',
1788+
SPAN_CAMERA_INIT: 'ManualCameraInit',
1789+
SPAN_SHUTTER_TO_CONFIRMATION: 'ManualShutterToConfirmation',
17881790
SPAN_SUBMIT_EXPENSE: 'ManualCreateExpenseSubmit',
17891791
SPAN_NAVIGATE_AFTER_EXPENSE_CREATE: 'ManualCreateExpenseNavigation',
17901792
SPAN_EXPENSE_SERVER_RESPONSE: 'ManualCreateExpenseServerResponse',

src/pages/iou/request/IOURequestStartPage.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,13 @@ function IOURequestStartPage({
155155
const prevTransactionReportID = usePrevious(transaction?.reportID);
156156

157157
useEffect(() => {
158+
// Don't end span for scan flows - it will be ended when camera initializes (or canceled if permission is denied).
159+
if (transactionRequestType === CONST.IOU.REQUEST_TYPE.SCAN) {
160+
return;
161+
}
158162
endSpan(CONST.TELEMETRY.SPAN_OPEN_CREATE_EXPENSE);
163+
// Tab switches change transactionRequestType but shouldn't re-trigger endSpan.
164+
// eslint-disable-next-line react-hooks/exhaustive-deps
159165
}, []);
160166

161167
useEffect(() => {

src/pages/iou/request/step/IOURequestStepConfirmation.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ function IOURequestStepConfirmation({
321321

322322
useEffect(() => {
323323
endSpan(CONST.TELEMETRY.SPAN_OPEN_CREATE_EXPENSE);
324+
endSpan(CONST.TELEMETRY.SPAN_SHUTTER_TO_CONFIRMATION);
324325
}, []);
325326

326327
useEffect(() => {

src/pages/iou/request/step/IOURequestStepScan/index.native.tsx

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {useFocusEffect} from '@react-navigation/core';
2-
import React, {useCallback, useRef, useState} from 'react';
2+
import React, {useCallback, useEffect, useRef, useState} from 'react';
33
import {Alert, AppState, StyleSheet, View} from 'react-native';
44
import type {LayoutRectangle} from 'react-native';
55
import ReactNativeBlobUtil from 'react-native-blob-util';
@@ -34,6 +34,7 @@ import getReceiptsUploadFolderPath from '@libs/getReceiptsUploadFolderPath';
3434
import Log from '@libs/Log';
3535
import Navigation from '@libs/Navigation/Navigation';
3636
import navigationRef from '@libs/Navigation/navigationRef';
37+
import {cancelSpan, endSpan, startSpan} from '@libs/telemetry/activeSpans';
3738
import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper';
3839
import withFullTransactionOrNotFound from '@pages/iou/request/step/withFullTransactionOrNotFound';
3940
import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound';
@@ -92,6 +93,59 @@ function IOURequestStepScan({
9293

9394
const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report?.policyID}`);
9495

96+
// Track camera init telemetry
97+
const cameraInitSpanStarted = useRef(false);
98+
const cameraInitialized = useRef(false);
99+
100+
// Start camera init span when permission is granted and camera is ready
101+
useEffect(() => {
102+
if (cameraInitSpanStarted.current || cameraPermissionStatus !== RESULTS.GRANTED || device == null) {
103+
return;
104+
}
105+
startSpan(CONST.TELEMETRY.SPAN_CAMERA_INIT, {
106+
name: CONST.TELEMETRY.SPAN_CAMERA_INIT,
107+
op: CONST.TELEMETRY.SPAN_CAMERA_INIT,
108+
});
109+
cameraInitSpanStarted.current = true;
110+
}, [cameraPermissionStatus, device]);
111+
112+
// Cancel spans when permission is denied/blocked/unavailable
113+
useEffect(() => {
114+
if (cameraPermissionStatus !== RESULTS.BLOCKED && cameraPermissionStatus !== RESULTS.UNAVAILABLE && cameraPermissionStatus !== RESULTS.DENIED) {
115+
return;
116+
}
117+
cancelSpan(CONST.TELEMETRY.SPAN_OPEN_CREATE_EXPENSE);
118+
}, [cameraPermissionStatus]);
119+
120+
// Cancel spans on unmount if camera never initialized
121+
useEffect(() => {
122+
return () => {
123+
// If camera initialized successfully, spans were already ended
124+
if (cameraInitialized.current) {
125+
return;
126+
}
127+
// Cancel camera init span if it was started
128+
if (cameraInitSpanStarted.current) {
129+
cancelSpan(CONST.TELEMETRY.SPAN_CAMERA_INIT);
130+
}
131+
// Always cancel the create expense span if camera never initialized
132+
cancelSpan(CONST.TELEMETRY.SPAN_OPEN_CREATE_EXPENSE);
133+
};
134+
}, []);
135+
136+
const handleCameraInitialized = useCallback(() => {
137+
// Prevent duplicate span endings if callback fires multiple times
138+
if (cameraInitialized.current) {
139+
return;
140+
}
141+
cameraInitialized.current = true;
142+
// Only end camera init span if it was actually started
143+
if (cameraInitSpanStarted.current) {
144+
endSpan(CONST.TELEMETRY.SPAN_CAMERA_INIT);
145+
}
146+
endSpan(CONST.TELEMETRY.SPAN_OPEN_CREATE_EXPENSE);
147+
}, []);
148+
95149
const askForPermissions = useCallback(() => {
96150
// There's no way we can check for the BLOCKED status without requesting the permission first
97151
// https://github.com/zoontek/react-native-permissions/blob/a836e114ce3a180b2b23916292c79841a267d828/README.md?plain=1#L670
@@ -165,6 +219,7 @@ function IOURequestStepScan({
165219

166220
return () => {
167221
subscription.remove();
222+
cancelSpan(CONST.TELEMETRY.SPAN_SHUTTER_TO_CONFIRMATION);
168223

169224
if (isLoaderVisible) {
170225
setIsLoaderVisible(false);
@@ -235,8 +290,24 @@ function IOURequestStepScan({
235290

236291
const viewfinderLayout = useRef<LayoutRectangle>(null);
237292

293+
const maybeCancelShutterSpan = useCallback(() => {
294+
if (isMultiScanEnabled) {
295+
return;
296+
}
297+
298+
cancelSpan(CONST.TELEMETRY.SPAN_SHUTTER_TO_CONFIRMATION);
299+
}, [isMultiScanEnabled]);
300+
238301
const capturePhoto = useCallback(() => {
302+
if (!isMultiScanEnabled) {
303+
startSpan(CONST.TELEMETRY.SPAN_SHUTTER_TO_CONFIRMATION, {
304+
name: CONST.TELEMETRY.SPAN_SHUTTER_TO_CONFIRMATION,
305+
op: CONST.TELEMETRY.SPAN_SHUTTER_TO_CONFIRMATION,
306+
});
307+
}
308+
239309
if (!camera.current && (cameraPermissionStatus === RESULTS.DENIED || cameraPermissionStatus === RESULTS.BLOCKED)) {
310+
maybeCancelShutterSpan();
240311
askForPermissions();
241312
return;
242313
}
@@ -246,10 +317,13 @@ function IOURequestStepScan({
246317
};
247318

248319
if (!camera.current) {
320+
maybeCancelShutterSpan();
249321
showCameraAlert();
322+
return;
250323
}
251324

252325
if (didCapturePhoto) {
326+
maybeCancelShutterSpan();
253327
return;
254328
}
255329

@@ -323,6 +397,7 @@ function IOURequestStepScan({
323397
})
324398
.catch((error: string) => {
325399
setDidCapturePhoto(false);
400+
maybeCancelShutterSpan();
326401
showCameraAlert();
327402
Log.warn('Error taking photo', error);
328403
});
@@ -416,6 +491,7 @@ function IOURequestStepScan({
416491
cameraTabIndex={1}
417492
onLayout={(e) => (viewfinderLayout.current = e.nativeEvent.layout)}
418493
forceInactive={isAttachmentPickerActive}
494+
onInitialized={handleCameraInitialized}
419495
/>
420496
<Animated.View style={[styles.cameraFocusIndicator, cameraFocusIndicatorAnimatedStyle]} />
421497
<Animated.View

src/pages/iou/request/step/IOURequestStepScan/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {isMobile, isMobileWebKit} from '@libs/Browser';
3131
import {base64ToFile, isLocalFile as isLocalFileFileUtils} from '@libs/fileDownload/FileUtils';
3232
import getCurrentPosition from '@libs/getCurrentPosition';
3333
import Navigation from '@libs/Navigation/Navigation';
34+
import {endSpan} from '@libs/telemetry/activeSpans';
3435
import StepScreenDragAndDropWrapper from '@pages/iou/request/step/StepScreenDragAndDropWrapper';
3536
import withFullTransactionOrNotFound from '@pages/iou/request/step/withFullTransactionOrNotFound';
3637
import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound';
@@ -78,6 +79,12 @@ function IOURequestStepScan({
7879
const lazyIllustrations = useMemoizedLazyIllustrations(['MultiScan', 'Hand', 'ReceiptStack', 'Shutter']);
7980
const lazyIcons = useMemoizedLazyExpensifyIcons(['Bolt', 'Gallery', 'ReceiptMultiple', 'boltSlash', 'ReplaceReceipt', 'SmartScan']);
8081
const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report?.policyID}`);
82+
83+
// End the create expense span on mount for web (no camera init tracking needed)
84+
useEffect(() => {
85+
endSpan(CONST.TELEMETRY.SPAN_OPEN_CREATE_EXPENSE);
86+
}, []);
87+
8188
const navigateBack = useCallback(() => {
8289
Navigation.goBack(backTo);
8390
}, [backTo]);

0 commit comments

Comments
 (0)