From e6c0fae218d697091ced61f36eef82372247d605 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 28 Nov 2025 19:48:12 +0100 Subject: [PATCH 1/9] Introduce hook useSkeletonSpan to track Skeletons' duration --- src/CONST/index.ts | 1 + src/components/AvatarSkeleton.tsx | 3 ++- src/libs/telemetry/useSkeletonSpan.ts | 22 ++++++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/libs/telemetry/useSkeletonSpan.ts diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 44414c94fbab..8c947fba3a71 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1682,6 +1682,7 @@ const CONST = { SPAN_OPEN_SEARCH_ROUTER: 'ManualOpenSearchRouter', SPAN_OPEN_CREATE_EXPENSE: 'ManualOpenCreateExpense', SPAN_SEND_MESSAGE: 'ManualSendMessage', + SPAN_SKELETON: 'ManualSkeleton', // Attribute names ATTRIBUTE_IOU_TYPE: 'iou_type', ATTRIBUTE_IOU_REQUEST_TYPE: 'iou_request_type', diff --git a/src/components/AvatarSkeleton.tsx b/src/components/AvatarSkeleton.tsx index 6e0a4f407d70..c11a57b6c677 100644 --- a/src/components/AvatarSkeleton.tsx +++ b/src/components/AvatarSkeleton.tsx @@ -3,12 +3,13 @@ import {Circle} from 'react-native-svg'; import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; +import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; import CONST from '@src/CONST'; import SkeletonViewContentLoader from './SkeletonViewContentLoader'; function AvatarSkeleton({size = CONST.AVATAR_SIZE.SMALL}: {size?: ValueOf}) { const theme = useTheme(); - + useSkeletonSpan('AvatarSkeleton'); const StyleUtils = useStyleUtils(); const avatarSize = StyleUtils.getAvatarSize(size); const skeletonCircleRadius = avatarSize / 2; diff --git a/src/libs/telemetry/useSkeletonSpan.ts b/src/libs/telemetry/useSkeletonSpan.ts new file mode 100644 index 000000000000..d6a4287c38d8 --- /dev/null +++ b/src/libs/telemetry/useSkeletonSpan.ts @@ -0,0 +1,22 @@ +/** + * This file contains logic related to tracking skeleton across the application. + */ +import CONST from '@src/CONST'; +import {endSpan, startSpan} from '@libs/telemetry/activeSpans'; +import {useEffect, useId, useMemo} from 'react'; + +function useSkeletonSpan(component: string) { + const reactId = useId(); + + useEffect(() => { + const spanId = `${CONST.TELEMETRY.SPAN_SKELETON}_${component}_${reactId}` + startSpan(spanId, { + op: CONST.TELEMETRY.SPAN_SKELETON, + name: component, + }); + + return () => endSpan(spanId); + }, []); +} + +export {useSkeletonSpan}; From 34952c0c18a5876999754a5780db6942830af483 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Sat, 29 Nov 2025 13:59:48 +0100 Subject: [PATCH 2/9] Add useSkeletonSpan to all components --- src/components/AccountSwitcherSkeletonView/index.tsx | 2 ++ src/components/MoneyReportHeaderStatusBarSkeleton.tsx | 2 ++ .../ReportActionsListLoadingSkeleton.tsx | 3 +++ src/components/OptionsListSkeletonView.tsx | 2 ++ src/components/ReportHeaderSkeletonView.tsx | 2 ++ src/components/Skeletons/CardRowSkeleton.tsx | 2 ++ src/components/Skeletons/MergeExpensesSkeleton.tsx | 2 ++ src/components/Skeletons/SearchFiltersSkeleton.tsx | 2 ++ src/components/Skeletons/SearchInputSelectionSkeleton.tsx | 2 ++ src/components/Skeletons/SearchRowSkeleton.tsx | 2 ++ src/components/Skeletons/TabNavigatorSkeleton.tsx | 2 ++ src/components/Skeletons/TableRowSkeleton.tsx | 2 ++ src/components/Skeletons/UnreportedExpensesSkeleton.tsx | 2 ++ src/components/Skeletons/WorkspaceRowSkeleton.tsx | 2 ++ src/components/TransactionPreviewSkeletonView.tsx | 2 ++ src/libs/telemetry/useSkeletonSpan.ts | 6 +++--- 16 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/components/AccountSwitcherSkeletonView/index.tsx b/src/components/AccountSwitcherSkeletonView/index.tsx index 379a4094e032..a99315120b45 100644 --- a/src/components/AccountSwitcherSkeletonView/index.tsx +++ b/src/components/AccountSwitcherSkeletonView/index.tsx @@ -6,6 +6,7 @@ import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; import CONST from '@src/CONST'; type AccountSwitcherSkeletonViewProps = { @@ -20,6 +21,7 @@ function AccountSwitcherSkeletonView({shouldAnimate = true, avatarSize = CONST.A const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + useSkeletonSpan('AccountSwitcherSkeletonView'); const avatarPlaceholderSize = StyleUtils.getAvatarSize(avatarSize); const avatarPlaceholderRadius = avatarPlaceholderSize / 2; const startPositionX = avatarPlaceholderRadius; diff --git a/src/components/MoneyReportHeaderStatusBarSkeleton.tsx b/src/components/MoneyReportHeaderStatusBarSkeleton.tsx index 6294fa4c80ba..c6f88dae19a5 100644 --- a/src/components/MoneyReportHeaderStatusBarSkeleton.tsx +++ b/src/components/MoneyReportHeaderStatusBarSkeleton.tsx @@ -3,11 +3,13 @@ import {View} from 'react-native'; import {Rect} from 'react-native-svg'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; import SkeletonViewContentLoader from './SkeletonViewContentLoader'; function MoneyReportHeaderStatusBarSkeleton() { const styles = useThemeStyles(); const theme = useTheme(); + useSkeletonSpan('MoneyReportHeaderStatusBarSkeleton'); return ( diff --git a/src/components/MoneyRequestReportView/ReportActionsListLoadingSkeleton.tsx b/src/components/MoneyRequestReportView/ReportActionsListLoadingSkeleton.tsx index b5bece629b5d..f7e6dd955f67 100644 --- a/src/components/MoneyRequestReportView/ReportActionsListLoadingSkeleton.tsx +++ b/src/components/MoneyRequestReportView/ReportActionsListLoadingSkeleton.tsx @@ -1,8 +1,11 @@ import React from 'react'; import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; +import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; function ReportActionsListLoadingSkeleton() { + useSkeletonSpan('ReportActionsListLoadingSkeleton'); + return ( (null); const styles = useThemeStyles(); const [pageWidth, setPageWidth] = React.useState(0); + useSkeletonSpan('MergeExpensesSkeleton'); useLayoutEffect(() => { containerRef.current?.measure((x, y, width) => { setPageWidth(width - 24); diff --git a/src/components/Skeletons/SearchFiltersSkeleton.tsx b/src/components/Skeletons/SearchFiltersSkeleton.tsx index bdeeb8ab4d6e..9cd6bc0c791a 100644 --- a/src/components/Skeletons/SearchFiltersSkeleton.tsx +++ b/src/components/Skeletons/SearchFiltersSkeleton.tsx @@ -4,6 +4,7 @@ import {Rect} from 'react-native-svg'; import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; type SearchFiltersSkeletonProps = { shouldAnimate?: boolean; @@ -12,6 +13,7 @@ type SearchFiltersSkeletonProps = { function SearchFiltersSkeleton({shouldAnimate = true}: SearchFiltersSkeletonProps) { const theme = useTheme(); const styles = useThemeStyles(); + useSkeletonSpan('SearchFiltersSkeleton'); const skeletonCount = new Array(5).fill(0); diff --git a/src/components/Skeletons/SearchInputSelectionSkeleton.tsx b/src/components/Skeletons/SearchInputSelectionSkeleton.tsx index 2207f41e4d5d..084aafab162d 100644 --- a/src/components/Skeletons/SearchInputSelectionSkeleton.tsx +++ b/src/components/Skeletons/SearchInputSelectionSkeleton.tsx @@ -4,11 +4,13 @@ import {Rect} from 'react-native-svg'; import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; import variables from '@styles/variables'; function SearchInputSelectionSkeleton() { const theme = useTheme(); const styles = useThemeStyles(); + useSkeletonSpan('SearchInputSelectionSkeleton'); return ( diff --git a/src/components/Skeletons/SearchRowSkeleton.tsx b/src/components/Skeletons/SearchRowSkeleton.tsx index 709918095d8a..231e11de780f 100644 --- a/src/components/Skeletons/SearchRowSkeleton.tsx +++ b/src/components/Skeletons/SearchRowSkeleton.tsx @@ -5,6 +5,7 @@ import {Circle, Rect} from 'react-native-svg'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ItemListSkeletonView from './ItemListSkeletonView'; @@ -36,6 +37,7 @@ function SearchRowSkeleton({shouldAnimate = true, fixedNumItems, gradientOpacity const styles = useThemeStyles(); const {windowWidth} = useWindowDimensions(); const {shouldUseNarrowLayout, isLargeScreenWidth} = useResponsiveLayout(); + useSkeletonSpan('SearchRowSkeleton'); if (shouldUseNarrowLayout) { return ( diff --git a/src/components/Skeletons/TabNavigatorSkeleton.tsx b/src/components/Skeletons/TabNavigatorSkeleton.tsx index 3b74b808eacd..b0f6affecc6a 100644 --- a/src/components/Skeletons/TabNavigatorSkeleton.tsx +++ b/src/components/Skeletons/TabNavigatorSkeleton.tsx @@ -4,10 +4,12 @@ import {Rect} from 'react-native-svg'; import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; function TabNavigatorSkeleton() { const styles = useThemeStyles(); const theme = useTheme(); + useSkeletonSpan('TabNavigatorSkeleton'); return ( diff --git a/src/components/Skeletons/TableRowSkeleton.tsx b/src/components/Skeletons/TableRowSkeleton.tsx index eb1237975dd4..38e39878d9c3 100644 --- a/src/components/Skeletons/TableRowSkeleton.tsx +++ b/src/components/Skeletons/TableRowSkeleton.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {Circle, Rect} from 'react-native-svg'; import useThemeStyles from '@hooks/useThemeStyles'; +import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; import ItemListSkeletonView from './ItemListSkeletonView'; type TableListItemSkeletonProps = { @@ -15,6 +16,7 @@ const longBarWidth = '124'; function TableListItemSkeleton({shouldAnimate = true, fixedNumItems, gradientOpacityEnabled = false}: TableListItemSkeletonProps) { const styles = useThemeStyles(); + useSkeletonSpan('TableRowSkeleton'); return ( (null); const styles = useThemeStyles(); const [pageWidth, setPageWidth] = React.useState(0); + useSkeletonSpan('UnreportedExpensesSkeleton'); useLayoutEffect(() => { containerRef.current?.measure((x, y, width) => { setPageWidth(width - 40); diff --git a/src/components/Skeletons/WorkspaceRowSkeleton.tsx b/src/components/Skeletons/WorkspaceRowSkeleton.tsx index 5c9f18ef4ef3..6c00cc3bbfb0 100644 --- a/src/components/Skeletons/WorkspaceRowSkeleton.tsx +++ b/src/components/Skeletons/WorkspaceRowSkeleton.tsx @@ -3,6 +3,7 @@ import {Rect} from 'react-native-svg'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; import variables from '@styles/variables'; import ItemListSkeletonView from './ItemListSkeletonView'; @@ -22,6 +23,7 @@ function WorkspaceRowSkeleton({shouldAnimate = true, fixedNumItems, gradientOpac const styles = useThemeStyles(); const {windowWidth} = useWindowDimensions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + useSkeletonSpan('WorkspaceRowSkeleton'); // We calculate the width of the sections on the skeleton by first calculating the skeleton view width // Then we subtract the width by 66, which is the x position of the first part. const partWidth = Math.floor((windowWidth - leftPaneWidth - gapWidth * 2 - 66) / 3); diff --git a/src/components/TransactionPreviewSkeletonView.tsx b/src/components/TransactionPreviewSkeletonView.tsx index 55ec76d0e086..cb3330581698 100644 --- a/src/components/TransactionPreviewSkeletonView.tsx +++ b/src/components/TransactionPreviewSkeletonView.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native'; import {Rect} from 'react-native-svg'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; import variables from '@styles/variables'; import SkeletonViewContentLoader from './SkeletonViewContentLoader'; @@ -13,6 +14,7 @@ type TransactionPreviewSkeletonViewProps = { function TransactionPreviewSkeletonView({transactionPreviewWidth}: TransactionPreviewSkeletonViewProps) { const theme = useTheme(); const styles = useThemeStyles(); + useSkeletonSpan('TransactionPreviewSkeletonView'); const height = variables.transactionPreviewSkeletonHeight; const widthOfTheLeftSkeleton = 120; diff --git a/src/libs/telemetry/useSkeletonSpan.ts b/src/libs/telemetry/useSkeletonSpan.ts index d6a4287c38d8..3346c9e28843 100644 --- a/src/libs/telemetry/useSkeletonSpan.ts +++ b/src/libs/telemetry/useSkeletonSpan.ts @@ -1,15 +1,15 @@ /** * This file contains logic related to tracking skeleton across the application. */ -import CONST from '@src/CONST'; -import {endSpan, startSpan} from '@libs/telemetry/activeSpans'; import {useEffect, useId, useMemo} from 'react'; +import {endSpan, startSpan} from '@libs/telemetry/activeSpans'; +import CONST from '@src/CONST'; function useSkeletonSpan(component: string) { const reactId = useId(); useEffect(() => { - const spanId = `${CONST.TELEMETRY.SPAN_SKELETON}_${component}_${reactId}` + const spanId = `${CONST.TELEMETRY.SPAN_SKELETON}_${component}_${reactId}`; startSpan(spanId, { op: CONST.TELEMETRY.SPAN_SKELETON, name: component, From 67710820b809a4a3421cf28f8fd34158ab3ca079 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Sat, 29 Nov 2025 14:09:15 +0100 Subject: [PATCH 3/9] lint related fixes --- src/components/AccountSwitcherSkeletonView/index.tsx | 2 +- src/components/AvatarSkeleton.tsx | 2 +- src/components/MoneyReportHeaderStatusBarSkeleton.tsx | 2 +- .../ReportActionsListLoadingSkeleton.tsx | 2 +- src/components/OptionsListSkeletonView.tsx | 2 +- src/components/ReportHeaderSkeletonView.tsx | 2 +- src/components/Skeletons/CardRowSkeleton.tsx | 2 +- src/components/Skeletons/MergeExpensesSkeleton.tsx | 2 +- src/components/Skeletons/SearchFiltersSkeleton.tsx | 2 +- src/components/Skeletons/SearchInputSelectionSkeleton.tsx | 2 +- src/components/Skeletons/SearchRowSkeleton.tsx | 2 +- src/components/Skeletons/TabNavigatorSkeleton.tsx | 2 +- src/components/Skeletons/TableRowSkeleton.tsx | 2 +- src/components/Skeletons/UnreportedExpensesSkeleton.tsx | 2 +- src/components/Skeletons/WorkspaceRowSkeleton.tsx | 2 +- src/components/TransactionPreviewSkeletonView.tsx | 2 +- src/libs/telemetry/useSkeletonSpan.ts | 8 ++++---- 17 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/components/AccountSwitcherSkeletonView/index.tsx b/src/components/AccountSwitcherSkeletonView/index.tsx index a99315120b45..3991396eae9d 100644 --- a/src/components/AccountSwitcherSkeletonView/index.tsx +++ b/src/components/AccountSwitcherSkeletonView/index.tsx @@ -6,7 +6,7 @@ import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; +import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan'; import CONST from '@src/CONST'; type AccountSwitcherSkeletonViewProps = { diff --git a/src/components/AvatarSkeleton.tsx b/src/components/AvatarSkeleton.tsx index c11a57b6c677..f3bf3c156fa5 100644 --- a/src/components/AvatarSkeleton.tsx +++ b/src/components/AvatarSkeleton.tsx @@ -3,7 +3,7 @@ import {Circle} from 'react-native-svg'; import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; -import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; +import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan'; import CONST from '@src/CONST'; import SkeletonViewContentLoader from './SkeletonViewContentLoader'; diff --git a/src/components/MoneyReportHeaderStatusBarSkeleton.tsx b/src/components/MoneyReportHeaderStatusBarSkeleton.tsx index c6f88dae19a5..a410885cbd43 100644 --- a/src/components/MoneyReportHeaderStatusBarSkeleton.tsx +++ b/src/components/MoneyReportHeaderStatusBarSkeleton.tsx @@ -3,7 +3,7 @@ import {View} from 'react-native'; import {Rect} from 'react-native-svg'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; +import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan'; import SkeletonViewContentLoader from './SkeletonViewContentLoader'; function MoneyReportHeaderStatusBarSkeleton() { diff --git a/src/components/MoneyRequestReportView/ReportActionsListLoadingSkeleton.tsx b/src/components/MoneyRequestReportView/ReportActionsListLoadingSkeleton.tsx index f7e6dd955f67..6c5d6de8c263 100644 --- a/src/components/MoneyRequestReportView/ReportActionsListLoadingSkeleton.tsx +++ b/src/components/MoneyRequestReportView/ReportActionsListLoadingSkeleton.tsx @@ -1,7 +1,7 @@ import React from 'react'; import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; -import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; +import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan'; function ReportActionsListLoadingSkeleton() { useSkeletonSpan('ReportActionsListLoadingSkeleton'); diff --git a/src/components/OptionsListSkeletonView.tsx b/src/components/OptionsListSkeletonView.tsx index 0b214f7dc2b2..e1701f63f6c9 100644 --- a/src/components/OptionsListSkeletonView.tsx +++ b/src/components/OptionsListSkeletonView.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {Circle, Rect} from 'react-native-svg'; import useThemeStyles from '@hooks/useThemeStyles'; -import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; +import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan'; import ItemListSkeletonView from './Skeletons/ItemListSkeletonView'; function getLinedWidth(index: number): string { diff --git a/src/components/ReportHeaderSkeletonView.tsx b/src/components/ReportHeaderSkeletonView.tsx index 4df82185e890..c5b60c228da1 100644 --- a/src/components/ReportHeaderSkeletonView.tsx +++ b/src/components/ReportHeaderSkeletonView.tsx @@ -5,7 +5,7 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; +import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan'; import CONST from '@src/CONST'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; diff --git a/src/components/Skeletons/CardRowSkeleton.tsx b/src/components/Skeletons/CardRowSkeleton.tsx index ead6e54eaad5..b39168a2e717 100644 --- a/src/components/Skeletons/CardRowSkeleton.tsx +++ b/src/components/Skeletons/CardRowSkeleton.tsx @@ -3,7 +3,7 @@ import {Circle, Rect} from 'react-native-svg'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; +import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan'; import variables from '@styles/variables'; import ItemListSkeletonView from './ItemListSkeletonView'; diff --git a/src/components/Skeletons/MergeExpensesSkeleton.tsx b/src/components/Skeletons/MergeExpensesSkeleton.tsx index 5b3480dd413e..b5dd1e7614e0 100644 --- a/src/components/Skeletons/MergeExpensesSkeleton.tsx +++ b/src/components/Skeletons/MergeExpensesSkeleton.tsx @@ -2,7 +2,7 @@ import React, {useCallback, useLayoutEffect, useRef} from 'react'; import {View} from 'react-native'; import {Rect} from 'react-native-svg'; import useThemeStyles from '@hooks/useThemeStyles'; -import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; +import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan'; import ItemListSkeletonView from './ItemListSkeletonView'; const barHeight = 7; diff --git a/src/components/Skeletons/SearchFiltersSkeleton.tsx b/src/components/Skeletons/SearchFiltersSkeleton.tsx index 9cd6bc0c791a..1c2b5e7d926b 100644 --- a/src/components/Skeletons/SearchFiltersSkeleton.tsx +++ b/src/components/Skeletons/SearchFiltersSkeleton.tsx @@ -4,7 +4,7 @@ import {Rect} from 'react-native-svg'; import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; +import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan'; type SearchFiltersSkeletonProps = { shouldAnimate?: boolean; diff --git a/src/components/Skeletons/SearchInputSelectionSkeleton.tsx b/src/components/Skeletons/SearchInputSelectionSkeleton.tsx index 084aafab162d..80a462e5c9c2 100644 --- a/src/components/Skeletons/SearchInputSelectionSkeleton.tsx +++ b/src/components/Skeletons/SearchInputSelectionSkeleton.tsx @@ -4,7 +4,7 @@ import {Rect} from 'react-native-svg'; import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; +import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan'; import variables from '@styles/variables'; function SearchInputSelectionSkeleton() { diff --git a/src/components/Skeletons/SearchRowSkeleton.tsx b/src/components/Skeletons/SearchRowSkeleton.tsx index 231e11de780f..9bb380e18c00 100644 --- a/src/components/Skeletons/SearchRowSkeleton.tsx +++ b/src/components/Skeletons/SearchRowSkeleton.tsx @@ -5,7 +5,7 @@ import {Circle, Rect} from 'react-native-svg'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; +import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ItemListSkeletonView from './ItemListSkeletonView'; diff --git a/src/components/Skeletons/TabNavigatorSkeleton.tsx b/src/components/Skeletons/TabNavigatorSkeleton.tsx index b0f6affecc6a..8597c861c0f3 100644 --- a/src/components/Skeletons/TabNavigatorSkeleton.tsx +++ b/src/components/Skeletons/TabNavigatorSkeleton.tsx @@ -4,7 +4,7 @@ import {Rect} from 'react-native-svg'; import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; +import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan'; function TabNavigatorSkeleton() { const styles = useThemeStyles(); diff --git a/src/components/Skeletons/TableRowSkeleton.tsx b/src/components/Skeletons/TableRowSkeleton.tsx index 38e39878d9c3..cb6da8d3ee53 100644 --- a/src/components/Skeletons/TableRowSkeleton.tsx +++ b/src/components/Skeletons/TableRowSkeleton.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {Circle, Rect} from 'react-native-svg'; import useThemeStyles from '@hooks/useThemeStyles'; -import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; +import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan'; import ItemListSkeletonView from './ItemListSkeletonView'; type TableListItemSkeletonProps = { diff --git a/src/components/Skeletons/UnreportedExpensesSkeleton.tsx b/src/components/Skeletons/UnreportedExpensesSkeleton.tsx index 787e19353d08..a74537f91c86 100644 --- a/src/components/Skeletons/UnreportedExpensesSkeleton.tsx +++ b/src/components/Skeletons/UnreportedExpensesSkeleton.tsx @@ -2,7 +2,7 @@ import React, {useCallback, useLayoutEffect, useRef} from 'react'; import {View} from 'react-native'; import {Rect} from 'react-native-svg'; import useThemeStyles from '@hooks/useThemeStyles'; -import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; +import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan'; import ItemListSkeletonView from './ItemListSkeletonView'; function getMessageSkeletonWidth(index: number) { diff --git a/src/components/Skeletons/WorkspaceRowSkeleton.tsx b/src/components/Skeletons/WorkspaceRowSkeleton.tsx index 6c00cc3bbfb0..c6d4fa465fd5 100644 --- a/src/components/Skeletons/WorkspaceRowSkeleton.tsx +++ b/src/components/Skeletons/WorkspaceRowSkeleton.tsx @@ -3,7 +3,7 @@ import {Rect} from 'react-native-svg'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; +import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan'; import variables from '@styles/variables'; import ItemListSkeletonView from './ItemListSkeletonView'; diff --git a/src/components/TransactionPreviewSkeletonView.tsx b/src/components/TransactionPreviewSkeletonView.tsx index cb3330581698..93de617d72ce 100644 --- a/src/components/TransactionPreviewSkeletonView.tsx +++ b/src/components/TransactionPreviewSkeletonView.tsx @@ -3,7 +3,7 @@ import {View} from 'react-native'; import {Rect} from 'react-native-svg'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {useSkeletonSpan} from '@libs/telemetry/useSkeletonSpan'; +import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan'; import variables from '@styles/variables'; import SkeletonViewContentLoader from './SkeletonViewContentLoader'; diff --git a/src/libs/telemetry/useSkeletonSpan.ts b/src/libs/telemetry/useSkeletonSpan.ts index 3346c9e28843..50ad55e4dcdb 100644 --- a/src/libs/telemetry/useSkeletonSpan.ts +++ b/src/libs/telemetry/useSkeletonSpan.ts @@ -1,9 +1,9 @@ /** * This file contains logic related to tracking skeleton across the application. */ -import {useEffect, useId, useMemo} from 'react'; -import {endSpan, startSpan} from '@libs/telemetry/activeSpans'; +import {useEffect, useId} from 'react'; import CONST from '@src/CONST'; +import {endSpan, startSpan} from './activeSpans'; function useSkeletonSpan(component: string) { const reactId = useId(); @@ -16,7 +16,7 @@ function useSkeletonSpan(component: string) { }); return () => endSpan(spanId); - }, []); + }, [component, reactId]); } -export {useSkeletonSpan}; +export default useSkeletonSpan; From 9dd1d7034c1b726e6ea0f9bc48b5ad6c3b7cb881 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Wed, 3 Dec 2025 14:08:18 +0100 Subject: [PATCH 4/9] Fix skeletons --- .../AccountSwitcherSkeletonView/index.tsx | 7 +++--- .../MoneyReportHeaderStatusBarSkeleton.tsx | 6 ++--- src/components/OptionsListSkeletonView.tsx | 7 +++--- src/components/ReportHeaderSkeletonView.tsx | 15 +++++-------- src/components/Skeletons/CardRowSkeleton.tsx | 22 ++++++++++++------- .../Skeletons/MergeExpensesSkeleton.tsx | 15 +++++-------- .../Skeletons/SearchFiltersSkeleton.tsx | 6 ++--- .../SearchInputSelectionSkeleton.tsx | 2 -- .../Skeletons/TabNavigatorSkeleton.tsx | 6 ++--- src/components/Skeletons/TableRowSkeleton.tsx | 6 ++--- .../Skeletons/UnreportedExpensesSkeleton.tsx | 12 ++++------ .../Skeletons/WorkspaceRowSkeleton.tsx | 21 ++++++------------ .../TransactionPreviewSkeletonView.tsx | 12 ++++------ 13 files changed, 53 insertions(+), 84 deletions(-) diff --git a/src/components/AccountSwitcherSkeletonView/index.tsx b/src/components/AccountSwitcherSkeletonView/index.tsx index 3991396eae9d..4e783be4fc8a 100644 --- a/src/components/AccountSwitcherSkeletonView/index.tsx +++ b/src/components/AccountSwitcherSkeletonView/index.tsx @@ -25,6 +25,7 @@ function AccountSwitcherSkeletonView({shouldAnimate = true, avatarSize = CONST.A const avatarPlaceholderSize = StyleUtils.getAvatarSize(avatarSize); const avatarPlaceholderRadius = avatarPlaceholderSize / 2; const startPositionX = avatarPlaceholderRadius; + const rectXTranslation = startPositionX + avatarPlaceholderRadius + styles.gap3.gap; return ( @@ -40,14 +41,12 @@ function AccountSwitcherSkeletonView({shouldAnimate = true, avatarSize = CONST.A r={avatarPlaceholderRadius} /> diff --git a/src/components/MoneyReportHeaderStatusBarSkeleton.tsx b/src/components/MoneyReportHeaderStatusBarSkeleton.tsx index a410885cbd43..63ec9573e0bb 100644 --- a/src/components/MoneyReportHeaderStatusBarSkeleton.tsx +++ b/src/components/MoneyReportHeaderStatusBarSkeleton.tsx @@ -19,14 +19,12 @@ function MoneyReportHeaderStatusBarSkeleton() { foregroundColor={theme.skeletonLHNOut} > diff --git a/src/components/OptionsListSkeletonView.tsx b/src/components/OptionsListSkeletonView.tsx index e1701f63f6c9..00af27f457da 100644 --- a/src/components/OptionsListSkeletonView.tsx +++ b/src/components/OptionsListSkeletonView.tsx @@ -39,6 +39,7 @@ function OptionsListSkeletonView({shouldAnimate = true, shouldStyleAsTable = fal gradientOpacityEnabled={gradientOpacityEnabled} renderSkeletonItem={({itemIndex}) => { const lineWidth = getLinedWidth(itemIndex); + const textStartX = shouldStyleAsTable ? '68' : '72'; return ( <> @@ -48,14 +49,12 @@ function OptionsListSkeletonView({shouldAnimate = true, shouldStyleAsTable = fal r="20" /> diff --git a/src/components/ReportHeaderSkeletonView.tsx b/src/components/ReportHeaderSkeletonView.tsx index c5b60c228da1..efc0194bd4ad 100644 --- a/src/components/ReportHeaderSkeletonView.tsx +++ b/src/components/ReportHeaderSkeletonView.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {View} from 'react-native'; import {Circle, Rect} from 'react-native-svg'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; @@ -8,7 +9,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan'; import CONST from '@src/CONST'; import Icon from './Icon'; -import * as Expensicons from './Icon/Expensicons'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; import SkeletonViewContentLoader from './SkeletonViewContentLoader'; @@ -22,6 +22,7 @@ function ReportHeaderSkeletonView({shouldAnimate = true, onBackButtonPress = () const styles = useThemeStyles(); const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const icons = useMemoizedLazyExpensifyIcons(['BackArrow'] as const); useSkeletonSpan('ReportHeaderSkeletonView'); const height = styles.headerBarHeight.height; const radius = 20; @@ -41,7 +42,7 @@ function ReportHeaderSkeletonView({shouldAnimate = true, onBackButtonPress = () > )} @@ -58,18 +59,12 @@ function ReportHeaderSkeletonView({shouldAnimate = true, onBackButtonPress = () r={radius} /> diff --git a/src/components/Skeletons/CardRowSkeleton.tsx b/src/components/Skeletons/CardRowSkeleton.tsx index b39168a2e717..76bd02311eab 100644 --- a/src/components/Skeletons/CardRowSkeleton.tsx +++ b/src/components/Skeletons/CardRowSkeleton.tsx @@ -42,15 +42,13 @@ function CardRowSkeleton({shouldAnimate = true, fixedNumItems, gradientOpacityEn r={20} /> @@ -59,16 +57,24 @@ function CardRowSkeleton({shouldAnimate = true, fixedNumItems, gradientOpacityEn <> diff --git a/src/components/Skeletons/MergeExpensesSkeleton.tsx b/src/components/Skeletons/MergeExpensesSkeleton.tsx index b5dd1e7614e0..6fce1cad7cb1 100644 --- a/src/components/Skeletons/MergeExpensesSkeleton.tsx +++ b/src/components/Skeletons/MergeExpensesSkeleton.tsx @@ -30,39 +30,34 @@ function MergeExpensesSkeleton({fixedNumItems, speed}: MergeExpensesSkeletonProp return ( <> diff --git a/src/components/Skeletons/SearchFiltersSkeleton.tsx b/src/components/Skeletons/SearchFiltersSkeleton.tsx index 1c2b5e7d926b..dacf34739b92 100644 --- a/src/components/Skeletons/SearchFiltersSkeleton.tsx +++ b/src/components/Skeletons/SearchFiltersSkeleton.tsx @@ -29,8 +29,7 @@ function SearchFiltersSkeleton({shouldAnimate = true}: SearchFiltersSkeletonProp diff --git a/src/components/Skeletons/SearchInputSelectionSkeleton.tsx b/src/components/Skeletons/SearchInputSelectionSkeleton.tsx index 80a462e5c9c2..f1c9f54bd5ba 100644 --- a/src/components/Skeletons/SearchInputSelectionSkeleton.tsx +++ b/src/components/Skeletons/SearchInputSelectionSkeleton.tsx @@ -21,8 +21,6 @@ function SearchInputSelectionSkeleton() { style={[styles.ml1]} > diff --git a/src/components/Skeletons/TabNavigatorSkeleton.tsx b/src/components/Skeletons/TabNavigatorSkeleton.tsx index 8597c861c0f3..ca04b4e9b094 100644 --- a/src/components/Skeletons/TabNavigatorSkeleton.tsx +++ b/src/components/Skeletons/TabNavigatorSkeleton.tsx @@ -21,8 +21,7 @@ function TabNavigatorSkeleton() { style={[styles.flex1, styles.ml4, styles.button, styles.highlightBG]} > @@ -35,8 +34,7 @@ function TabNavigatorSkeleton() { style={[styles.flex1, styles.mr4, styles.button, styles.appBG]} > diff --git a/src/components/Skeletons/TableRowSkeleton.tsx b/src/components/Skeletons/TableRowSkeleton.tsx index cb6da8d3ee53..c504472ecb7c 100644 --- a/src/components/Skeletons/TableRowSkeleton.tsx +++ b/src/components/Skeletons/TableRowSkeleton.tsx @@ -32,14 +32,12 @@ function TableListItemSkeleton({shouldAnimate = true, fixedNumItems, gradientOpa r="20" /> diff --git a/src/components/Skeletons/UnreportedExpensesSkeleton.tsx b/src/components/Skeletons/UnreportedExpensesSkeleton.tsx index a74537f91c86..545584ab196a 100644 --- a/src/components/Skeletons/UnreportedExpensesSkeleton.tsx +++ b/src/components/Skeletons/UnreportedExpensesSkeleton.tsx @@ -47,30 +47,26 @@ function UnreportedExpensesSkeleton({fixedNumberOfItems}: {fixedNumberOfItems?: return ( <> diff --git a/src/components/Skeletons/WorkspaceRowSkeleton.tsx b/src/components/Skeletons/WorkspaceRowSkeleton.tsx index c6d4fa465fd5..a5be700aa9aa 100644 --- a/src/components/Skeletons/WorkspaceRowSkeleton.tsx +++ b/src/components/Skeletons/WorkspaceRowSkeleton.tsx @@ -36,48 +36,41 @@ function WorkspaceRowSkeleton({shouldAnimate = true, fixedNumItems, gradientOpac renderSkeletonItem={() => ( <> {!shouldUseNarrowLayout && ( <> diff --git a/src/components/TransactionPreviewSkeletonView.tsx b/src/components/TransactionPreviewSkeletonView.tsx index 93de617d72ce..57366081aee5 100644 --- a/src/components/TransactionPreviewSkeletonView.tsx +++ b/src/components/TransactionPreviewSkeletonView.tsx @@ -31,20 +31,17 @@ function TransactionPreviewSkeletonView({transactionPreviewWidth}: TransactionPr foregroundColor={theme.skeletonLHNOut} > @@ -57,8 +54,7 @@ function TransactionPreviewSkeletonView({transactionPreviewWidth}: TransactionPr backgroundColor={theme.skeletonLHNIn} > From fedada1e66bcd2b7e67fc64e7d7e536b9979c682 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Wed, 3 Dec 2025 14:29:49 +0100 Subject: [PATCH 5/9] Fixes after AI review --- src/libs/telemetry/useSkeletonSpan.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/telemetry/useSkeletonSpan.ts b/src/libs/telemetry/useSkeletonSpan.ts index 50ad55e4dcdb..0c50ce32cadd 100644 --- a/src/libs/telemetry/useSkeletonSpan.ts +++ b/src/libs/telemetry/useSkeletonSpan.ts @@ -1,11 +1,11 @@ -/** - * This file contains logic related to tracking skeleton across the application. - */ import {useEffect, useId} from 'react'; import CONST from '@src/CONST'; import {endSpan, startSpan} from './activeSpans'; -function useSkeletonSpan(component: string) { +/** + * Create a span for a skeleton component. This helps identify "infinite skeleton" issues where loading states don't resolve. + */ +function useSkeletonSpan(component: string ) { const reactId = useId(); useEffect(() => { From 446f1093c77c8290840f7c3f6176188315c8527e Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Wed, 3 Dec 2025 14:37:47 +0100 Subject: [PATCH 6/9] pretty fix for prettier --- src/libs/telemetry/useSkeletonSpan.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/telemetry/useSkeletonSpan.ts b/src/libs/telemetry/useSkeletonSpan.ts index 0c50ce32cadd..84e9948673a3 100644 --- a/src/libs/telemetry/useSkeletonSpan.ts +++ b/src/libs/telemetry/useSkeletonSpan.ts @@ -5,7 +5,7 @@ import {endSpan, startSpan} from './activeSpans'; /** * Create a span for a skeleton component. This helps identify "infinite skeleton" issues where loading states don't resolve. */ -function useSkeletonSpan(component: string ) { +function useSkeletonSpan(component: string) { const reactId = useId(); useEffect(() => { From 0624c0778a2f291190eae89f569844a96d59e124 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Thu, 4 Dec 2025 08:31:52 +0100 Subject: [PATCH 7/9] Fix typecheck --- src/components/OptionsListSkeletonView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/OptionsListSkeletonView.tsx b/src/components/OptionsListSkeletonView.tsx index 00af27f457da..c07e5cb2e3a9 100644 --- a/src/components/OptionsListSkeletonView.tsx +++ b/src/components/OptionsListSkeletonView.tsx @@ -39,7 +39,7 @@ function OptionsListSkeletonView({shouldAnimate = true, shouldStyleAsTable = fal gradientOpacityEnabled={gradientOpacityEnabled} renderSkeletonItem={({itemIndex}) => { const lineWidth = getLinedWidth(itemIndex); - const textStartX = shouldStyleAsTable ? '68' : '72'; + const textStartX = shouldStyleAsTable ? 68 : 72; return ( <> From cba82f8defb32665b8eaa38493c9fcba0141f7ec Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 5 Dec 2025 13:55:07 +0100 Subject: [PATCH 8/9] Introduce minDurationFilter.ts --- src/CONST/index.ts | 5 ++++ src/libs/telemetry/activeSpans.ts | 22 ++++++++++++---- src/libs/telemetry/middlewares/index.ts | 3 ++- .../middlewares/minDurationFilter.ts | 25 +++++++++++++++++++ src/libs/telemetry/useSkeletonSpan.ts | 14 ++++++++--- 5 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 src/libs/telemetry/middlewares/minDurationFilter.ts diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 64a9ca7ce53a..1a70c3524078 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1700,6 +1700,11 @@ const CONST = { ATTRIBUTE_CANCELED: 'canceled', ATTRIBUTE_ROUTE_FROM: 'route_from', ATTRIBUTE_ROUTE_TO: 'route_to', + ATTRIBUTE_MIN_DURATION: 'min_duration', + ATTRIBUTE_FINISHED_MANUALLY: 'finished_manually', + CONFIG: { + SKELETON_MIN_DURATION: 10_000, + }, }, PRIORITY_MODE: { GSD: 'gsd', diff --git a/src/libs/telemetry/activeSpans.ts b/src/libs/telemetry/activeSpans.ts index d429a365b1f5..41ae7de8b917 100644 --- a/src/libs/telemetry/activeSpans.ts +++ b/src/libs/telemetry/activeSpans.ts @@ -4,15 +4,26 @@ import CONST from '@src/CONST'; const activeSpans = new Map>(); -function startSpan(spanId: string, options: StartSpanOptions) { +type StartSpanExtraOptions = Partial<{ + /** + * Minimum duration of the span in milliseconds. If the span is shorter than this duration, it will be discarded (filtered out) before sending to Sentry. + * + */ + minDuration: number; +}>; + +function startSpan(spanId: string, options: StartSpanOptions, extraOptions: StartSpanExtraOptions = {}): ReturnType | undefined { // End any existing span for this name cancelSpan(spanId); - const span = Sentry.startInactiveSpan(options); - if (span) { - activeSpans.set(spanId, span); + if (!span) { + return; + } + if (extraOptions.minDuration) { + span.setAttribute(CONST.TELEMETRY.ATTRIBUTE_MIN_DURATION, extraOptions.minDuration); } + activeSpans.set(spanId, span); return span; } @@ -24,6 +35,7 @@ function endSpan(spanId: string) { return; } span.setStatus({code: 1}); + span.setAttribute(CONST.TELEMETRY.ATTRIBUTE_FINISHED_MANUALLY, true); span.end(); activeSpans.delete(spanId); } @@ -32,7 +44,7 @@ function cancelSpan(spanId: string) { const span = activeSpans.get(spanId); span?.setAttribute(CONST.TELEMETRY.ATTRIBUTE_CANCELED, true); // In Sentry there are only OK or ERROR status codes. - // We treat canceled spans as OK so we have ability to properly track spans that are not finished at all (their status would be different) + // We treat canceled spans as OK, so we can properly track spans that are not finished at all (their status would be different) span?.setStatus({code: 1}); endSpan(spanId); } diff --git a/src/libs/telemetry/middlewares/index.ts b/src/libs/telemetry/middlewares/index.ts index 4f4f0ae3fbf6..290a2855995e 100644 --- a/src/libs/telemetry/middlewares/index.ts +++ b/src/libs/telemetry/middlewares/index.ts @@ -1,10 +1,11 @@ import type {EventHint, TransactionEvent} from '@sentry/core'; import emailDomainFilter from './emailDomainFilter'; import firebasePerformanceFilter from './firebasePerformanceFilter'; +import minDurationFilter from './minDurationFilter'; type TelemetryBeforeSend = (event: TransactionEvent, hint: EventHint) => TransactionEvent | null | Promise; -const middlewares: TelemetryBeforeSend[] = [emailDomainFilter, firebasePerformanceFilter]; +const middlewares: TelemetryBeforeSend[] = [emailDomainFilter, firebasePerformanceFilter, minDurationFilter]; function processBeforeSendTransactions(event: TransactionEvent, hint: EventHint): Promise { return middlewares.reduce( diff --git a/src/libs/telemetry/middlewares/minDurationFilter.ts b/src/libs/telemetry/middlewares/minDurationFilter.ts new file mode 100644 index 000000000000..5ce82f1c3b52 --- /dev/null +++ b/src/libs/telemetry/middlewares/minDurationFilter.ts @@ -0,0 +1,25 @@ +import CONST from '@src/CONST'; +import type {TelemetryBeforeSend} from './index'; + +const minDurationFilter: TelemetryBeforeSend = (event) => { + if (!event.spans) { + return event; + } + + const spans = event.spans.filter((span) => { + const minDuration = span.data?.[CONST.TELEMETRY.ATTRIBUTE_MIN_DURATION]; + if (!minDuration || typeof minDuration !== 'number' || Number.isNaN(minDuration)) { + return true; + } + if (!span.timestamp) { + return true; + } + + const duration = (span.timestamp - span.start_timestamp) * 1000; + + return duration >= minDuration; + }); + return {...event, spans}; +}; + +export default minDurationFilter; diff --git a/src/libs/telemetry/useSkeletonSpan.ts b/src/libs/telemetry/useSkeletonSpan.ts index 84e9948673a3..83cdcee604db 100644 --- a/src/libs/telemetry/useSkeletonSpan.ts +++ b/src/libs/telemetry/useSkeletonSpan.ts @@ -10,10 +10,16 @@ function useSkeletonSpan(component: string) { useEffect(() => { const spanId = `${CONST.TELEMETRY.SPAN_SKELETON}_${component}_${reactId}`; - startSpan(spanId, { - op: CONST.TELEMETRY.SPAN_SKELETON, - name: component, - }); + startSpan( + spanId, + { + op: CONST.TELEMETRY.SPAN_SKELETON, + name: component, + }, + { + minDuration: CONST.TELEMETRY.CONFIG.SKELETON_MIN_DURATION, + }, + ); return () => endSpan(spanId); }, [component, reactId]); From 550af94e7916179eea6a1c7d6c2d9fd098808b0e Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 5 Dec 2025 14:20:01 +0100 Subject: [PATCH 9/9] Introduce minDurationFilter.ts --- src/libs/telemetry/activeSpans.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/libs/telemetry/activeSpans.ts b/src/libs/telemetry/activeSpans.ts index 41ae7de8b917..8ccf60816570 100644 --- a/src/libs/telemetry/activeSpans.ts +++ b/src/libs/telemetry/activeSpans.ts @@ -12,14 +12,11 @@ type StartSpanExtraOptions = Partial<{ minDuration: number; }>; -function startSpan(spanId: string, options: StartSpanOptions, extraOptions: StartSpanExtraOptions = {}): ReturnType | undefined { +function startSpan(spanId: string, options: StartSpanOptions, extraOptions: StartSpanExtraOptions = {}) { // End any existing span for this name cancelSpan(spanId); const span = Sentry.startInactiveSpan(options); - if (!span) { - return; - } if (extraOptions.minDuration) { span.setAttribute(CONST.TELEMETRY.ATTRIBUTE_MIN_DURATION, extraOptions.minDuration); }