From 9a89d49b34ce60a3a2fa8886569b1a52b2e67579 Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Mon, 23 Feb 2026 17:08:42 +0100 Subject: [PATCH 1/7] Perf: Add diagnostic sub-spans to ManualOpenSearchRouter Add 4 sub-spans and 2 attributes to the ManualOpenSearchRouter Sentry span to identify which phases of the search router opening are bottlenecks. Sub-spans: - SearchRouter.ModalCloseWait: Modal.close() callback latency - SearchRouter.OptionsInit: Cold-path createOptionList() cost (guarded) - SearchRouter.ComputeOptions: JS computation in SearchAutocompleteList (guarded) - SearchRouter.ListRender: FlashList rendering + native layout Attributes: - cold_start: whether options needed initialization - trigger: 'button' or 'keyboard' Co-authored-by: Cursor --- src/CONST/index.ts | 4 ++++ src/components/OptionListContextProvider.tsx | 12 +++++++++++ .../Search/SearchAutocompleteList.tsx | 21 ++++++++++++++++++- .../Search/SearchRouter/SearchButton.tsx | 3 +++ .../Search/SearchRouter/SearchRouter.tsx | 11 ++++++++++ .../SearchRouter/SearchRouterContext.tsx | 7 ++++++- 6 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 85d95ed617b12..09a0d72757c5c 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1811,6 +1811,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_SUBMIT_EXPENSE: 'ManualCreateExpenseSubmit', SPAN_NAVIGATE_AFTER_EXPENSE_CREATE: 'ManualCreateExpenseNavigation', diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 2b75a390acc13..d8c93b42c3c8d 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'; @@ -253,8 +255,18 @@ 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', + }); + } 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 e2377dd9e523e..1aeaf23e867d1 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -45,7 +45,7 @@ import { import {buildSearchQueryJSON, buildUserReadableQueryString, getQueryWithoutFilters, getUserFriendlyKey, getUserFriendlyValue, shouldHighlight} from '@libs/SearchQueryUtils'; import {getDatePresets, getHasOptions} from '@libs/SearchUIUtils'; import StringUtils from '@libs/StringUtils'; -import {endSpan} from '@libs/telemetry/activeSpans'; +import {endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans'; import CONST, {CONTINUATION_DETECTION_SEARCH_FILTER_KEYS} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {CardFeeds, CardList, PersonalDetailsList, Policy, Report} from '@src/types/onyx'; @@ -184,6 +184,16 @@ function SearchAutocompleteList({ const expensifyIcons = useMemoizedLazyExpensifyIcons(['History', 'MagnifyingGlass']); const {options, areOptionsInitialized} = useOptionsList(); + + const computeSpanStarted = useRef(false); + if (!computeSpanStarted.current && 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', + }); + computeSpanStarted.current = true; + } + const searchOptions = (() => { if (!areOptionsInitialized) { return defaultListOptions; @@ -901,6 +911,14 @@ function SearchAutocompleteList({ } }, [autocompleteQueryValue, onHighlightFirstItem, normalizedReferenceText]); + if (isInitialRender && computeSpanStarted.current) { + 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', + }); + } + return ( showLoadingPlaceholder @@ -922,6 +940,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 15c6e70eb862c..212706c7a5de5 100644 --- a/src/components/Search/SearchRouter/SearchButton.tsx +++ b/src/components/Search/SearchRouter/SearchButton.tsx @@ -34,6 +34,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: { + trigger: 'button', + }, }); openSearchRouter(); diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 9db1bbc9960e0..e0347a3c308f0 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -38,6 +38,7 @@ import type {OptionData} from '@libs/ReportUtils'; import {getAutocompleteQueryWithComma, getTrimmedUserSearchQueryPreservingComma} from '@libs/SearchAutocompleteUtils'; import {getQueryWithUpdatedValues, sanitizeSearchValue} from '@libs/SearchQueryUtils'; import StringUtils from '@libs/StringUtils'; +import {getSpan} from '@libs/telemetry/activeSpans'; import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; import {navigateToAndOpenReport, searchInServer} from '@userActions/Report'; @@ -70,6 +71,16 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false}); const isRecentSearchesDataLoaded = !isLoadingOnyxValue(recentSearchesMetadata); const shouldShowList = isRecentSearchesDataLoaded && areOptionsInitialized; + + const coldStartAttributeSet = useRef(false); + if (!coldStartAttributeSet.current) { + const parentSpan = getSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER); + if (parentSpan) { + parentSpan.setAttribute('cold_start', !areOptionsInitialized); + coldStartAttributeSet.current = true; + } + } + const personalDetails = usePersonalDetails(); const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); diff --git a/src/components/Search/SearchRouter/SearchRouterContext.tsx b/src/components/Search/SearchRouter/SearchRouterContext.tsx index 56a60c8fb59af..e294af430d0cd 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, startSpan} from '@libs/telemetry/activeSpans'; import {close} from '@userActions/Modal'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; @@ -77,8 +77,13 @@ 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', + }); close( () => { + endSpan(CONST.TELEMETRY.SPAN_SEARCH_ROUTER_MODAL_CLOSE_WAIT); openSearch(setIsSearchRouterDisplayed); searchRouterDisplayedRef.current = true; }, From 5f99d514ac8a2cadbb97bfc1c1b898278981291c Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Tue, 24 Feb 2026 11:24:50 +0100 Subject: [PATCH 2/7] Link sub-spans as children of SPAN_OPEN_SEARCH_ROUTER via parentSpan Co-authored-by: Cursor --- src/components/OptionListContextProvider.tsx | 1 + src/components/Search/SearchAutocompleteList.tsx | 2 ++ src/components/Search/SearchRouter/SearchRouterContext.tsx | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 47a632f2da40f..f04f4dee221d1 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -262,6 +262,7 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) { 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(); diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 4d7fda1b5dc78..348c45e38be05 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -188,6 +188,7 @@ function SearchAutocompleteList({ 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; } @@ -907,6 +908,7 @@ function SearchAutocompleteList({ 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), }); } diff --git a/src/components/Search/SearchRouter/SearchRouterContext.tsx b/src/components/Search/SearchRouter/SearchRouterContext.tsx index e294af430d0cd..b06226b277a58 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 {endSpan, 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'; @@ -80,6 +80,7 @@ function SearchRouterContextProvider({children}: ChildrenProps) { 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( () => { From ff0acd5c856816d016f0fffe2ee9fc74bf9daf80 Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Wed, 25 Feb 2026 10:45:31 +0100 Subject: [PATCH 3/7] Fix cold_start attribute timing and add span cleanup on unmount Move cold_start attribute setting to useEffect so it reads the synced areOptionsInitialized value instead of the initial useState(false). Add cleanup effect in SearchAutocompleteList to cancel ComputeOptions and ListRender spans if the component unmounts before they complete. Co-authored-by: Cursor --- src/components/Search/SearchAutocompleteList.tsx | 9 ++++++++- src/components/Search/SearchRouter/SearchRouter.tsx | 7 +++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 348c45e38be05..96641362b1ef0 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -44,7 +44,7 @@ import { import {buildSearchQueryJSON, buildUserReadableQueryString, getQueryWithoutFilters, getUserFriendlyKey, getUserFriendlyValue, shouldHighlight} from '@libs/SearchQueryUtils'; import {getDatePresets, getHasOptions} from '@libs/SearchUIUtils'; import StringUtils from '@libs/StringUtils'; -import {endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans'; +import {cancelSpan, endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans'; import CONST, {CONTINUATION_DETECTION_SEARCH_FILTER_KEYS} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {CardFeeds, CardList, PersonalDetailsList, Policy, Report} from '@src/types/onyx'; @@ -193,6 +193,13 @@ function SearchAutocompleteList({ computeSpanStarted.current = true; } + useEffect(() => { + return () => { + cancelSpan(CONST.TELEMETRY.SPAN_SEARCH_ROUTER_COMPUTE_OPTIONS); + cancelSpan(CONST.TELEMETRY.SPAN_SEARCH_ROUTER_LIST_RENDER); + }; + }, []); + const searchOptions = (() => { if (!areOptionsInitialized) { return defaultListOptions; diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index f6c367bb5c755..fe684f16f0b21 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -73,13 +73,16 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla const shouldShowList = isRecentSearchesDataLoaded && areOptionsInitialized; const coldStartAttributeSet = useRef(false); - if (!coldStartAttributeSet.current) { + useEffect(() => { + if (coldStartAttributeSet.current) { + return; + } const parentSpan = getSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER); if (parentSpan) { parentSpan.setAttribute('cold_start', !areOptionsInitialized); coldStartAttributeSet.current = true; } - } + }, [areOptionsInitialized]); const personalDetails = usePersonalDetails(); const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); From a3d27e36a0f07d8efdb565b3794abde7faec7a1b Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Thu, 26 Feb 2026 23:25:44 +0000 Subject: [PATCH 4/7] Fix compute span overlap and cold_start attribute accuracy 1. Guard COMPUTE_OPTIONS span behind areOptionsInitialized so it only starts when real computation happens, avoiding overlap with OPTIONS_INIT on cold opens. 2. Read cold_start from OptionsListContext directly instead of the delayed useOptionsList() internal state, which always starts as false regardless of actual initialization status. Made-with: Cursor --- src/components/Search/SearchAutocompleteList.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 1c2047f20a0b3..29bfdafdc45c7 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 {OptionsListContext, 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'; @@ -167,7 +167,7 @@ function SearchAutocompleteList({ const computeSpanStarted = useRef(false); const spanHandoffDone = useRef(false); // eslint-disable-next-line react-hooks/refs -- intentional: telemetry span must start during render to measure computation time - if (!computeSpanStarted.current && getSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER)) { + if (!computeSpanStarted.current && areOptionsInitialized && 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', @@ -183,6 +183,7 @@ function SearchAutocompleteList({ }; }, []); + const {areOptionsInitialized: contextAreOptionsInitialized} = useContext(OptionsListContext); const coldStartAttributeSet = useRef(false); useEffect(() => { if (coldStartAttributeSet.current) { @@ -190,10 +191,10 @@ function SearchAutocompleteList({ } const parentSpan = getSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER); if (parentSpan) { - parentSpan.setAttribute('cold_start', !areOptionsInitialized); + parentSpan.setAttribute('cold_start', !contextAreOptionsInitialized); coldStartAttributeSet.current = true; } - }, [areOptionsInitialized]); + }, [contextAreOptionsInitialized]); const searchOptions = (() => { if (!areOptionsInitialized) { From 4016f146774f58f302f2d49338933a050f92bad6 Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Thu, 26 Feb 2026 23:40:59 +0000 Subject: [PATCH 5/7] Use CONST attribute names for cold_start and trigger Add ATTRIBUTE_COLD_START and ATTRIBUTE_TRIGGER constants to match the existing pattern for telemetry attribute names, replacing bare string literals. Made-with: Cursor --- src/CONST/index.ts | 2 ++ src/components/Search/SearchAutocompleteList.tsx | 2 +- src/components/Search/SearchRouter/SearchButton.tsx | 2 +- src/components/Search/SearchRouter/SearchRouterContext.tsx | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 82ad713b5c1e8..a0425cf60a080 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1867,6 +1867,8 @@ const CONST = { ATTRIBUTE_THROTTLE_WAIT_MS: 'throttle_wait_ms', ATTRIBUTE_JSON_CODE: 'json_code', ATTRIBUTE_ONYX_UPDATES_COUNT: 'onyx_updates_count', + ATTRIBUTE_COLD_START: 'cold_start', + ATTRIBUTE_TRIGGER: 'trigger', SUBMIT_EXPENSE_SCENARIO: { REQUEST_MONEY_MANUAL: 'request_money_manual', REQUEST_MONEY_SCAN: 'request_money_scan', diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 29bfdafdc45c7..72e8a6b28df5e 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -191,7 +191,7 @@ function SearchAutocompleteList({ } const parentSpan = getSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER); if (parentSpan) { - parentSpan.setAttribute('cold_start', !contextAreOptionsInitialized); + parentSpan.setAttribute(CONST.TELEMETRY.ATTRIBUTE_COLD_START, !contextAreOptionsInitialized); coldStartAttributeSet.current = true; } }, [contextAreOptionsInitialized]); diff --git a/src/components/Search/SearchRouter/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx index a176ae3d1ea64..83e4813170d8e 100644 --- a/src/components/Search/SearchRouter/SearchButton.tsx +++ b/src/components/Search/SearchRouter/SearchButton.tsx @@ -33,7 +33,7 @@ function SearchButton({style, shouldUseAutoHitSlop = false}: SearchButtonProps) name: CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER, op: CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER, attributes: { - trigger: 'button', + [CONST.TELEMETRY.ATTRIBUTE_TRIGGER]: 'button', }, }); diff --git a/src/components/Search/SearchRouter/SearchRouterContext.tsx b/src/components/Search/SearchRouter/SearchRouterContext.tsx index b06226b277a58..eb0b4a86d0044 100644 --- a/src/components/Search/SearchRouter/SearchRouterContext.tsx +++ b/src/components/Search/SearchRouter/SearchRouterContext.tsx @@ -109,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', }, }); }; From 08ed61f4a1c4ddff39817b333e8a48cb3339b28c Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Mon, 2 Mar 2026 21:33:35 +0000 Subject: [PATCH 6/7] Gate compute span to non-loading render pass Hoist isRecentSearchesDataLoaded above the compute span guard so ComputeOptions only starts once all loading states have cleared, preventing idle Onyx hydration time from inflating the span on cold opens. Made-with: Cursor --- src/components/Search/SearchAutocompleteList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 72e8a6b28df5e..0d036013011bf 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -166,8 +166,9 @@ function SearchAutocompleteList({ 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 && getSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER)) { + 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', @@ -446,7 +447,6 @@ function SearchAutocompleteList({ } }, [autocompleteQueryValue, onHighlightFirstItem, normalizedReferenceText]); - const isRecentSearchesDataLoaded = !isLoadingOnyxValue(recentSearchesMetadata); const isLoading = !isRecentSearchesDataLoaded || !areOptionsInitialized; if (isLoading) { From 61bb27a87ccba93379b4c90f033b87ccbd07f653 Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Mon, 2 Mar 2026 22:16:25 +0000 Subject: [PATCH 7/7] Fix OptionsListContext rename after main merge OptionsListContext was renamed to OptionsListStateContext on main. Update the import and useContext call to match. Made-with: Cursor --- src/components/Search/SearchAutocompleteList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 0d036013011bf..87780927913f0 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, {useContext, useEffect, useRef, useState} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {OptionsListContext, 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'; @@ -184,7 +184,7 @@ function SearchAutocompleteList({ }; }, []); - const {areOptionsInitialized: contextAreOptionsInitialized} = useContext(OptionsListContext); + const {areOptionsInitialized: contextAreOptionsInitialized} = useContext(OptionsListStateContext); const coldStartAttributeSet = useRef(false); useEffect(() => { if (coldStartAttributeSet.current) {