diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 89577aa736c7b..5af5a9c82ee5b 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1790,6 +1790,10 @@ const CONST = { SPAN_OD_ND_TRANSITION: 'ManualOdNdTransition', SPAN_OD_ND_TRANSITION_LOGGED_OUT: 'ManualOdNdTransitionLoggedOut', SPAN_OPEN_SEARCH_ROUTER: 'ManualOpenSearchRouter', + SPAN_SEARCH_ROUTER_MODAL_CLOSE_WAIT: 'SearchRouter.ModalCloseWait', + SPAN_SEARCH_ROUTER_OPTIONS_INIT: 'SearchRouter.OptionsInit', + SPAN_SEARCH_ROUTER_COMPUTE_OPTIONS: 'SearchRouter.ComputeOptions', + SPAN_SEARCH_ROUTER_LIST_RENDER: 'SearchRouter.ListRender', SPAN_OPEN_CREATE_EXPENSE: 'ManualOpenCreateExpense', SPAN_CAMERA_INIT: 'ManualCameraInit', SPAN_SHUTTER_TO_CONFIRMATION: 'ManualShutterToConfirmation', @@ -1847,6 +1851,8 @@ const CONST = { ATTRIBUTE_IS_FROM_GLOBAL_CREATE: 'is_from_global_create', ATTRIBUTE_COMMAND: 'command', ATTRIBUTE_JSON_CODE: 'json_code', + ATTRIBUTE_COLD_START: 'cold_start', + ATTRIBUTE_TRIGGER: 'trigger', ATTRIBUTE_PLATFORM: 'platform', ATTRIBUTE_IS_MULTI_SCAN: 'is_multi_scan', SUBMIT_EXPENSE_SCENARIO: { diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 0c9ed7f56057b..1aa1cb33ca950 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -7,6 +7,8 @@ import usePrivateIsArchivedMap from '@hooks/usePrivateIsArchivedMap'; import {createOptionFromReport, createOptionList, processReport, shallowOptionsListCompare} from '@libs/OptionsListUtils'; import type {OptionList, SearchOption} from '@libs/OptionsListUtils'; import {isSelfDM} from '@libs/ReportUtils'; +import {endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails, Report} from '@src/types/onyx'; import {usePersonalDetails} from './OnyxListItemProvider'; @@ -267,8 +269,19 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) { }, [personalDetails]); const initializeOptions = useCallback(() => { + const isSearchRouterSpanActive = !!getSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER); + if (isSearchRouterSpanActive) { + startSpan(CONST.TELEMETRY.SPAN_SEARCH_ROUTER_OPTIONS_INIT, { + name: CONST.TELEMETRY.SPAN_SEARCH_ROUTER_OPTIONS_INIT, + op: 'function', + parentSpan: getSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER), + }); + } loadOptions(); areOptionsInitialized.current = true; + if (isSearchRouterSpanActive) { + endSpan(CONST.TELEMETRY.SPAN_SEARCH_ROUTER_OPTIONS_INIT); + } }, [loadOptions]); const resetOptions = useCallback(() => { diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 290d932986670..bb1df6ca43e7f 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -1,7 +1,7 @@ import type {ForwardedRef, RefObject} from 'react'; -import React, {useEffect, useRef, useState} from 'react'; +import React, {useContext, useEffect, useRef, useState} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {useOptionsList} from '@components/OptionListContextProvider'; +import {OptionsListStateContext, useOptionsList} from '@components/OptionListContextProvider'; import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import type {ListItem as NewListItem, UserListItemProps} from '@components/SelectionList/ListItem/types'; @@ -30,7 +30,7 @@ import type {OptionData} from '@libs/ReportUtils'; import {getReportOrDraftReport} from '@libs/ReportUtils'; import {buildSearchQueryJSON, buildUserReadableQueryString, getQueryWithoutFilters, shouldHighlight} from '@libs/SearchQueryUtils'; import StringUtils from '@libs/StringUtils'; -import {endSpan} from '@libs/telemetry/activeSpans'; +import {cancelSpan, endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {CardFeeds, CardList, PersonalDetailsList, Policy, Report} from '@src/types/onyx'; @@ -163,6 +163,40 @@ function SearchAutocompleteList({ const taxRates = getAllTaxRates(policies); const {options, areOptionsInitialized} = useOptionsList(); + + const computeSpanStarted = useRef(false); + const spanHandoffDone = useRef(false); + const isRecentSearchesDataLoaded = !isLoadingOnyxValue(recentSearchesMetadata); + // eslint-disable-next-line react-hooks/refs -- intentional: telemetry span must start during render to measure computation time + if (!computeSpanStarted.current && areOptionsInitialized && isRecentSearchesDataLoaded && getSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER)) { + startSpan(CONST.TELEMETRY.SPAN_SEARCH_ROUTER_COMPUTE_OPTIONS, { + name: CONST.TELEMETRY.SPAN_SEARCH_ROUTER_COMPUTE_OPTIONS, + op: 'function', + parentSpan: getSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER), + }); + computeSpanStarted.current = true; + } + + useEffect(() => { + return () => { + cancelSpan(CONST.TELEMETRY.SPAN_SEARCH_ROUTER_COMPUTE_OPTIONS); + cancelSpan(CONST.TELEMETRY.SPAN_SEARCH_ROUTER_LIST_RENDER); + }; + }, []); + + const {areOptionsInitialized: contextAreOptionsInitialized} = useContext(OptionsListStateContext); + const coldStartAttributeSet = useRef(false); + useEffect(() => { + if (coldStartAttributeSet.current) { + return; + } + const parentSpan = getSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER); + if (parentSpan) { + parentSpan.setAttribute(CONST.TELEMETRY.ATTRIBUTE_COLD_START, !contextAreOptionsInitialized); + coldStartAttributeSet.current = true; + } + }, [contextAreOptionsInitialized]); + const searchOptions = (() => { if (!areOptionsInitialized) { return defaultListOptions; @@ -413,7 +447,6 @@ function SearchAutocompleteList({ } }, [autocompleteQueryValue, onHighlightFirstItem, normalizedReferenceText]); - const isRecentSearchesDataLoaded = !isLoadingOnyxValue(recentSearchesMetadata); const isLoading = !isRecentSearchesDataLoaded || !areOptionsInitialized; if (isLoading) { @@ -426,6 +459,17 @@ function SearchAutocompleteList({ ); } + // eslint-disable-next-line react-hooks/refs -- intentional: telemetry handoff must run during render to accurately mark compute→render transition + if (isInitialRender && computeSpanStarted.current && !spanHandoffDone.current) { + spanHandoffDone.current = true; + endSpan(CONST.TELEMETRY.SPAN_SEARCH_ROUTER_COMPUTE_OPTIONS); + startSpan(CONST.TELEMETRY.SPAN_SEARCH_ROUTER_LIST_RENDER, { + name: CONST.TELEMETRY.SPAN_SEARCH_ROUTER_LIST_RENDER, + op: 'ui.render', + parentSpan: getSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER), + }); + } + return ( showLoadingPlaceholder @@ -447,6 +491,7 @@ function SearchAutocompleteList({ disableKeyboardShortcuts={!shouldSubscribeToArrowKeyEvents} addBottomSafeAreaPadding onLayout={() => { + endSpan(CONST.TELEMETRY.SPAN_SEARCH_ROUTER_LIST_RENDER); setPerformanceTimersEnd(); setIsInitialRender(false); innerListRef.current?.updateExternalTextInputFocus(textInputRef?.current?.isFocused() ?? false); diff --git a/src/components/Search/SearchRouter/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx index 033daa0be0b3d..83e4813170d8e 100644 --- a/src/components/Search/SearchRouter/SearchButton.tsx +++ b/src/components/Search/SearchRouter/SearchButton.tsx @@ -32,6 +32,9 @@ function SearchButton({style, shouldUseAutoHitSlop = false}: SearchButtonProps) startSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER, { name: CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER, op: CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER, + attributes: { + [CONST.TELEMETRY.ATTRIBUTE_TRIGGER]: 'button', + }, }); openSearchRouter(); diff --git a/src/components/Search/SearchRouter/SearchRouterContext.tsx b/src/components/Search/SearchRouter/SearchRouterContext.tsx index 56a60c8fb59af..eb0b4a86d0044 100644 --- a/src/components/Search/SearchRouter/SearchRouterContext.tsx +++ b/src/components/Search/SearchRouter/SearchRouterContext.tsx @@ -2,7 +2,7 @@ import React, {useContext, useEffect, useRef, useState} from 'react'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import {navigationRef} from '@libs/Navigation/Navigation'; -import {startSpan} from '@libs/telemetry/activeSpans'; +import {endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans'; import {close} from '@userActions/Modal'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; @@ -77,8 +77,14 @@ function SearchRouterContextProvider({children}: ChildrenProps) { if (isBrowserWithHistory) { window.history.pushState({isSearchModalOpen: true} satisfies HistoryState, ''); } + startSpan(CONST.TELEMETRY.SPAN_SEARCH_ROUTER_MODAL_CLOSE_WAIT, { + name: CONST.TELEMETRY.SPAN_SEARCH_ROUTER_MODAL_CLOSE_WAIT, + op: 'ui.modal.wait', + parentSpan: getSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER), + }); close( () => { + endSpan(CONST.TELEMETRY.SPAN_SEARCH_ROUTER_MODAL_CLOSE_WAIT); openSearch(setIsSearchRouterDisplayed); searchRouterDisplayedRef.current = true; }, @@ -103,7 +109,7 @@ function SearchRouterContextProvider({children}: ChildrenProps) { name: CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER, op: CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER, attributes: { - trigger: 'keyboard', + [CONST.TELEMETRY.ATTRIBUTE_TRIGGER]: 'keyboard', }, }); };