diff --git a/src/libs/Navigation/OnyxTabNavigator.tsx b/src/libs/Navigation/OnyxTabNavigator.tsx index d5cd04e27cbf..84f565653bf3 100644 --- a/src/libs/Navigation/OnyxTabNavigator.tsx +++ b/src/libs/Navigation/OnyxTabNavigator.tsx @@ -61,6 +61,11 @@ type OnyxTabNavigatorProps = Child /** Whether tabs should have equal width */ equalWidth?: boolean; + + /** Fires when a tab becomes active, including the initial mount. Deduplicated so it fires at most + * once per distinct active tab. Intended for side-effects like telemetry where consumers want to + * observe focus events without duplicating React Navigation state-listener noise. */ + onTabFocused?: (tabName: TTabName) => void; }; const TopTab = createMaterialTopTabNavigator(); @@ -105,10 +110,12 @@ function OnyxTabNavigator({ lazyLoadEnabled = false, onTabSelect, equalWidth = false, + onTabFocused, ...rest }: OnyxTabNavigatorProps) { const styles = useThemeStyles(); const isFirstMountRef = useRef(true); + const lastFocusedTabRef = useRef(null); // Mapping of tab name to focus trap container element const [focusTrapContainerElementMapping, setFocusTrapContainerElementMapping] = useState>({}); const [selectedTab, selectedTabResult] = useOnyx(`${ONYXKEYS.COLLECTION.SELECTED_TAB}${id}`); @@ -193,6 +200,10 @@ function OnyxTabNavigator({ isFirstMountRef.current = false; } const newSelectedTab = routeNames.at(index); + if (newSelectedTab && newSelectedTab !== lastFocusedTabRef.current) { + lastFocusedTabRef.current = newSelectedTab; + onTabFocused?.(newSelectedTab as TTabName); + } if (selectedTab === newSelectedTab) { return; } diff --git a/src/libs/actions/IOU/Split.ts b/src/libs/actions/IOU/Split.ts index 90ac609fe906..4dd1c7cd468d 100644 --- a/src/libs/actions/IOU/Split.ts +++ b/src/libs/actions/IOU/Split.ts @@ -46,6 +46,7 @@ import { } from '@libs/ReportUtils'; import type {OptimisticChatReport} from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; +import emitExpenseCreated from '@libs/telemetry/emitExpenseCreated'; import {addOptimization, setPendingSubmitFollowUpAction} from '@libs/telemetry/submitFollowUpAction'; import { buildOptimisticTransaction, @@ -1995,6 +1996,8 @@ function createDistanceRequest(distanceRequestInformation: CreateDistanceRequest onyxData = moneyRequestOnyxData; distanceIouReport = iouReport; + emitExpenseCreated(transaction.iouRequestType); + const isGPSDistanceRequest = transaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_GPS; if (isDistanceExpenseType(transaction.iouRequestType)) { diff --git a/src/libs/telemetry/emitExpenseCreated.ts b/src/libs/telemetry/emitExpenseCreated.ts new file mode 100644 index 000000000000..7ef6dc6b2029 --- /dev/null +++ b/src/libs/telemetry/emitExpenseCreated.ts @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/react-native'; +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type IOURequestType = ValueOf; + +function emitExpenseCreated(iouRequestType: IOURequestType | undefined) { + if (!iouRequestType) { + return; + } + Sentry.startInactiveSpan({ + name: `ExpenseCreated.${iouRequestType}`, + op: 'expense.created', + forceTransaction: true, + })?.end(); +} + +export default emitExpenseCreated; diff --git a/src/pages/iou/request/DistanceRequestStartPage.tsx b/src/pages/iou/request/DistanceRequestStartPage.tsx index 84aa6620cb55..00e56356e2bb 100644 --- a/src/pages/iou/request/DistanceRequestStartPage.tsx +++ b/src/pages/iou/request/DistanceRequestStartPage.tsx @@ -1,4 +1,5 @@ -import React, {useEffect, useMemo, useState} from 'react'; +import * as Sentry from '@sentry/react-native'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import FocusTrapContainerElement from '@components/FocusTrap/FocusTrapContainerElement'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -18,7 +19,7 @@ import {endSpan} from '@libs/telemetry/activeSpans'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type SCREENS from '@src/SCREENS'; +import SCREENS from '@src/SCREENS'; import type {SelectedTabRequest} from '@src/types/onyx'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import IOURequestStepDistanceGPS from './step/IOURequestStepDistanceGPS'; @@ -84,6 +85,14 @@ function DistanceRequestStartPage({ endSpan(CONST.TELEMETRY.SPAN_OPEN_CREATE_EXPENSE); }, []); + const handleTabFocused = useCallback((tabName: SelectedTabRequest) => { + Sentry.startInactiveSpan({ + name: `${SCREENS.MONEY_REQUEST.DISTANCE_CREATE}.tab.${tabName}`, + op: 'ui.tab.focus', + forceTransaction: true, + })?.end(); + }, []); + const navigateBack = () => { Navigation.closeRHPFlow(); }; @@ -126,6 +135,7 @@ function DistanceRequestStartPage({ tabBar={TabSelector} onTabBarFocusTrapContainerElementChanged={setTabBarContainerElement} onActiveTabFocusTrapContainerElementChanged={setActiveTabContainerElement} + onTabFocused={handleTabFocused} lazyLoadEnabled >