Skip to content

Commit 40a8a01

Browse files
authored
Merge pull request #89871 from software-mansion-labs/kuba_nowakowski/fix/manual-tab-switch-span-metrics
Adjust sentry tab switching spans to tab navigator
2 parents b3b7460 + 28709f1 commit 40a8a01

5 files changed

Lines changed: 74 additions & 14 deletions

File tree

src/CONST/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2094,6 +2094,7 @@ const CONST = {
20942094
ATTRIBUTE_MIN_DURATION: 'min_duration',
20952095
ATTRIBUTE_FINISHED_MANUALLY: 'finished_manually',
20962096
ATTRIBUTE_IS_WARM: 'is_warm',
2097+
ATTRIBUTE_LAZY_TAB_FALLBACK_SHOWN: 'lazy_tab_fallback_shown',
20972098
ATTRIBUTE_WAS_LIST_EMPTY: 'was_list_empty',
20982099
ATTRIBUTE_SKELETON_PREFIX: 'skeleton.',
20992100
ATTRIBUTE_SCENARIO: 'scenario',

src/components/Search/index.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,6 +1575,15 @@ function Search({
15751575
onContentReady?.();
15761576
}, [sortedData, onContentReady]);
15771577

1578+
// Empty deps so this fires only on blur — merging with the body would cancel the span on every shouldShowLoadingState flip.
1579+
useFocusEffect(
1580+
useCallback(() => {
1581+
return () => {
1582+
cancelSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS);
1583+
};
1584+
}, []),
1585+
);
1586+
15781587
// Reset before conditional returns. Only cancelNavigationSpans (error/empty paths)
15791588
// sets it to true. Must happen during render since it coordinates with the
15801589
// dep-free useEffect above — see comment on didBailToFallbackState.

