diff --git a/src/analytics/metrics/types.ts b/src/analytics/metrics/types.ts index 91bbefd13a..6e35ce317d 100644 --- a/src/analytics/metrics/types.ts +++ b/src/analytics/metrics/types.ts @@ -60,6 +60,20 @@ export type Events = { 'router:navigate': { from?: string } + 'nav:click': { + item: + | 'home' + | 'search' + | 'chat' + | 'notifications' + | 'profile' + | 'feeds' + | 'lists' + | 'saved' + | 'settings' + | 'menu' + surface: 'bottomBar' | 'drawer' | 'drawerHeader' | 'topBar' | 'leftNav' + } 'deepLink:referrerReceived': { to: string referrer: string diff --git a/src/components/Layout/Header/index.tsx b/src/components/Layout/Header/index.tsx index 3fce67aa26..f2f5ba122f 100644 --- a/src/components/Layout/Header/index.tsx +++ b/src/components/Layout/Header/index.tsx @@ -30,6 +30,7 @@ import { } from '#/components/Layout/const' import {ScrollbarOffsetContext} from '#/components/Layout/context' import {Text} from '#/components/Typography' +import {useAnalytics} from '#/analytics' import {IS_IOS} from '#/env' export function Outer({ @@ -150,13 +151,15 @@ export function BackButton({onPress, style, ...props}: Partial) { export function MenuButton() { const {_} = useLingui() + const ax = useAnalytics() const setDrawerOpen = useSetDrawerOpen() const {gtMobile} = useBreakpoints() const onPress = useCallback(() => { + ax.metric('nav:click', {item: 'menu', surface: 'topBar'}) Keyboard.dismiss() setDrawerOpen(true) - }, [setDrawerOpen]) + }, [setDrawerOpen, ax]) return gtMobile ? null : ( diff --git a/src/lib/routes/tab-to-nav-item.ts b/src/lib/routes/tab-to-nav-item.ts new file mode 100644 index 0000000000..38d58b25a4 --- /dev/null +++ b/src/lib/routes/tab-to-nav-item.ts @@ -0,0 +1,19 @@ +import {type Events} from '#/analytics/metrics/types' + +export type SharedNavTab = + | 'Home' + | 'Search' + | 'Messages' + | 'Notifications' + | 'MyProfile' + +export const TAB_TO_NAV_ITEM: Record< + SharedNavTab, + Events['nav:click']['item'] +> = { + Home: 'home', + Search: 'search', + Messages: 'chat', + Notifications: 'notifications', + MyProfile: 'profile', +} diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index 55eabf336a..7624b585f6 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -190,7 +190,7 @@ const GalleryItem = ({ return ( { + ax.metric('nav:click', {item: 'feeds', surface: 'topBar'}) + }} style={[a.justify_center]}> diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx index 3cf91ce7d6..628514bd53 100644 --- a/src/view/com/home/HomeHeaderLayoutMobile.tsx +++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx @@ -19,6 +19,7 @@ import {ButtonIcon} from '#/components/Button' import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag' import * as Layout from '#/components/Layout' import {Link} from '#/components/Link' +import {useAnalytics} from '#/analytics' import {IS_DEV, IS_LIQUID_GLASS} from '#/env' export function HomeHeaderLayoutMobile({ @@ -29,6 +30,7 @@ export function HomeHeaderLayoutMobile({ }) { const t = useTheme() const {_} = useLingui() + const ax = useAnalytics() const {headerHeight} = useShellLayout() const insets = useSafeAreaInsets() const headerMinimalShellTransform = useHomeHeaderTransform() @@ -84,6 +86,9 @@ export function HomeHeaderLayoutMobile({ variant="ghost" color="secondary" shape="square" + onPress={() => { + ax.metric('nav:click', {item: 'feeds', surface: 'topBar'}) + }} style={[ a.justify_center, {marginRight: -Layout.BUTTON_VISUAL_ALIGNMENT_OFFSET}, diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 88da23875d..10e4af037f 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -10,6 +10,7 @@ import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants' import {type PressableScale} from '#/lib/custom-animations/PressableScale' import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState' import {getTabState, TabState} from '#/lib/routes/helpers' +import {type SharedNavTab, TAB_TO_NAV_ITEM} from '#/lib/routes/tab-to-nav-item' import {type NavigationProp} from '#/lib/routes/types' import {sanitizeHandle} from '#/lib/strings/handles' import {colors} from '#/lib/styles' @@ -55,6 +56,7 @@ import { import {InlineLinkText} from '#/components/Link' import {ProfileBadges} from '#/components/ProfileBadges' import {Text} from '#/components/Typography' +import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' import {useActorStatus} from '#/features/liveNow' @@ -138,6 +140,7 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => { const insets = useSafeAreaInsets() const setDrawerOpen = useSetDrawerOpen() const navigation = useNavigation() + const ax = useAnalytics() const { isAtHome, isAtSearch, @@ -153,7 +156,11 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => { // = const onPressTab = useCallback( - (tab: 'Home' | 'Search' | 'Messages' | 'Notifications' | 'MyProfile') => { + (tab: SharedNavTab, surface: 'drawer' | 'drawerHeader' = 'drawer') => { + ax.metric('nav:click', { + item: TAB_TO_NAV_ITEM[tab], + surface, + }) const state = navigation.getState() setDrawerOpen(false) if (IS_WEB) { @@ -190,7 +197,7 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => { } } }, - [navigation, setDrawerOpen, currentAccount], + [navigation, setDrawerOpen, currentAccount, ax], ) const onPressHome = useCallback(() => onPressTab('Home'), [onPressTab]) @@ -211,25 +218,33 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => { onPressTab('MyProfile') }, [onPressTab]) + const onPressDrawerHeaderProfile = useCallback(() => { + onPressTab('MyProfile', 'drawerHeader') + }, [onPressTab]) + const onPressMyFeeds = useCallback(() => { + ax.metric('nav:click', {item: 'feeds', surface: 'drawer'}) navigation.navigate('Feeds') setDrawerOpen(false) - }, [navigation, setDrawerOpen]) + }, [navigation, setDrawerOpen, ax]) const onPressLists = useCallback(() => { + ax.metric('nav:click', {item: 'lists', surface: 'drawer'}) navigation.navigate('Lists') setDrawerOpen(false) - }, [navigation, setDrawerOpen]) + }, [navigation, setDrawerOpen, ax]) const onPressBookmarks = useCallback(() => { + ax.metric('nav:click', {item: 'saved', surface: 'drawer'}) navigation.navigate('Bookmarks') setDrawerOpen(false) - }, [navigation, setDrawerOpen]) + }, [navigation, setDrawerOpen, ax]) const onPressSettings = useCallback(() => { + ax.metric('nav:click', {item: 'settings', surface: 'drawer'}) navigation.navigate('Settings') setDrawerOpen(false) - }, [navigation, setDrawerOpen]) + }, [navigation, setDrawerOpen, ax]) const onPressFeedback = useCallback(() => { Linking.openURL( @@ -265,7 +280,7 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => { {hasSession && currentAccount ? ( ) : ( diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index c61a13c083..e6a1eeaad5 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -17,6 +17,7 @@ import {useMinimalShellFooterTransform} from '#/lib/hooks/useMinimalShellTransfo import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState' import {clamp} from '#/lib/numbers' import {getTabState, TabState} from '#/lib/routes/helpers' +import {type SharedNavTab, TAB_TO_NAV_ITEM} from '#/lib/routes/tab-to-nav-item' import {emitSoftReset} from '#/state/events' import {useUnreadMessageCount} from '#/state/queries/messages/list-conversations' import {useUnreadNotifications} from '#/state/queries/notifications/unread' @@ -50,16 +51,16 @@ import { } from '#/components/icons/Message' import {Text} from '#/components/Typography' import {useAgeAssurance} from '#/ageAssurance' +import {useAnalytics} from '#/analytics' import {useActorStatus} from '#/features/liveNow' import {useDemoMode} from '#/storage/hooks/demo-mode' import {styles} from './BottomBarStyles' -type TabOptions = 'Home' | 'Search' | 'Messages' | 'Notifications' | 'MyProfile' - export function BottomBar({navigation}: BottomTabBarProps) { const {hasSession, currentAccount} = useSession() const t = useTheme() const {_} = useLingui() + const ax = useAnalytics() const safeAreaInsets = useSafeAreaInsets() const {footerHeight} = useShellLayout() const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile, isAtMessages} = @@ -89,7 +90,11 @@ export function BottomBar({navigation}: BottomTabBarProps) { }, [requestSwitchToAccount, closeAllActiveElements]) const onPressTab = useCallback( - (tab: TabOptions) => { + (tab: SharedNavTab) => { + ax.metric('nav:click', { + item: TAB_TO_NAV_ITEM[tab], + surface: 'bottomBar', + }) const state = navigation.getState() const tabState = getTabState(state, tab) if (tabState === TabState.InsideAtRoot) { @@ -117,7 +122,7 @@ export function BottomBar({navigation}: BottomTabBarProps) { dedupe(() => navigation.navigate(`${tab}Tab`)) } }, - [navigation, dedupe], + [navigation, dedupe, ax], ) const onPressHome = useCallback(() => onPressTab('Home'), [onPressTab]) const onPressSearch = useCallback(() => onPressTab('Search'), [onPressTab]) diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx index 2bf9807e43..e98158fe6c 100644 --- a/src/view/shell/bottom-bar/BottomBarWeb.tsx +++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx @@ -44,8 +44,11 @@ import { } from '#/components/icons/Message' import {Text} from '#/components/Typography' import {useAgeAssurance} from '#/ageAssurance' +import {useAnalytics} from '#/analytics' import {styles} from './BottomBarStyles' +type NavItemValue = 'home' | 'search' | 'chat' | 'notifications' | 'profile' + export function BottomBarWeb() { const {_} = useLingui() const {hasSession, currentAccount} = useSession() @@ -96,7 +99,7 @@ export function BottomBarWeb() { onLayout={event => footerHeight.set(event.nativeEvent.layout.height)}> {hasSession ? ( <> - + {({isActive}) => { const Icon = isActive ? HomeFilled : Home return ( @@ -108,7 +111,7 @@ export function BottomBarWeb() { ) }} - + {({isActive}) => { const Icon = isActive ? MagnifyingGlassFilled : MagnifyingGlass return ( @@ -126,6 +129,7 @@ export function BottomBarWeb() { {({isActive}) => { const Icon = isActive ? BellFilled : Bell @@ -174,6 +179,7 @@ export function BottomBarWeb() { }) : '/' } + navItem="profile" onLongPress={onLongPressProfile}> {({isActive}) => ( @@ -257,12 +263,22 @@ const NavItem: React.FC<{ children: (props: {isActive: boolean}) => React.ReactNode href: string routeName: string + navItem: NavItemValue hasNew?: boolean notificationCount?: string onLongPress?: () => void -}> = ({children, href, routeName, hasNew, notificationCount, onLongPress}) => { +}> = ({ + children, + href, + routeName, + navItem, + hasNew, + notificationCount, + onLongPress, +}) => { const t = useTheme() const {_} = useLingui() + const ax = useAnalytics() const {currentAccount} = useSession() const currentRoute = useNavigationState(state => { if (!state) { @@ -271,6 +287,10 @@ const NavItem: React.FC<{ return getCurrentRoute(state) }) + const onBeforePress = useCallback(() => { + ax.metric('nav:click', {item: navItem, surface: 'bottomBar'}) + }, [ax, navItem]) + // Checks whether we're on someone else's profile const isOnDifferentProfile = currentRoute.name === 'Profile' && @@ -295,6 +315,7 @@ const NavItem: React.FC<{ aria-role="link" aria-label={routeName} accessible={true} + onBeforePress={onBeforePress} onLongPress={onLongPress}> {children({isActive})} {notificationCount ? ( diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index 5a3321f060..600f68e088 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -83,6 +83,8 @@ import * as Menu from '#/components/Menu' import * as Prompt from '#/components/Prompt' import {Text} from '#/components/Typography' import {useAgeAssurance} from '#/ageAssurance' +import {useAnalytics} from '#/analytics' +import {type Events} from '#/analytics/metrics/types' import {useActorStatus} from '#/features/liveNow' import {router} from '#/routes' import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army' @@ -389,10 +391,20 @@ interface NavItemProps { } label: string minimal: boolean + navItem: Events['nav:click']['item'] } -function NavItem({count, hasNew, href, icons, label, minimal}: NavItemProps) { +function NavItem({ + count, + hasNew, + href, + icons, + label, + minimal, + navItem, +}: NavItemProps) { const t = useTheme() const {t: l} = useLingui() + const ax = useAnalytics() const {currentAccount} = useSession() const [pathName] = useMemo(() => router.matchPath(href), [href]) @@ -412,6 +424,7 @@ function NavItem({count, hasNew, href, icons, label, minimal}: NavItemProps) { const navigation = useNavigation() const onPressWrapped = useCallback( (e: React.MouseEvent) => { + ax.metric('nav:click', {item: navItem, surface: 'leftNav'}) if (e.ctrlKey || e.metaKey || e.altKey) { return } @@ -424,7 +437,7 @@ function NavItem({count, hasNew, href, icons, label, minimal}: NavItemProps) { navigation.navigate(screen, params, {pop: true}) } }, - [navigation, href, isCurrent], + [navigation, href, isCurrent, ax, navItem], ) const Icon = isCurrent || isRelated ? icons.active : icons.inactive @@ -652,6 +665,7 @@ export function DesktopLeftNav({routeName}: {routeName: string}) {