src/libs/Navigation/AppNavigator/Navigators/TabNavigator.tsx

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
1111
import useTheme from '@hooks/useTheme';
1212
import useThemeStyles from '@hooks/useThemeStyles';
1313
import type {TabNavigatorParamList} from '@libs/Navigation/types';
14+
import {getSpan} from '@libs/telemetry/activeSpans';
15+
import CONST from '@src/CONST';
1416
import NAVIGATORS from '@src/NAVIGATORS';
1517
import SCREENS from '@src/SCREENS';
1618
import TabNavigatorBar from './TabNavigatorBar';
@@ -21,20 +23,36 @@ const LazySearchFullscreenNavigator = lazy(() => import('./SearchFullscreenNavig
2123
const LazySettingsSplitNavigator = lazy(() => import('./SettingsSplitNavigator'));
2224
const LazyWorkspaceNavigator = lazy(() => import('./WorkspaceNavigator'));
2325

24-
function withSuspense<P extends Record<string, unknown>>(LazyComponent: React.LazyExoticComponent<React.ComponentType<P>>) {
26+
type LazyFallbackProps = {
27+
/** Sentry span to tag when this fallback renders. */
28+
tabSpanName?: string;
29+
};
30+
31+
function LazyFallback({tabSpanName}: LazyFallbackProps) {
32+
const styles = useThemeStyles();
33+
34+
// Lets Sentry split slow tab navigations into "lazy chunk fetch" vs "screen render" buckets.
35+
useEffect(() => {
36+
if (!tabSpanName) {
37+
return;
38+
}
39+
getSpan(tabSpanName)?.setAttribute(CONST.TELEMETRY.ATTRIBUTE_LAZY_TAB_FALLBACK_SHOWN, true);
40+
}, [tabSpanName]);
41+
42+
return (
43+
<View style={[styles.flex1, styles.justifyContentCenter, styles.alignItemsCenter, styles.appBG]}>
44+
<ActivityIndicator
45+
size="large"
46+
reasonAttributes={{context: 'TabNavigator.LazyTab'}}
47+
/>
48+
</View>
49+
);
50+
}
51+
52+
function withSuspense<P extends Record<string, unknown>>(LazyComponent: React.LazyExoticComponent<React.ComponentType<P>>, tabSpanName?: string) {
2553
function SuspenseWrapper(props: P) {
26-
const styles = useThemeStyles();
2754
return (
28-
<Suspense
29-
fallback={
30-
<View style={[styles.flex1, styles.justifyContentCenter, styles.alignItemsCenter, styles.appBG]}>
31-
<ActivityIndicator
32-
size="large"
33-
reasonAttributes={{context: 'TabNavigator.LazyTab'}}
34-
/>
35-
</View>
36-
}
37-
>
55+
<Suspense fallback={<LazyFallback tabSpanName={tabSpanName} />}>
3856
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
3957
<LazyComponent {...props} />
4058
</Suspense>
@@ -44,8 +62,8 @@ function withSuspense<P extends Record<string, unknown>>(LazyComponent: React.La
4462
}
4563

4664
const HomePageScreen = withSuspense(LazyHomePage);
47-
const ReportsSplitNavigatorScreen = withSuspense(LazyReportsSplitNavigator);
48-
const SearchFullscreenNavigatorScreen = withSuspense(LazySearchFullscreenNavigator);
65+
const ReportsSplitNavigatorScreen = withSuspense(LazyReportsSplitNavigator, CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB);
66+
const SearchFullscreenNavigatorScreen = withSuspense(LazySearchFullscreenNavigator, CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS);
4967
const SettingsSplitNavigatorScreen = withSuspense(LazySettingsSplitNavigator);
5068
const WorkspaceNavigatorScreen = withSuspense(LazyWorkspaceNavigator);
5169

src/libs/Navigation/AppNavigator/Navigators/TabNavigatorBar.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
1111
import useStyleUtils from '@hooks/useStyleUtils';
1212
import useThemeStyles from '@hooks/useThemeStyles';
1313
import getFocusedLeafScreenName from '@libs/Navigation/helpers/getFocusedLeafScreenName';
14+
import cancelTabNavigationSpans from '@libs/telemetry/cancelTabNavigationSpans';
15+
import CONST from '@src/CONST';
1416
import NAVIGATORS from '@src/NAVIGATORS';
1517
import SCREENS from '@src/SCREENS';
1618

@@ -22,6 +24,11 @@ const ROUTE_TO_NAVIGATION_TAB: Record<string, ValueOf<typeof NAVIGATION_TABS>> =
2224
[NAVIGATORS.WORKSPACE_NAVIGATOR]: NAVIGATION_TABS.WORKSPACES,
2325
};
2426

27+
const NAVIGATION_TAB_TO_SPAN: Partial<Record<ValueOf<typeof NAVIGATION_TABS>, string>> = {
28+
[NAVIGATION_TABS.INBOX]: CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB,
29+
[NAVIGATION_TABS.SEARCH]: CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS,
30+
};
31+
2532
// Count as tab-root when they surface as the resolved leaf.
2633
const TAB_WRAPPER_NAVIGATORS = new Set<string>([
2734
NAVIGATORS.REPORTS_SPLIT_NAVIGATOR,
@@ -95,6 +102,12 @@ function TabNavigatorBar({state}: Pick<BottomTabBarProps, 'state'>) {
95102
return () => cancelAnimationFrame(frameId);
96103
}, [stateKey]);
97104

105+
// Cancel any in-flight tab-navigation span that doesn't match the new focused tab.
106+
// The span for the new tab is started by the tab button before navigation, so we keep it via `except`.
107+
useEffect(() => {
108+
cancelTabNavigationSpans(NAVIGATION_TAB_TO_SPAN[selectedTab]);
109+
}, [selectedTab]);
110+
98111
const isHidden = shouldHide || (shouldApplyDelay && animationDoneKey !== stateKey);
99112

100113
if (shouldUseNarrowLayout) {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import CONST from '@src/CONST';
2+
import {cancelSpan} from './activeSpans';
3+
4+
const TAB_NAVIGATION_SPAN_IDS = [CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS, CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB] as const;
5+
6+
/**
7+
* Cancels in-flight tab-navigation spans so an abandoned tap doesn't leave one ticking until the user returns.
8+
* Pass `except` to preserve the span for the tab the user is now on.
9+
*/
10+
function cancelTabNavigationSpans(except?: string) {
11+
for (const id of TAB_NAVIGATION_SPAN_IDS) {
12+
if (id === except) {
13+
continue;
14+
}
15+
cancelSpan(id);
16+
}
17+
}
18+
19+
export default cancelTabNavigationSpans;

0 commit comments

Comments
 (0)