[APP-1782] Analytics migration (#9734)

* WIP

* Clean up growthbook code, integrate into init and sessions

* Move everything out of React

* Add metrics client

* Move to separate file

* Shared metadata cache

* Ensure we update metadata when session ID changes

* Ensure userMetadata is cleared when logging out

* WIP revamp

* Integrate feature gates into analytics context

* Clean up old code

* Fix useMeta util

* Some comments and cleanup

* Add logger to base analytics context

* Refactor current route handling

* Rip out LogEvent from navigation

* Update tracking endpoint

* Migrate toClout

* Clear out statsig client

* Add todo, reset logger readme

* Ope fix statsig noop

* Refactor logging in feed-feedback, add debug logging to metrics client

* Remove LogEvents alias for Metrics

* Prefer root package export

* Remove Metrics alias from logger

* [APP-1782] Migrate to new analytics APIs (#9735)

* Migrate logEvent to useAnalytics

* Migrate logger.metric to useAnalytics

* Migrate tricky spot, fix types

* Migrate remaining tricky spot

* Missed one

* Remove metric() from logger

* Migrate useGate to useAnalytics

* Remove all other StatSig mentions

* Update event payload

* Update logger tests

* Mock expo method

* Fix session ID bug

* Add session ID test

* Add test for metrics client

* Clarify intent

* Clean up core analytics file

* Clean up the call once utils

* Fix TODO

* Fix TODO

* Fix TODO

* Fix TODO

* Fix TODO

* Remove debug code

* Fix navigation context

* OK nav context is not working, todo

* Checkpoint: works but feels hacky

* Fix navigation context issue

* Improve feature API

* Improve metric logging

* Update logger tests
This commit is contained in:
Eric Bailey
2026-01-22 15:33:45 -06:00
committed by GitHub
parent 8614e5e105
commit 25a1ed1209
190 changed files with 2358 additions and 1689 deletions
+26 -14
View File
@@ -20,7 +20,6 @@ import {
type CommonNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {logger} from '#/logger'
import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation'
import {useBookmarksQuery} from '#/state/queries/bookmarks/useBookmarksQuery'
import {useSetMinimalShellMode} from '#/state/shell'
@@ -37,18 +36,20 @@ import {ListFooter} from '#/components/Lists'
import * as Skele from '#/components/Skeleton'
import * as toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_IOS} from '#/env'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Bookmarks'>
export function BookmarksScreen({}: Props) {
const setMinimalShellMode = useSetMinimalShellMode()
const ax = useAnalytics()
useFocusEffect(
useCallback(() => {
setMinimalShellMode(false)
logger.metric('bookmarks:view', {})
}, [setMinimalShellMode]),
ax.metric('bookmarks:view', {})
}, [setMinimalShellMode, ax]),
)
return (
@@ -144,7 +145,7 @@ function BookmarksInner() {
key: bookmark.item.uri,
bookmark: {
...bookmark,
item: bookmark.item as $Typed<AppBskyFeedDefs.NotFoundPost>,
item: bookmark.item,
},
})
}
@@ -154,7 +155,7 @@ function BookmarksInner() {
key: bookmark.item.uri,
bookmark: {
...bookmark,
item: bookmark.item as $Typed<AppBskyFeedDefs.PostView>,
item: bookmark.item,
},
})
}
@@ -270,6 +271,25 @@ function BookmarkNotFound({
)
}
function BookmarkItem({
item,
hideTopBorder,
}: {
item: Extract<ListItem, {type: 'bookmark'}>
hideTopBorder: boolean
}) {
const ax = useAnalytics()
return (
<Post
post={item.bookmark.item}
hideTopBorder={hideTopBorder}
onBeforePress={() => {
ax.metric('bookmarks:post-clicked', {})
}}
/>
)
}
function BookmarksEmpty() {
const t = useTheme()
const {_} = useLingui()
@@ -301,15 +321,7 @@ function renderItem({item, index}: {item: ListItem; index: number}) {
return <BookmarksEmpty />
}
case 'bookmark': {
return (
<Post
post={item.bookmark.item}
hideTopBorder={index === 0}
onBeforePress={() => {
logger.metric('bookmarks:post-clicked', {})
}}
/>
)
return <BookmarkItem item={item} hideTopBorder={index === 0} />
}
case 'bookmarkNotFound': {
return (
+3 -2
View File
@@ -3,7 +3,6 @@ import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {type SessionAccount, useSession, useSessionApi} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
@@ -12,6 +11,7 @@ import {atoms as a, web} from '#/alf'
import {AccountList} from '#/components/AccountList'
import {Button, ButtonText} from '#/components/Button'
import * as TextField from '#/components/forms/TextField'
import {useAnalytics} from '#/analytics'
import {FormContainer} from './FormContainer'
export const ChooseAccountForm = ({
@@ -23,6 +23,7 @@ export const ChooseAccountForm = ({
}) => {
const [pendingDid, setPendingDid] = React.useState<string | null>(null)
const {_} = useLingui()
const ax = useAnalytics()
const {currentAccount} = useSession()
const {resumeSession} = useSessionApi()
const {setShowLoggedOut} = useLoggedOutViewControls()
@@ -46,7 +47,7 @@ export const ChooseAccountForm = ({
try {
setPendingDid(account.did)
await resumeSession(account, true)
logEvent('account:loggedIn', {
ax.metric('account:loggedIn', {
logContext: 'ChooseAccountForm',
withPassword: false,
})
+6 -6
View File
@@ -3,9 +3,7 @@ import {ActivityIndicator, View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logEvent} from '#/lib/statsig/statsig'
import {isNetworkError} from '#/lib/strings/errors'
import {cleanError} from '#/lib/strings/errors'
import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {checkAndFormatResetCode} from '#/lib/strings/password'
import {logger} from '#/logger'
import {Agent} from '#/state/session/agent'
@@ -16,6 +14,7 @@ import * as TextField from '#/components/forms/TextField'
import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {FormContainer} from './FormContainer'
export const SetNewPasswordForm = ({
@@ -33,6 +32,7 @@ export const SetNewPasswordForm = ({
}) => {
const {_} = useLingui()
const t = useTheme()
const ax = useAnalytics()
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [resetCode, setResetCode] = useState<string>('')
@@ -49,7 +49,7 @@ export const SetNewPasswordForm = ({
msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
),
)
logEvent('signin:passwordResetFailure', {})
ax.metric('signin:passwordResetFailure', {})
return
}
@@ -69,11 +69,11 @@ export const SetNewPasswordForm = ({
password,
})
onPasswordSet()
logEvent('signin:passwordResetSuccess', {})
ax.metric('signin:passwordResetSuccess', {})
} catch (e: any) {
const errMsg = e.toString()
logger.warn('Failed to set new password', {error: e})
logEvent('signin:passwordResetFailure', {})
ax.metric('signin:passwordResetFailure', {})
setIsProcessing(false)
if (isNetworkError(e)) {
setError(
+6 -5
View File
@@ -5,7 +5,6 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {DEFAULT_SERVICE} from '#/lib/constants'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {useServiceQuery} from '#/state/queries/service'
import {type SessionAccount, useSession} from '#/state/session'
@@ -17,6 +16,7 @@ import {PasswordUpdatedForm} from '#/screens/Login/PasswordUpdatedForm'
import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm'
import {atoms as a, native} from '#/alf'
import {ScreenTransition} from '#/components/ScreenTransition'
import {useAnalytics} from '#/analytics'
import {ChooseAccountForm} from './ChooseAccountForm'
enum Forms {
@@ -64,6 +64,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
'Forward' | 'Backward'
>('Forward')
const ax = useAnalytics()
const {
data: serviceDescription,
error: serviceError,
@@ -96,7 +97,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
logger.warn(`Failed to fetch service description for ${serviceUrl}`, {
error: String(serviceError),
})
logEvent('signin:hostingProviderFailedResolution', {})
ax.metric('signin:hostingProviderFailedResolution', {})
} else {
setError('')
}
@@ -104,19 +105,19 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
const onPressForgotPassword = () => {
gotoForm(Forms.ForgotPassword)
logEvent('signin:forgotPasswordPressed', {})
ax.metric('signin:forgotPasswordPressed', {})
}
const handlePressBack = () => {
onPressBack()
setScreenTransitionDirection('Backward')
logEvent('signin:backPressed', {
ax.metric('signin:backPressed', {
failedAttemptsCount: failedAttemptCountRef.current,
})
}
const onAttemptSuccess = () => {
logEvent('signin:success', {
ax.metric('signin:success', {
isUsingCustomProvider: serviceUrl !== DEFAULT_SERVICE,
timeTakenSeconds: Math.round((Date.now() - startTimeRef.current) / 1000),
failedAttemptsCount: failedAttemptCountRef.current,
+1 -1
View File
@@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react'
import {useFocusEffect, useIsFocused} from '@react-navigation/native'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {useAppState} from '#/lib/hooks/useAppState'
import {useAppState} from '#/lib/appState'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {type MessagesTabNavigatorParams} from '#/lib/routes/types'
+1 -1
View File
@@ -12,7 +12,7 @@ import {
type UseInfiniteQueryResult,
} from '@tanstack/react-query'
import {useAppState} from '#/lib/hooks/useAppState'
import {useAppState} from '#/lib/appState'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {
type CommonNavigatorParams,
@@ -13,7 +13,6 @@ import {useQueryClient} from '@tanstack/react-query'
import {GestureActionView} from '#/lib/custom-animations/GestureActionView'
import {useHaptics} from '#/lib/haptics'
import {decrementBadgeCount} from '#/lib/notifications/notifications'
import {logEvent} from '#/lib/statsig/statsig'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {
postUriToRelativePath,
@@ -45,6 +44,7 @@ import {createPortalGroup} from '#/components/Portal'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env'
import type * as bsky from '#/types/bsky'
@@ -96,6 +96,7 @@ function ChatListItemReady({
showMenu?: boolean
children?: React.ReactNode
}) {
const ax = useAnalytics()
const t = useTheme()
const {_} = useLingui()
const {currentAccount} = useSession()
@@ -290,10 +291,10 @@ function ChatListItemReady({
menuControl.open()
return false
} else {
logEvent('chat:open', {logContext: 'ChatsList'})
ax.metric('chat:open', {logContext: 'ChatsList'})
}
},
[isDeletedAccount, menuControl, queryClient, profile, convo],
[ax, isDeletedAccount, menuControl, queryClient, profile, convo],
)
const onLongPress = useCallback(() => {
@@ -3,7 +3,6 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {urls} from '#/lib/constants'
import {logger} from '#/logger'
import {
usePreferencesQuery,
type UsePreferencesQueryResponse,
@@ -17,9 +16,11 @@ import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheck} from '#/components/i
import * as Layout from '#/components/Layout'
import {InlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {useAnalytics} from '#/analytics'
export function Screen() {
const {_} = useLingui()
const ax = useAnalytics()
const gutters = useGutters(['base'])
const {data: preferences} = usePreferencesQuery()
@@ -51,13 +52,9 @@ export function Screen() {
}),
)}
onPress={() => {
logger.metric(
'verification:learn-more',
{
location: 'verificationSettings',
},
{statsig: true},
)
ax.metric('verification:learn-more', {
location: 'verificationSettings',
})
}}>
Learn more here.
</InlineLinkText>
@@ -2,10 +2,11 @@ import {useCallback, useState} from 'react'
import {LayoutAnimationConfig} from 'react-native-reanimated'
import {SafeAreaView} from 'react-native-safe-area-context'
import {logger} from '#/logger'
import {useCallOnce} from '#/lib/once'
import {FindContactsFlow} from '#/components/contacts/FindContactsFlow'
import {type Action, type State} from '#/components/contacts/state'
import {ScreenTransition} from '#/components/ScreenTransition'
import {useAnalytics} from '#/analytics'
import {useOnboardingInternalState} from '../state'
export function StepFindContacts({
@@ -16,6 +17,11 @@ export function StepFindContacts({
flowDispatch: React.ActionDispatch<[Action]>
}) {
const {dispatch} = useOnboardingInternalState()
const ax = useAnalytics()
useCallOnce(() => {
ax.metric('onboarding:contacts:begin', {})
})()
const [transitionDirection, setTransitionDirection] = useState<
'Forward' | 'Backward'
@@ -24,10 +30,10 @@ export function StepFindContacts({
const isFinalStep = flowState.step === '4: view matches'
const onSkip = useCallback(() => {
if (!isFinalStep) {
logger.metric('onboarding:contacts:skipPressed', {})
ax.metric('onboarding:contacts:skipPressed', {})
}
dispatch({type: 'next'})
}, [dispatch, isFinalStep])
}, [dispatch, isFinalStep, ax])
const canGoBack = flowState.step === '2: verify number'
const onBack = useCallback(() => {
@@ -5,11 +5,13 @@ import {useLingui} from '@lingui/react'
import {useQuery} from '@tanstack/react-query'
import {urls} from '#/lib/constants'
import {useCallOnce} from '#/lib/once'
import {atoms as a} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Button, ButtonText} from '#/components/Button'
import {ContactsHeroImage} from '#/components/contacts/components/HeroImage'
import {InlineLinkText} from '#/components/Link'
import {useAnalytics} from '#/analytics'
import {
OnboardingControls,
OnboardingDescriptionText,
@@ -19,9 +21,14 @@ import {
import {useOnboardingInternalState} from '../state'
export function StepFindContactsIntro() {
const ax = useAnalytics()
const {_} = useLingui()
const {dispatch} = useOnboardingInternalState()
useCallOnce(() => {
ax.metric('onboarding:contacts:presented', {})
})()
const {data: isAvailable, isSuccess} = useQuery({
queryKey: ['contacts-available'],
queryFn: async () => await Contacts.isAvailableAsync(),
+10 -7
View File
@@ -20,7 +20,6 @@ import {
VIDEO_SAVED_FEED,
} from '#/lib/constants'
import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
import {getAllListMembers} from '#/state/queries/list-members'
@@ -46,12 +45,14 @@ import {atoms as a, useBreakpoints} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow'
import {Loader} from '#/components/Loader'
import {useAnalytics} from '#/analytics'
import {IS_WEB} from '#/env'
import * as bsky from '#/types/bsky'
import {ValuePropositionPager} from './ValuePropositionPager'
export function StepFinished() {
const {state, dispatch} = useOnboardingInternalState()
const ax = useAnalytics()
const onboardDispatch = useOnboardingDispatch()
const [saving, setSaving] = useState(false)
const queryClient = useQueryClient()
@@ -165,7 +166,7 @@ export function StepFinished() {
return next
})
logEvent('onboarding:finished:avatarResult', {
ax.metric('onboarding:finished:avatarResult', {
avatarResult: profileStepResults.isCreatedAvatar
? 'created'
: profileStepResults.image
@@ -200,7 +201,7 @@ export function StepFinished() {
startProgressGuide('follow-10')
dispatch({type: 'finish'})
onboardDispatch({type: 'finish'})
logEvent('onboarding:finished:nextPressed', {
ax.metric('onboarding:finished:nextPressed', {
usedStarterPack: Boolean(starterPack),
starterPackName:
starterPack &&
@@ -216,13 +217,14 @@ export function StepFinished() {
feedsPinned: starterPack?.feeds?.length ?? 0,
})
if (starterPack && listItems?.length) {
logEvent('starterPack:followAll', {
ax.metric('starterPack:followAll', {
logContext: 'Onboarding',
starterPack: starterPack.uri,
count: listItems?.length,
})
}
}, [
ax,
queryClient,
agent,
dispatch,
@@ -255,6 +257,7 @@ function ValueProposition({
}) {
const [subStep, setSubStep] = useState<0 | 1 | 2>(0)
const {_} = useLingui()
const ax = useAnalytics()
const {gtMobile} = useBreakpoints()
const onPress = () => {
@@ -262,10 +265,10 @@ function ValueProposition({
finishOnboarding() // has its own metrics
} else if (subStep === 1) {
setSubStep(2)
logger.metric('onboarding:valueProp:stepTwo:nextPressed', {})
ax.metric('onboarding:valueProp:stepTwo:nextPressed', {})
} else if (subStep === 0) {
setSubStep(1)
logger.metric('onboarding:valueProp:stepOne:nextPressed', {})
ax.metric('onboarding:valueProp:stepOne:nextPressed', {})
}
}
@@ -280,7 +283,7 @@ function ValueProposition({
size="small"
label={_(msg`Skip introduction and start using your account`)}
onPress={() => {
logger.metric('onboarding:valueProp:skipPressed', {})
ax.metric('onboarding:valueProp:skipPressed', {})
finishOnboarding()
}}
style={[a.bg_transparent]}>
@@ -4,7 +4,6 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {interests, useInterestsDisplayNames} from '#/lib/interests'
import {logEvent} from '#/lib/statsig/statsig'
import {capitalize} from '#/lib/strings/capitalize'
import {logger} from '#/logger'
import {
@@ -19,9 +18,11 @@ import {atoms as a} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Toggle from '#/components/forms/Toggle'
import {Loader} from '#/components/Loader'
import {useAnalytics} from '#/analytics'
export function StepInterests() {
const {_} = useLingui()
const ax = useAnalytics()
const interestsDisplayNames = useInterestsDisplayNames()
const {state, dispatch} = useOnboardingInternalState()
@@ -40,7 +41,7 @@ export function StepInterests() {
selectedInterests,
})
dispatch({type: 'next'})
logEvent('onboarding:interests:nextPressed', {
ax.metric('onboarding:interests:nextPressed', {
selectedInterests,
selectedInterestsLength: selectedInterests.length,
})
@@ -48,7 +49,7 @@ export function StepInterests() {
logger.info(`onboading: error saving interests`)
logger.error(e)
}
}, [selectedInterests, setSaving, dispatch])
}, [ax, selectedInterests, setSaving, dispatch])
return (
<View style={[a.align_start, a.gap_sm]} testID="onboardingInterests">
+5 -5
View File
@@ -14,7 +14,6 @@ import {compressIfNeeded} from '#/lib/media/manip'
import {openCropper} from '#/lib/media/picker'
import {getDataUriSize} from '#/lib/media/util'
import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
import {logEvent, useGate} from '#/lib/statsig/statsig'
import {isCancelledError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {
@@ -37,6 +36,7 @@ import * as Dialog from '#/components/Dialog'
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE, IS_WEB} from '#/env'
import {type AvatarColor, avatarColors, type Emoji, emojiItems} from './types'
@@ -66,11 +66,11 @@ const randomColor =
avatarColors[Math.floor(Math.random() * avatarColors.length)]
export function StepProfile() {
const ax = useAnalytics()
const {_} = useLingui()
const t = useTheme()
const {gtMobile} = useBreakpoints()
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
const gate = useGate()
const requestNotificationsPermission = useRequestNotificationsPermission()
const creatorControl = Dialog.useDialogControl()
@@ -89,7 +89,7 @@ export function StepProfile() {
React.useEffect(() => {
requestNotificationsPermission('StartOnboarding')
}, [gate, requestNotificationsPermission])
}, [requestNotificationsPermission])
const sheetWrapper = useSheetWrapper()
const openPicker = React.useCallback(
@@ -156,8 +156,8 @@ export function StepProfile() {
}
dispatch({type: 'next'})
logEvent('onboarding:profile:nextPressed', {})
}, [avatar, dispatch])
ax.metric('onboarding:profile:nextPressed', {})
}, [ax, avatar, dispatch])
const onDoneCreating = React.useCallback(() => {
setAvatar(prev => ({
@@ -9,7 +9,6 @@ import * as bcp47Match from 'bcp-47-match'
import {wait} from '#/lib/async/wait'
import {popularInterests, useInterestsDisplayNames} from '#/lib/interests'
import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
import {logger} from '#/logger'
import {updateProfileShadow} from '#/state/cache/profile-shadow'
import {useLanguagePrefs} from '#/state/preferences'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
@@ -30,12 +29,14 @@ import {boostInterests, InterestTabs} from '#/components/InterestTabs'
import {Loader} from '#/components/Loader'
import * as ProfileCard from '#/components/ProfileCard'
import * as toast from '#/components/Toast'
import {useAnalytics} from '#/analytics'
import {IS_WEB} from '#/env'
import type * as bsky from '#/types/bsky'
import {bulkWriteFollows} from '../util'
export function StepSuggestedAccounts() {
const {_} = useLingui()
const ax = useAnalytics()
const t = useTheme()
const {gtMobile} = useBreakpoints()
const moderationOpts = useModerationOpts()
@@ -92,7 +93,7 @@ export function StepSuggestedAccounts() {
const {mutate: followAll, isPending: isFollowingAll} = useMutation({
onMutate: () => {
logger.metric('onboarding:suggestedAccounts:followAllPressed', {
ax.metric('onboarding:suggestedAccounts:followAllPressed', {
tab: selectedInterest ?? 'all',
numAccounts: followableDids.length,
})
@@ -132,20 +133,16 @@ export function StepSuggestedAccounts() {
(did: string, position: number) => {
if (!seenProfilesRef.current.has(did)) {
seenProfilesRef.current.add(did)
logger.metric(
'suggestedUser:seen',
{
logContext: 'Onboarding',
recId: undefined,
position,
suggestedDid: did,
category: selectedInterest,
},
{statsig: true},
)
ax.metric('suggestedUser:seen', {
logContext: 'Onboarding',
recId: undefined,
position,
suggestedDid: did,
category: selectedInterest,
})
}
},
[selectedInterest],
[ax, selectedInterest],
)
return (
@@ -297,6 +294,7 @@ function TabBar({
defaultTabLabel?: string
}) {
const {_} = useLingui()
const ax = useAnalytics()
const interestsDisplayNames = useInterestsDisplayNames()
const interests = Object.keys(interestsDisplayNames)
.sort(boostInterests(popularInterests))
@@ -309,11 +307,7 @@ function TabBar({
selectedInterest || (hideDefaultTab ? interests[0] : 'all')
}
onSelectTab={tab => {
logger.metric(
'onboarding:suggestedAccounts:tabPressed',
{tab: tab},
{statsig: true},
)
ax.metric('onboarding:suggestedAccounts:tabPressed', {tab: tab})
onSelectInterest(tab === 'all' ? null : tab)
}}
interestsDisplayNames={
@@ -343,6 +337,7 @@ function SuggestedProfileCard({
onSeen: (did: string, position: number) => void
}) {
const t = useTheme()
const ax = useAnalytics()
const cardRef = useRef<View>(null)
const hasTrackedRef = useRef(false)
@@ -403,18 +398,14 @@ function SuggestedProfileCard({
withIcon={false}
logContext="OnboardingSuggestedAccounts"
onFollow={() => {
logger.metric(
'suggestedUser:follow',
{
logContext: 'Onboarding',
location: 'Card',
recId: undefined,
position,
suggestedDid: profile.did,
category,
},
{statsig: true},
)
ax.metric('suggestedUser:follow', {
logContext: 'Onboarding',
location: 'Card',
recId: undefined,
position,
suggestedDid: profile.did,
category,
})
}}
/>
</ProfileCard.Header>
@@ -19,6 +19,7 @@ import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Che
import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import * as bsky from '#/types/bsky'
const IGNORED_ACCOUNT = 'did:plc:pifkcjimdcfwaxkanzhwxufp'
@@ -30,6 +31,7 @@ export function StarterPackCard({
}) {
const t = useTheme()
const {_} = useLingui()
const ax = useAnalytics()
const {currentAccount} = useSession()
const {gtPhone} = useBreakpoints()
const agent = useAgent()
@@ -89,7 +91,7 @@ export function StarterPackCard({
}
})
Toast.show(_(msg`All accounts have been followed!`), {type: 'success'})
logger.metric('starterPack:followAll', {
ax.metric('starterPack:followAll', {
logContext: 'Onboarding',
starterPack: view.uri,
count: dids.length,
+3 -3
View File
@@ -3,7 +3,6 @@ import {View} from 'react-native'
import * as bcp47Match from 'bcp-47-match'
import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController'
import {useGate} from '#/lib/statsig/statsig'
import {useLanguagePrefs} from '#/state/preferences'
import {
Layout,
@@ -23,6 +22,7 @@ import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from '#/components/co
import {useFindContactsFlowState} from '#/components/contacts/state'
import {Portal} from '#/components/Portal'
import {ScreenTransition} from '#/components/ScreenTransition'
import {useAnalytics} from '#/analytics'
import {ENV, IS_NATIVE} from '#/env'
import {StepFindContacts} from './StepFindContacts'
import {StepFindContactsIntro} from './StepFindContactsIntro'
@@ -31,7 +31,7 @@ import {StepSuggestedStarterpacks} from './StepSuggestedStarterpacks'
export function Onboarding() {
const t = useTheme()
const gate = useGate()
const ax = useAnalytics()
const {contentLanguages} = useLanguagePrefs()
const probablySpeaksEnglish = useMemo(() => {
@@ -48,7 +48,7 @@ export function Onboarding() {
ENV !== 'e2e' &&
IS_NATIVE &&
findContactsEnabled &&
!gate('disable_onboarding_find_contacts')
!ax.features.enabled(ax.features.DisableOnboardingFindContacts)
const [state, dispatch] = useReducer(
reducer,
-9
View File
@@ -172,15 +172,6 @@ export function reducer(
}
}
if (a.type === 'next') {
if (next.activeStep === 'find-contacts-intro') {
logger.metric('onboarding:contacts:presented', {})
}
if (next.activeStep === 'find-contacts') {
logger.metric('onboarding:contacts:begin', {})
}
}
const state = {
...next,
hasPrev: next.activeStep !== 'profile',
@@ -2,7 +2,7 @@ import {useState} from 'react'
import {View} from 'react-native'
import {PrivacySensitive} from 'expo-privacy-sensitive'
import {useAppState} from '#/lib/hooks/useAppState'
import {useAppState} from '#/lib/appState'
import {atoms as a, useTheme} from '#/alf'
import {sizes as iconSizes} from '#/components/icons/common'
import {Mark as Logo} from '#/components/icons/Logo'
@@ -2,11 +2,11 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {HITSLOP_10} from '#/lib/constants'
import {logger} from '#/logger'
import {type ThreadPreferences} from '#/state/queries/preferences/useThreadPreferences'
import {Button, ButtonIcon} from '#/components/Button'
import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider'
import * as Menu from '#/components/Menu'
import {useAnalytics} from '#/analytics'
export function HeaderDropdown({
sort,
@@ -17,6 +17,7 @@ export function HeaderDropdown({
ThreadPreferences,
'sort' | 'setSort' | 'view' | 'setView'
>): React.ReactNode {
const ax = useAnalytics()
const {_} = useLingui()
return (
<Menu.Root>
@@ -30,7 +31,7 @@ export function HeaderDropdown({
shape="round"
hitSlop={HITSLOP_10}
onPress={() => {
logger.metric('thread:click:headerMenuOpen', {})
ax.metric('thread:click:headerMenuOpen', {})
onPress()
}}
{...props}>
@@ -18,7 +18,6 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {niceDate} from '#/lib/strings/time'
import {getTranslatorLink, isPostInLanguage} from '#/locale/helpers'
import {logger} from '#/logger'
import {
POST_TOMBSTONE,
type Shadow,
@@ -60,6 +59,7 @@ import * as Skele from '#/components/Skeleton'
import {Text} from '#/components/Typography'
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
import {WhoCanReply} from '#/components/WhoCanReply'
import {useAnalytics} from '#/analytics'
import * as bsky from '#/types/bsky'
export function ThreadItemAnchor({
@@ -177,6 +177,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
postSource?: PostSource
}) {
const t = useTheme()
const ax = useAnalytics()
const {_} = useLingui()
const {openComposer} = useOpenComposer()
const {currentAccount, hasSession} = useSession()
@@ -281,7 +282,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
])
const onOpenAuthor = () => {
logger.metric('post:clickthroughAuthor', {
ax.metric('post:clickthroughAuthor', {
uri: post.uri,
authorDid: post.author.did,
logContext: 'PostThreadItem',
@@ -298,7 +299,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
}
const onOpenEmbed = () => {
logger.metric('post:clickthroughEmbed', {
ax.metric('post:clickthroughEmbed', {
uri: post.uri,
authorDid: post.author.did,
logContext: 'PostThreadItem',
@@ -540,6 +541,7 @@ function ExpandedPostDetails({
isThreadAuthor: boolean
}) {
const t = useTheme()
const ax = useAnalytics()
const {_, i18n} = useLingui()
const translate = useTranslate()
const isRootPost = !('reply' in post.record)
@@ -565,7 +567,7 @@ function ExpandedPostDetails({
AppBskyFeedPost.isRecord,
)
) {
logger.metric('translate', {
ax.metric('translate', {
sourceLanguages: post.record.langs ?? [],
targetLanguage: langPrefs.primaryLanguage,
textLength: post.record.text.length,
@@ -574,7 +576,7 @@ function ExpandedPostDetails({
return false
},
[translate, langPrefs, post],
[ax, translate, langPrefs, post],
)
return (
@@ -2,22 +2,23 @@ import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
export function ThreadItemShowOtherReplies({onPress}: {onPress: () => void}) {
const {_} = useLingui()
const t = useTheme()
const ax = useAnalytics()
const {_} = useLingui()
const label = _(msg`Show more replies`)
return (
<Button
onPress={() => {
onPress()
logger.metric('thread:click:showOtherReplies', {})
ax.metric('thread:click:showOtherReplies', {})
}}
label={label}>
{({hovered, pressed}) => (
+9 -12
View File
@@ -6,7 +6,6 @@ import {Trans} from '@lingui/macro'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
import {logger} from '#/logger'
import {useFeedFeedback} from '#/state/feed-feedback'
import {type ThreadViewOption} from '#/state/queries/preferences/useThreadPreferences'
import {
@@ -44,11 +43,13 @@ import {
import {atoms as a, native, platform, useBreakpoints, web} from '#/alf'
import * as Layout from '#/components/Layout'
import {ListFooter} from '#/components/Lists'
import {useAnalytics} from '#/analytics'
const PARENT_CHUNK_SIZE = 5
const CHILDREN_CHUNK_SIZE = 50
export function PostThread({uri}: {uri: string}) {
const ax = useAnalytics()
const {gtMobile} = useBreakpoints()
const {hasSession} = useSession()
const initialNumToRender = useInitialNumToRender()
@@ -84,18 +85,14 @@ export function PostThread({uri}: {uri: string}) {
const post = anchor.value.post
seenPostUriRef.current = post.uri
logger.metric(
'post:view',
{
uri: post.uri,
authorDid: post.author.did,
logContext: 'Post',
feedDescriptor: feedFeedback.feedDescriptor,
},
{statsig: false},
)
ax.metric('post:view', {
uri: post.uri,
authorDid: post.author.did,
logContext: 'Post',
feedDescriptor: feedFeedback.feedDescriptor,
})
}
}, [anchor, feedFeedback.feedDescriptor])
}, [ax, anchor, feedFeedback.feedDescriptor])
// Track post:view events for parent posts and replies (non-anchor posts)
const trackThreadItemView = usePostViewTracking('PostThreadItem')
@@ -14,7 +14,6 @@ import {useLingui} from '@lingui/react'
import {MAX_LABELERS} from '#/lib/constants'
import {useHaptics} from '#/lib/haptics'
import {isAppLabeler} from '#/lib/moderation'
import {logger} from '#/logger'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {type Shadow} from '#/state/cache/types'
import {useLabelerSubscriptionMutation} from '#/state/queries/labeler'
@@ -34,6 +33,7 @@ import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_IOS} from '#/env'
import {ProfileHeaderDisplayName} from './DisplayName'
import {EditProfileDialog} from './EditProfileDialog'
@@ -61,6 +61,7 @@ let ProfileHeaderLabeler = ({
const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> =
useProfileShadow(profileUnshadowed)
const t = useTheme()
const ax = useAnalytics()
const {_} = useLingui()
const {currentAccount, hasSession} = useSession()
const playHaptic = useHaptics()
@@ -99,9 +100,9 @@ let ProfileHeaderLabeler = ({
),
{type: 'error'},
)
logger.error(`Failed to toggle labeler like`, {message: e.message})
ax.logger.error(`Failed to toggle labeler like`, {message: e.message})
}
}, [labeler, playHaptic, likeUri, unlikeMod, likeMod, _])
}, [ax, labeler, playHaptic, likeUri, unlikeMod, likeMod, _])
return (
<ProfileHeaderShell
@@ -233,8 +234,9 @@ export function HeaderLabelerButtons({
/** disable the subscribe button */
minimal?: boolean
}) {
const {_} = useLingui()
const t = useTheme()
const ax = useAnalytics()
const {_} = useLingui()
const {currentAccount} = useSession()
const requireAuth = useRequireAuth()
const playHaptic = useHaptics()
@@ -264,12 +266,11 @@ export function HeaderLabelerButtons({
subscribe,
})
logger.metric(
ax.metric(
subscribe
? 'moderation:subscribedToLabeler'
: 'moderation:unsubscribedFromLabeler',
{},
{statsig: true},
)
} catch (e: any) {
reset()
@@ -277,7 +278,7 @@ export function HeaderLabelerButtons({
cantSubscribePrompt.open()
return
}
logger.error(`Failed to subscribe to labeler`, {message: e.message})
ax.logger.error(`Failed to subscribe to labeler`, {message: e.message})
}
})
return (
+6 -12
View File
@@ -18,7 +18,6 @@ import {useActorStatus} from '#/lib/actor-status'
import {BACK_HITSLOP} from '#/lib/constants'
import {useHaptics} from '#/lib/haptics'
import {type NavigationProp} from '#/lib/routes/types'
import {logger} from '#/logger'
import {type Shadow} from '#/state/cache/types'
import {useLightboxControls} from '#/state/lightbox'
import {useSession} from '#/state/session'
@@ -34,6 +33,7 @@ import {LiveIndicator} from '#/components/live/LiveIndicator'
import {LiveStatusDialog} from '#/components/live/LiveStatusDialog'
import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'
import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'
import {useAnalytics} from '#/analytics'
import {IS_IOS} from '#/env'
import {GrowableAvatar} from './GrowableAvatar'
import {GrowableBanner} from './GrowableBanner'
@@ -54,6 +54,7 @@ let ProfileHeaderShell = ({
isPlaceholderProfile,
}: React.PropsWithChildren<Props>): React.ReactNode => {
const t = useTheme()
const ax = useAnalytics()
const {currentAccount} = useSession()
const {_} = useLingui()
const {openLightbox} = useLightboxControls()
@@ -116,22 +117,14 @@ let ProfileHeaderShell = ({
useEffect(() => {
if (live.isActive) {
logger.metric(
'live:view:profile',
{subject: profile.did},
{statsig: true},
)
ax.metric('live:view:profile', {subject: profile.did})
}
}, [live.isActive, profile.did])
}, [ax, live.isActive, profile.did])
const onPressAvi = useCallback(() => {
if (live.isActive) {
playHaptic('Light')
logger.metric(
'live:card:open',
{subject: profile.did, from: 'profile'},
{statsig: true},
)
ax.metric('live:card:open', {subject: profile.did, from: 'profile'})
liveStatusControl.open()
} else {
const modui = moderation.ui('avatar')
@@ -145,6 +138,7 @@ let ProfileHeaderShell = ({
}
}
}, [
ax,
profile,
moderation,
_openLightbox,
@@ -5,8 +5,7 @@ import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useHaptics} from '#/lib/haptics'
import {makeProfileLink} from '#/lib/routes/links'
import {makeCustomFeedLink} from '#/lib/routes/links'
import {makeCustomFeedLink, makeProfileLink} from '#/lib/routes/links'
import {shareUrl} from '#/lib/sharing'
import {sanitizeHandle} from '#/lib/strings/handles'
import {toShareUrl} from '#/lib/strings/url-helpers'
@@ -51,6 +50,7 @@ import {
} from '#/components/moderation/ReportDialog'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_WEB} from '#/env'
export function ProfileFeedHeaderSkeleton() {
@@ -86,6 +86,7 @@ export function ProfileFeedHeaderSkeleton() {
export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
const t = useTheme()
const {_, i18n} = useLingui()
const ax = useAnalytics()
const {hasSession} = useSession()
const {gtMobile} = useBreakpoints()
const infoControl = Dialog.useDialogControl()
@@ -120,7 +121,7 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
if (savedFeedConfig) {
await removeFeed(savedFeedConfig)
Toast.show(_(msg`Removed from your feeds`))
logger.metric('feed:unsave', {feedUrl: info.uri})
ax.metric('feed:unsave', {feedUrl: info.uri})
} else {
await addSavedFeeds([
{
@@ -130,7 +131,7 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
},
])
Toast.show(_(msg`Saved to your feeds`))
logger.metric('feed:save', {feedUrl: info.uri})
ax.metric('feed:save', {feedUrl: info.uri})
}
} catch (err) {
Toast.show(
@@ -158,10 +159,10 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
if (pinned) {
Toast.show(_(msg`Pinned ${info.displayName} to Home`))
logger.metric('feed:pin', {feedUrl: info.uri})
ax.metric('feed:pin', {feedUrl: info.uri})
} else {
Toast.show(_(msg`Unpinned ${info.displayName} from Home`))
logger.metric('feed:unpin', {feedUrl: info.uri})
ax.metric('feed:unpin', {feedUrl: info.uri})
}
} else {
await addSavedFeeds([
@@ -172,7 +173,7 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
},
])
Toast.show(_(msg`Pinned ${info.displayName} to Home`))
logger.metric('feed:pin', {feedUrl: info.uri})
ax.metric('feed:pin', {feedUrl: info.uri})
}
} catch (e) {
Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
@@ -388,6 +389,7 @@ function DialogInner({
}) {
const t = useTheme()
const {_} = useLingui()
const ax = useAnalytics()
const {hasSession} = useSession()
const playHaptic = useHaptics()
const control = Dialog.useDialogContext()
@@ -407,11 +409,11 @@ function DialogInner({
if (isLiked && likeUri) {
await unlikeFeed({uri: likeUri})
setLikeUri('')
logger.metric('feed:unlike', {feedUrl: info.uri})
ax.metric('feed:unlike', {feedUrl: info.uri})
} else {
const res = await likeFeed({uri: info.uri, cid: info.cid})
setLikeUri(res.uri)
logger.metric('feed:like', {feedUrl: info.uri})
ax.metric('feed:like', {feedUrl: info.uri})
}
} catch (err) {
Toast.show(
@@ -428,7 +430,7 @@ function DialogInner({
playHaptic()
const url = toShareUrl(info.route.href)
shareUrl(url)
logger.metric('feed:share', {feedUrl: info.uri})
ax.metric('feed:share', {feedUrl: info.uri})
}, [info, playHaptic])
const onPressReport = React.useCallback(() => {
+4 -10
View File
@@ -21,6 +21,7 @@ import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
import {Loader} from '#/components/Loader'
import {RichText} from '#/components/RichText'
import * as Toast from '#/components/Toast'
import {useAnalytics} from '#/analytics'
import {MoreOptionsMenu} from './MoreOptionsMenu'
import {SubscribeMenu} from './SubscribeMenu'
@@ -34,6 +35,7 @@ export function Header({
preferences: UsePreferencesQueryResponse
}) {
const {_} = useLingui()
const ax = useAnalytics()
const {currentAccount} = useSession()
const isCurateList = list.purpose === AppBskyGraphDefs.CURATELIST
const isModList = list.purpose === AppBskyGraphDefs.MODLIST
@@ -96,11 +98,7 @@ export function Header({
try {
await muteList({uri: list.uri, mute: false})
Toast.show(_(msg({message: 'List unmuted', context: 'toast'})))
logger.metric(
'moderation:unsubscribedFromList',
{listType: 'mute'},
{statsig: true},
)
ax.metric('moderation:unsubscribedFromList', {listType: 'mute'})
} catch {
Toast.show(
_(
@@ -114,11 +112,7 @@ export function Header({
try {
await blockList({uri: list.uri, block: false})
Toast.show(_(msg({message: 'List unblocked', context: 'toast'})))
logger.metric(
'moderation:unsubscribedFromList',
{listType: 'block'},
{statsig: true},
)
ax.metric('moderation:unsubscribedFromList', {listType: 'block'})
} catch {
Toast.show(
_(
@@ -33,6 +33,7 @@ import {
} from '#/components/moderation/ReportDialog'
import * as Prompt from '#/components/Prompt'
import * as Toast from '#/components/Toast'
import {useAnalytics} from '#/analytics'
import {IS_WEB} from '#/env'
export function MoreOptionsMenu({
@@ -43,6 +44,7 @@ export function MoreOptionsMenu({
savedFeedConfig?: AppBskyActorDefs.SavedFeed
}) {
const {_} = useLingui()
const ax = useAnalytics()
const {currentAccount} = useSession()
const editListDialogControl = useDialogControl()
const deleteListPromptControl = useDialogControl()
@@ -111,11 +113,7 @@ export function MoreOptionsMenu({
try {
await muteList({uri: list.uri, mute: false})
Toast.show(_(msg({message: 'List unmuted', context: 'toast'})))
logger.metric(
'moderation:unsubscribedFromList',
{listType: 'mute'},
{statsig: true},
)
ax.metric('moderation:unsubscribedFromList', {listType: 'mute'})
} catch {
Toast.show(
_(
@@ -129,11 +127,7 @@ export function MoreOptionsMenu({
try {
await blockList({uri: list.uri, block: false})
Toast.show(_(msg({message: 'List unblocked', context: 'toast'})))
logger.metric(
'moderation:unsubscribedFromList',
{listType: 'block'},
{statsig: true},
)
ax.metric('moderation:unsubscribedFromList', {listType: 'block'})
} catch {
Toast.show(
_(
@@ -2,7 +2,6 @@ import {type AppBskyGraphDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {useListBlockMutation, useListMuteMutation} from '#/state/queries/list'
import {atoms as a} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -12,9 +11,11 @@ import {Loader} from '#/components/Loader'
import * as Menu from '#/components/Menu'
import * as Prompt from '#/components/Prompt'
import * as Toast from '#/components/Toast'
import {useAnalytics} from '#/analytics'
export function SubscribeMenu({list}: {list: AppBskyGraphDefs.ListView}) {
const {_} = useLingui()
const ax = useAnalytics()
const subscribeMutePromptControl = Prompt.usePromptControl()
const subscribeBlockPromptControl = Prompt.usePromptControl()
@@ -29,11 +30,7 @@ export function SubscribeMenu({list}: {list: AppBskyGraphDefs.ListView}) {
try {
await muteList({uri: list.uri, mute: true})
Toast.show(_(msg({message: 'List muted', context: 'toast'})))
logger.metric(
'moderation:subscribedToList',
{listType: 'mute'},
{statsig: true},
)
ax.metric('moderation:subscribedToList', {listType: 'mute'})
} catch {
Toast.show(
_(
@@ -48,11 +45,7 @@ export function SubscribeMenu({list}: {list: AppBskyGraphDefs.ListView}) {
try {
await blockList({uri: list.uri, block: true})
Toast.show(_(msg({message: 'List blocked', context: 'toast'})))
logger.metric(
'moderation:subscribedToList',
{listType: 'block'},
{statsig: true},
)
ax.metric('moderation:subscribedToList', {listType: 'block'})
} catch {
Toast.show(
_(
+24 -38
View File
@@ -13,8 +13,6 @@ import * as bcp47Match from 'bcp-47-match'
import {popularInterests, useInterestsDisplayNames} from '#/lib/interests'
import {cleanError} from '#/lib/strings/errors'
import {sanitizeHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
import {type MetricEvents} from '#/logger/metrics'
import {useLanguagePrefs} from '#/state/preferences/languages'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {RQKEY_ROOT as useActorSearchQueryKeyRoot} from '#/state/queries/actor-search'
@@ -68,6 +66,7 @@ import {Loader} from '#/components/Loader'
import * as ProfileCard from '#/components/ProfileCard'
import {SubtleHover} from '#/components/SubtleHover'
import {Text} from '#/components/Typography'
import {type Metrics, useAnalytics} from '#/analytics'
import {ExploreScreenLiveEventFeedsBanner} from '#/features/liveEvents/components/ExploreScreenLiveEventFeedsBanner'
import * as ModuleHeader from './components/ModuleHeader'
import {
@@ -124,7 +123,7 @@ type ExploreScreenItems =
bottomBorder?: boolean
searchButton?: {
label: string
metricsTag: MetricEvents['explore:module:searchButtonPress']['module']
metricsTag: Metrics['explore:module:searchButtonPress']['module']
tab: 'user' | 'profile' | 'feed'
}
}
@@ -135,7 +134,7 @@ type ExploreScreenItems =
icon: React.ComponentType<SVGIconProps>
searchButton?: {
label: string
metricsTag: MetricEvents['explore:module:searchButtonPress']['module']
metricsTag: Metrics['explore:module:searchButtonPress']['module']
tab: 'user' | 'profile' | 'feed'
}
hideDefaultTab?: boolean
@@ -213,6 +212,7 @@ export function Explore({
focusSearchInput: (tab: 'user' | 'profile' | 'feed') => void
headerHeight: number
}) {
const ax = useAnalytics()
const {_} = useLingui()
const t = useTheme()
const {data: preferences, error: preferencesError} = usePreferencesQuery()
@@ -273,9 +273,10 @@ export function Explore({
try {
await fetchNextFeedsPage()
} catch (err) {
logger.error('Failed to load more suggested follows', {message: err})
ax.logger.error('Failed to load more suggested follows', {message: err})
}
}, [
ax,
isFetchingNextFeedsPage,
hasNextFeedsPage,
feedsError,
@@ -333,9 +334,10 @@ export function Explore({
try {
await fetchNextPageFeedPreviews()
} catch (err) {
logger.error('Failed to load more feed previews', {message: err})
ax.logger.error('Failed to load more feed previews', {message: err})
}
}, [
ax,
isPendingFeedPreviews,
isFetchingNextPageFeedPreviews,
hasNextPageFeedPreviews,
@@ -492,11 +494,7 @@ export function Explore({
if (hasPressedLoadMoreFeeds && index < 6) {
continue
}
logger.metric(
'feed:suggestion:seen',
{feedUrl: item.feed.uri},
{statsig: false},
)
ax.metric('feed:suggestion:seen', {feedUrl: item.feed.uri})
}
}
if (!hasPressedLoadMoreFeeds) {
@@ -609,6 +607,7 @@ export function Explore({
return i
}, [
_,
ax,
useFullExperience,
suggestedFeeds,
preferences,
@@ -729,12 +728,7 @@ export function Explore({
<ModuleHeader.SearchButton
{...item.searchButton}
onPress={() =>
focusSearchInput(
(item.searchButton?.tab || 'user') as
| 'user'
| 'profile'
| 'feed',
)
focusSearchInput(item.searchButton?.tab || 'user')
}
/>
)}
@@ -751,12 +745,7 @@ export function Explore({
<ModuleHeader.SearchButton
{...item.searchButton}
onPress={() =>
focusSearchInput(
(item.searchButton?.tab || 'user') as
| 'user'
| 'profile'
| 'feed',
)
focusSearchInput(item.searchButton?.tab || 'user')
}
/>
)}
@@ -822,7 +811,7 @@ export function Explore({
if (!useFullExperience) {
return
}
logger.metric('feed:suggestion:press', {
ax.metric('feed:suggestion:press', {
feedUrl: item.feed.uri,
})
}}
@@ -1011,6 +1000,7 @@ export function Explore({
}
},
[
ax,
t.atoms.border_contrast_low,
t.atoms.bg_contrast_25,
t.atoms.text_contrast_medium,
@@ -1043,7 +1033,7 @@ export function Explore({
const seenProfilesRef = useRef<Set<string>>(new Set())
const onItemSeen = useCallback(
(item: ExploreScreenItems) => {
let module: MetricEvents['explore:module:seen']['module']
let module: Metrics['explore:module:seen']['module']
if (item.type === 'trendingTopics' || item.type === 'trendingVideos') {
module = item.type
} else if (item.type === 'profile') {
@@ -1054,17 +1044,13 @@ export function Explore({
const position = suggestedFollowsModule.findIndex(
i => i.type === 'profile' && i.profile.did === item.profile.did,
)
logger.metric(
'suggestedUser:seen',
{
logContext: 'Explore',
recId: item.recId,
position: position !== -1 ? position - 1 : 0, // -1 to account for header
suggestedDid: item.profile.did,
category: null,
},
{statsig: true},
)
ax.metric('suggestedUser:seen', {
logContext: 'Explore',
recId: item.recId,
position: position !== -1 ? position - 1 : 0, // -1 to account for header
suggestedDid: item.profile.did,
category: null,
})
}
} else if (item.type === 'feed') {
module = 'suggestedFeeds'
@@ -1077,10 +1063,10 @@ export function Explore({
}
if (!alreadyReportedRef.current.has(module)) {
alreadyReportedRef.current.set(module, module)
logger.metric('explore:module:seen', {module}, {statsig: false})
ax.metric('explore:module:seen', {module})
}
},
[suggestedFollowsModule],
[ax, suggestedFollowsModule],
)
return (
@@ -4,7 +4,6 @@ import {type AppBskyFeedDefs, AtUri} from '@atproto/api'
import {PressableScale} from '#/lib/custom-animations/PressableScale'
import {makeCustomFeedLink} from '#/lib/routes/links'
import {logger} from '#/logger'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, native, useTheme, type ViewStyleProp} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
@@ -13,6 +12,7 @@ import {sizes as iconSizes} from '#/components/icons/common'
import {MagnifyingGlass_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass'
import {Link} from '#/components/Link'
import {Text, type TextProps} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
export function Container({
style,
@@ -126,6 +126,7 @@ export function SearchButton({
metricsTag: 'suggestedAccounts' | 'suggestedFeeds'
onPress?: () => void
}) {
const ax = useAnalytics()
return (
<Button
label={label}
@@ -135,11 +136,7 @@ export function SearchButton({
shape="round"
PressableComponent={native(PressableScale)}
onPress={() => {
logger.metric(
'explore:module:searchButtonPress',
{module: metricsTag},
{statsig: true},
)
ax.metric('explore:module:searchButtonPress', {module: metricsTag})
onPress?.()
}}
style={[
@@ -2,7 +2,6 @@ import {View} from 'react-native'
import {type AppBskyUnspeccedDefs} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {logger} from '#/logger'
import {
DEFAULT_LIMIT as RECOMMENDATIONS_COUNT,
useTrendingTopics,
@@ -16,6 +15,7 @@ import {
TrendingTopicSkeleton,
} from '#/components/TrendingTopics'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_WEB} from '#/env'
// Note: This module is not currently used and may be removed in the future.
@@ -27,6 +27,7 @@ export function ExploreRecommendations() {
function Inner() {
const t = useTheme()
const ax = useAnalytics()
const gutters = useGutters([0, 'compact'])
const {data: trending, error, isLoading} = useTrendingTopics()
const noRecs = !isLoading && !error && !trending?.suggested?.length
@@ -88,11 +89,7 @@ function Inner() {
key={topic.link}
topic={topic}
onPress={() => {
logger.metric(
'recommendedTopic:click',
{context: 'explore'},
{statsig: true},
)
ax.metric('recommendedTopic:click', {context: 'explore'})
}}>
{({hovered}) => (
<TrendingTopic
@@ -9,11 +9,11 @@ import {popularInterests, useInterestsDisplayNames} from '#/lib/interests'
import {logger} from '#/logger'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
import {useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {atoms as a, useTheme} from '#/alf'
import {boostInterests, InterestTabs} from '#/components/InterestTabs'
import * as ProfileCard from '#/components/ProfileCard'
import {SubtleHover} from '#/components/SubtleHover'
import {useAnalytics} from '#/analytics'
import type * as bsky from '#/types/bsky'
export function useLoadEnoughProfiles({
@@ -62,6 +62,7 @@ export function SuggestedAccountsTabBar({
defaultTabLabel?: string
}) {
const {_} = useLingui()
const ax = useAnalytics()
const interestsDisplayNames = useInterestsDisplayNames()
const {data: preferences} = usePreferencesQuery()
const personalizedInterests = preferences?.interests?.tags
@@ -77,11 +78,7 @@ export function SuggestedAccountsTabBar({
selectedInterest || (hideDefaultTab ? interests[0] : 'all')
}
onSelectTab={tab => {
logger.metric(
'explore:suggestedAccounts:tabPressed',
{tab: tab},
{statsig: true},
)
ax.metric('explore:suggestedAccounts:tabPressed', {tab: tab})
onSelectInterest(tab === 'all' ? null : tab)
}}
interestsDisplayNames={
@@ -112,22 +109,19 @@ let SuggestedProfileCard = ({
position: number
}): React.ReactNode => {
const t = useTheme()
const ax = useAnalytics()
return (
<ProfileCard.Link
profile={profile}
style={[a.flex_1]}
onPress={() => {
logger.metric(
'suggestedUser:press',
{
logContext: 'Explore',
recId,
position,
suggestedDid: profile.did,
category: null,
},
{statsig: true},
)
ax.metric('suggestedUser:press', {
logContext: 'Explore',
recId,
position,
suggestedDid: profile.did,
category: null,
})
}}>
{s => (
<>
@@ -157,18 +151,14 @@ let SuggestedProfileCard = ({
withIcon={false}
logContext="ExploreSuggestedAccounts"
onFollow={() => {
logger.metric(
'suggestedUser:follow',
{
logContext: 'Explore',
location: 'Card',
recId,
position,
suggestedDid: profile.did,
category: null,
},
{statsig: true},
)
ax.metric('suggestedUser:follow', {
logContext: 'Explore',
location: 'Card',
recId,
position,
suggestedDid: profile.did,
category: null,
})
}}
/>
</ProfileCard.Header>
@@ -4,7 +4,6 @@ import {type AppBskyUnspeccedDefs, moderateProfile} from '@atproto/api'
import {msg, plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useTrendingSettings} from '#/state/preferences/trending'
import {useGetTrendsQuery} from '#/state/queries/trending/useGetTrendsQuery'
@@ -19,6 +18,7 @@ import {Trending3_Stroke2_Corner1_Rounded as TrendingIcon} from '#/components/ic
import {Link} from '#/components/Link'
import {SubtleHover} from '#/components/SubtleHover'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
const TOPIC_COUNT = 5
@@ -29,6 +29,7 @@ export function ExploreTrendingTopics() {
}
function Inner() {
const ax = useAnalytics()
const {data: trending, error, isLoading, isRefetching} = useGetTrendsQuery()
const noTopics = !isLoading && !error && !trending?.trends?.length
@@ -44,11 +45,7 @@ function Inner() {
trend={trend}
rank={index + 1}
onPress={() => {
logger.metric(
'trendingTopic:click',
{context: 'explore'},
{statsig: true},
)
ax.metric('trendingTopic:click', {context: 'explore'})
}}
/>
))}
@@ -8,7 +8,6 @@ import {useQueryClient} from '@tanstack/react-query'
import {VIDEO_FEED_URI} from '#/lib/constants'
import {makeCustomFeedLink} from '#/lib/routes/links'
import {logger} from '#/logger'
import {RQKEY, usePostFeedQuery} from '#/state/queries/post-feed'
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
import {atoms as a, tokens, useGutters, useTheme} from '#/alf'
@@ -20,6 +19,7 @@ import {
CompactVideoPostCard,
CompactVideoPostCardPlaceholder,
} from '#/components/VideoPostCard'
import {useAnalytics} from '#/analytics'
const CARD_WIDTH = 100
@@ -151,6 +151,7 @@ function VideoCards({
}) {
const t = useTheme()
const {_} = useLingui()
const ax = useAnalytics()
const items = useMemo(() => {
return data.pages
.flatMap(page => page.slices)
@@ -177,11 +178,7 @@ function VideoCards({
sourceInterstitial: 'explore',
}}
onInteract={() => {
logger.metric(
'videoCard:click',
{context: 'interstitial:explore'},
{statsig: true},
)
ax.metric('videoCard:click', {context: 'interstitial:explore'})
}}
/>
</View>
+2 -4
View File
@@ -1,4 +1,3 @@
import {useMemo} from 'react'
import {Platform} from 'react-native'
import {setStringAsync} from 'expo-clipboard'
import * as FileSystem from 'expo-file-system/legacy'
@@ -7,7 +6,6 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {useMutation} from '@tanstack/react-query'
import {Statsig} from 'statsig-react-native-expo'
import {STATUS_PAGE_URL} from '#/lib/constants'
import {type CommonNavigatorParams} from '#/lib/routes/types'
@@ -21,6 +19,7 @@ import {Newspaper_Stroke2_Corner2_Rounded as NewspaperIcon} from '#/components/i
import {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench'
import * as Layout from '#/components/Layout'
import {Loader} from '#/components/Loader'
import {getDeviceId} from '#/analytics/identifiers'
import {IS_ANDROID, IS_IOS, IS_NATIVE} from '#/env'
import * as env from '#/env'
import {useDemoMode} from '#/storage/hooks/demo-mode'
@@ -32,7 +31,6 @@ export function AboutSettingsScreen({}: Props) {
const {_, i18n} = useLingui()
const [devModeEnabled, setDevModeEnabled] = useDevMode()
const [demoModeEnabled, setDemoModeEnabled] = useDemoMode()
const stableID = useMemo(() => Statsig.getStableID(), [])
const {mutate: onClearImageCache, isPending: isClearingImageCache} =
useMutation({
@@ -146,7 +144,7 @@ export function AboutSettingsScreen({}: Props) {
}}
onPress={() => {
setStringAsync(
`Build version: ${env.APP_VERSION}; Bundle info: ${env.APP_METADATA}; Bundle date: ${env.BUNDLE_DATE}; Platform: ${Platform.OS}; Platform version: ${Platform.Version}; Anonymous ID: ${stableID}`,
`Build version: ${env.APP_VERSION}; Bundle info: ${env.APP_METADATA}; Bundle date: ${env.BUNDLE_DATE}; Platform: ${Platform.OS}; Platform version: ${Platform.Version}; Device ID: ${getDeviceId() ?? 'N/A'}`,
)
Toast.show(_(msg`Copied build version to clipboard`))
}}>
@@ -3,7 +3,6 @@ import {useLingui} from '@lingui/react'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {type CommonNavigatorParams} from '#/lib/routes/types'
import {logEvent} from '#/lib/statsig/statsig'
import {useAutoplayDisabled, useSetAutoplayDisabled} from '#/state/preferences'
import {
useInAppBrowser,
@@ -25,6 +24,7 @@ import {Play_Stroke2_Corner2_Rounded as PlayIcon} from '#/components/icons/Play'
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending'
import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window'
import * as Layout from '#/components/Layout'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env'
import {LiveEventFeedsSettingsToggle} from '#/features/liveEvents/components/LiveEventFeedsSettingsToggle'
@@ -34,6 +34,7 @@ type Props = NativeStackScreenProps<
>
export function ContentAndMediaSettingsScreen({}: Props) {
const {_} = useLingui()
const ax = useAnalytics()
const autoplayDisabledPref = useAutoplayDisabled()
const setAutoplayDisabledPref = useSetAutoplayDisabled()
const inAppBrowserPref = useInAppBrowser()
@@ -135,9 +136,9 @@ export function ContentAndMediaSettingsScreen({}: Props) {
onChange={value => {
const hide = Boolean(!value)
if (hide) {
logEvent('trendingTopics:hide', {context: 'settings'})
ax.metric('trendingTopics:hide', {context: 'settings'})
} else {
logEvent('trendingTopics:show', {context: 'settings'})
ax.metric('trendingTopics:show', {context: 'settings'})
}
setTrendingDisabled(hide)
}}>
@@ -157,9 +158,9 @@ export function ContentAndMediaSettingsScreen({}: Props) {
onChange={value => {
const hide = Boolean(!value)
if (hide) {
logEvent('trendingVideos:hide', {context: 'settings'})
ax.metric('trendingVideos:hide', {context: 'settings'})
} else {
logEvent('trendingVideos:show', {context: 'settings'})
ax.metric('trendingVideos:show', {context: 'settings'})
}
setTrendingVideoDisabled(hide)
}}>
+12 -6
View File
@@ -47,6 +47,7 @@ import {Loader} from '#/components/Loader'
import * as ProfileCard from '#/components/ProfileCard'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env'
import type * as bsky from '#/types/bsky'
import {bulkWriteFollows} from '../Onboarding/util'
@@ -54,13 +55,14 @@ import {bulkWriteFollows} from '../Onboarding/util'
type Props = NativeStackScreenProps<AllNavigatorParams, 'FindContactsSettings'>
export function FindContactsSettingsScreen({}: Props) {
const {_} = useLingui()
const ax = useAnalytics()
const {data, error, refetch} = useContactsSyncStatusQuery()
const isFocused = useIsFocused()
useEffect(() => {
if (data && isFocused) {
logger.metric('contacts:settings:presented', {
ax.metric('contacts:settings:presented', {
hasPreviouslySynced: !!data.syncStatus,
matchCount: data.syncStatus?.matchesCount,
})
@@ -169,6 +171,7 @@ function SyncStatus({
info: AppBskyContactDefs.SyncStatus
refetchStatus: () => Promise<any>
}) {
const ax = useAnalytics()
const agent = useAgent()
const queryClient = useQueryClient()
const {_} = useLingui()
@@ -197,7 +200,7 @@ function SyncStatus({
await agent.app.bsky.contact.dismissMatch({subject: did})
},
onMutate: async (did: string) => {
logger.metric('contacts:settings:dismiss', {})
ax.metric('contacts:settings:dismiss', {})
optimisticRemoveMatch(queryClient, did)
},
onError: err => {
@@ -278,6 +281,7 @@ function MatchItem({
}) {
const t = useTheme()
const {_} = useLingui()
const ax = useAnalytics()
const shadow = useProfileShadow(profile)
return (
@@ -314,7 +318,7 @@ function MatchItem({
profile={profile}
moderationOpts={moderationOpts}
logContext="FindContacts"
onFollow={() => logger.metric('contacts:settings:follow', {})}
onFollow={() => ax.metric('contacts:settings:follow', {})}
/>
{!shadow.viewer?.following && (
<Button
@@ -343,6 +347,7 @@ function StatusHeader({
isAnyUnfollowed: boolean
}) {
const {_} = useLingui()
const ax = useAnalytics()
const agent = useAgent()
const queryClient = useQueryClient()
const {currentAccount} = useSession()
@@ -374,7 +379,7 @@ function StatusHeader({
}
} while (cursor)
logger.metric('contacts:settings:followAll', {
ax.metric('contacts:settings:followAll', {
followCount: didsToFollow.length,
})
@@ -459,6 +464,7 @@ function StatusHeader({
function StatusFooter({syncedAt}: {syncedAt: string}) {
const {_, i18n} = useLingui()
const t = useTheme()
const ax = useAnalytics()
const agent = useAgent()
const queryClient = useQueryClient()
@@ -466,7 +472,7 @@ function StatusFooter({syncedAt}: {syncedAt: string}) {
mutationFn: async () => {
await agent.app.bsky.contact.removeData({})
},
onMutate: () => logger.metric('contacts:settings:removeData', {}),
onMutate: () => ax.metric('contacts:settings:removeData', {}),
onSuccess: () => {
Toast.show(_(msg`Contacts removed`))
queryClient.setQueryData<AppBskyContactGetSyncStatus.OutputSchema>(
@@ -520,7 +526,7 @@ function StatusFooter({syncedAt}: {syncedAt: string}) {
(Date.now() - new Date(syncedAt).getTime()) /
(1000 * 60 * 60 * 24),
)
logger.metric('contacts:settings:resync', {
ax.metric('contacts:settings:resync', {
daysSinceLastSync,
})
}}
@@ -4,12 +4,12 @@ import {type AppBskyNotificationDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {useNotificationSettingsUpdateMutation} from '#/state/queries/notifications/settings'
import {atoms as a, platform, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {Divider} from '../../components/SettingsList'
export function PreferenceControls({
@@ -61,6 +61,7 @@ export function Inner({
}) {
const t = useTheme()
const {_} = useLingui()
const ax = useAnalytics()
const {mutate} = useNotificationSettingsUpdateMutation()
const channels = useMemo(() => {
@@ -77,7 +78,7 @@ export function Inner({
push: change.includes('push'),
} satisfies typeof preference
logger.metric('activityPreference:changeChannels', {
ax.metric('activityPreference:changeChannels', {
name,
push: newPreference.push,
list: newPreference.list,
@@ -98,7 +99,7 @@ export function Inner({
include: change,
} satisfies typeof preference
logger.metric('activityPreference:changeFilter', {name, value: change})
ax.metric('activityPreference:changeFilter', {name, value: change})
mutate({
[name]: newPreference,
@@ -6,7 +6,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQuery, useQueryClient} from '@tanstack/react-query'
import {useAppState} from '#/lib/hooks/useAppState'
import {useAppState} from '#/lib/appState'
import {
type AllNavigatorParams,
type NativeStackScreenProps,
+3 -3
View File
@@ -15,7 +15,6 @@ import {
type CommonNavigatorParams,
type NavigationProp,
} from '#/lib/routes/types'
import {useGate} from '#/lib/statsig/statsig'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useProfileShadow} from '#/state/cache/profile-shadow'
@@ -69,12 +68,14 @@ import {
shouldShowVerificationCheckButton,
VerificationCheckButton,
} from '#/components/verification/VerificationCheckButton'
import {useAnalytics} from '#/analytics'
import {IS_INTERNAL, IS_IOS, IS_NATIVE} from '#/env'
import {device, useStorage} from '#/storage'
import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
export function SettingsScreen({}: Props) {
const ax = useAnalytics()
const {_} = useLingui()
const reducedMotion = useReducedMotion()
const {logoutEveryAccount} = useSessionApi()
@@ -92,7 +93,6 @@ export function SettingsScreen({}: Props) {
const [showDevOptions, setShowDevOptions] = useState(false)
const findContactsEnabled =
useIsFindContactsFeatureEnabledBasedOnGeolocation()
const gate = useGate()
return (
<Layout.Screen>
@@ -213,7 +213,7 @@ export function SettingsScreen({}: Props) {
</SettingsList.LinkItem>
{IS_NATIVE &&
findContactsEnabled &&
!gate('disable_settings_find_contacts') && (
!ax.features.enabled(ax.features.DisableSettingsFindContacts) && (
<SettingsList.LinkItem
to="/settings/find-contacts"
label={_(msg`Find friends from contacts`)}>
+7 -6
View File
@@ -11,8 +11,8 @@ import {useSignupContext} from '#/screens/Signup/state'
import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView'
import {atoms as a, useTheme} from '#/alf'
import {FormError} from '#/components/forms/FormError'
import {IS_ANDROID, IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
import {GCP_PROJECT_ID} from '#/env'
import {useAnalytics} from '#/analytics'
import {GCP_PROJECT_ID, IS_ANDROID, IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
import {BackNextButtons} from '../BackNextButtons'
const CAPTCHA_PATH =
@@ -71,6 +71,7 @@ function StepCaptchaInner({
payload?: string
}) {
const {_} = useLingui()
const ax = useAnalytics()
const theme = useTheme()
const {state, dispatch} = useSignupContext()
@@ -109,13 +110,13 @@ function StepCaptchaInner({
const onSuccess = React.useCallback(
(code: string) => {
setCompleted(true)
logger.metric('signup:captchaSuccess', {}, {statsig: true})
ax.metric('signup:captchaSuccess', {})
dispatch({
type: 'submit',
task: {verificationCode: code, mutableProcessed: false},
})
},
[dispatch],
[ax, dispatch],
)
const onError = React.useCallback(
@@ -124,13 +125,13 @@ function StepCaptchaInner({
type: 'setError',
value: _(msg`Error receiving captcha response.`),
})
logger.metric('signup:captchaFailure', {}, {statsig: true})
ax.metric('signup:captchaFailure', {})
logger.error('Signup Flow Error', {
registrationHandle: state.handle,
error,
})
},
[_, dispatch, state.handle],
[_, ax, dispatch, state.handle],
)
const onBackPress = React.useCallback(() => {
+13 -16
View File
@@ -26,11 +26,13 @@ import {useThrottledValue} from '#/components/hooks/useThrottledValue'
import {At_Stroke2_Corner0_Rounded as AtIcon} from '#/components/icons/At'
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {BackNextButtons} from '../BackNextButtons'
import {HandleSuggestions} from './HandleSuggestions'
export function StepHandle() {
const {_} = useLingui()
const ax = useAnalytics()
const t = useTheme()
const {state, dispatch} = useSignupContext()
const [draftValue, setDraftValue] = useState(state.handle)
@@ -68,16 +70,19 @@ export function StepHandle() {
const {available: handleAvailable} = await checkHandleAvailability(
createFullHandle(handle, state.userDomain),
state.serviceDescription?.did ?? 'UNKNOWN',
{typeahead: false},
{},
)
if (!handleAvailable) {
ax.metric('signup:handleTaken', {typeahead: false})
dispatch({
type: 'setError',
value: _(msg`That username is already taken`),
field: 'handle',
})
return
} else {
ax.metric('signup:handleAvailable', {typeahead: false})
}
} catch (error) {
logger.error('Failed to check handle availability on next press', {
@@ -88,15 +93,11 @@ export function StepHandle() {
dispatch({type: 'setIsLoading', value: false})
}
logger.metric(
'signup:nextPressed',
{
activeStep: state.activeStep,
phoneVerificationRequired:
state.serviceDescription?.phoneVerificationRequired,
},
{statsig: true},
)
ax.metric('signup:nextPressed', {
activeStep: state.activeStep,
phoneVerificationRequired:
state.serviceDescription?.phoneVerificationRequired,
})
// phoneVerificationRequired is actually whether a captcha is required
if (!state.serviceDescription?.phoneVerificationRequired) {
dispatch({
@@ -115,11 +116,7 @@ export function StepHandle() {
value: handle,
})
dispatch({type: 'prev'})
logger.metric(
'signup:backPressed',
{activeStep: state.activeStep},
{statsig: true},
)
ax.metric('signup:backPressed', {activeStep: state.activeStep})
}
const hasDebounceSettled = draftValue === debouncedDraftValue
@@ -202,7 +199,7 @@ export function StepHandle() {
state.userDomain.length * -1,
),
)
logger.metric('signup:handleSuggestionSelected', {
ax.metric('signup:handleSuggestionSelected', {
method: suggestion.method,
})
}}
+5 -7
View File
@@ -30,6 +30,7 @@ import {
MIN_ACCESS_AGE,
useAgeAssuranceRegionConfigWithFallback,
} from '#/ageAssurance/util'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env'
import {
useDeviceGeolocationApi,
@@ -59,6 +60,7 @@ export function StepInfo({
isLoadingStarterPack: boolean
}) {
const {_} = useLingui()
const ax = useAnalytics()
const {state, dispatch} = useSignupContext()
const preemptivelyCompleteActivePolicyUpdate =
usePreemptivelyCompleteActivePolicyUpdate()
@@ -165,13 +167,9 @@ export function StepInfo({
dispatch({type: 'setEmail', value: email})
dispatch({type: 'setPassword', value: password})
dispatch({type: 'next'})
logger.metric(
'signup:nextPressed',
{
activeStep: state.activeStep,
},
{statsig: true},
)
ax.metric('signup:nextPressed', {
activeStep: state.activeStep,
})
}
return (
+14 -3
View File
@@ -29,17 +29,28 @@ import {LinearGradientBackground} from '#/components/LinearGradientBackground'
import {InlineLinkText} from '#/components/Link'
import {ScreenTransition} from '#/components/ScreenTransition'
import {Text} from '#/components/Typography'
import {IS_ANDROID} from '#/env'
import {GCP_PROJECT_ID} from '#/env'
import {useAnalytics} from '#/analytics'
import {GCP_PROJECT_ID, IS_ANDROID} from '#/env'
import * as bsky from '#/types/bsky'
export function Signup({onPressBack}: {onPressBack: () => void}) {
const ax = useAnalytics()
const {_} = useLingui()
const t = useTheme()
const [state, dispatch] = useReducer(reducer, initialState)
const [state, dispatch] = useReducer(reducer, {
...initialState,
analytics: ax,
})
const {gtMobile} = useBreakpoints()
const submit = useSubmitSignup()
useEffect(() => {
dispatch({
type: 'setAnalytics',
value: ax,
})
}, [ax])
const activeStarterPack = useActiveStarterPack()
const {
data: starterPack,
+27 -23
View File
@@ -12,9 +12,9 @@ import {DEFAULT_SERVICE} from '#/lib/constants'
import {cleanError} from '#/lib/strings/errors'
import {createFullHandle} from '#/lib/strings/handles'
import {getAge} from '#/lib/strings/time'
import {logger} from '#/logger'
import {useSessionApi} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell'
import {type AnalyticsContextType, useAnalytics} from '#/analytics'
export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
@@ -39,6 +39,8 @@ type ErrorField =
| 'date-of-birth'
export type SignupState = {
analytics?: AnalyticsContextType
hasPrev: boolean
activeStep: SignupStep
screenTransitionDirection: 'Forward' | 'Backward'
@@ -65,6 +67,7 @@ export type SignupState = {
}
export type SignupAction =
| {type: 'setAnalytics'; value: AnalyticsContextType}
| {type: 'prev'}
| {type: 'next'}
| {type: 'finish'}
@@ -83,6 +86,8 @@ export type SignupAction =
| {type: 'incrementBackgroundCount'}
export const initialState: SignupState = {
analytics: undefined,
hasPrev: false,
activeStep: SignupStep.INFO,
screenTransitionDirection: 'Forward',
@@ -126,6 +131,10 @@ export function reducer(s: SignupState, a: SignupAction): SignupState {
let next = {...s}
switch (a.type) {
case 'setAnalytics': {
next.analytics = a.value
break
}
case 'prev': {
if (s.activeStep !== SignupStep.INFO) {
next.screenTransitionDirection = 'Backward'
@@ -194,16 +203,12 @@ export function reducer(s: SignupState, a: SignupAction): SignupState {
next.fieldErrors[a.field] = (next.fieldErrors[a.field] || 0) + 1
// Log the field error
logger.metric(
'signup:fieldError',
{
field: a.field,
errorCount: next.fieldErrors[a.field],
errorMessage: a.value,
activeStep: next.activeStep,
},
{statsig: true},
)
s.analytics?.metric('signup:fieldError', {
field: a.field,
errorCount: next.fieldErrors[a.field],
errorMessage: a.value,
activeStep: next.activeStep,
})
}
break
}
@@ -220,24 +225,22 @@ export function reducer(s: SignupState, a: SignupAction): SignupState {
next.backgroundCount = s.backgroundCount + 1
// Log background/foreground event during signup
logger.metric(
'signup:backgrounded',
{
activeStep: next.activeStep,
backgroundCount: next.backgroundCount,
},
{statsig: true},
)
s.analytics?.metric('signup:backgrounded', {
activeStep: next.activeStep,
backgroundCount: next.backgroundCount,
})
break
}
}
next.hasPrev = next.activeStep !== SignupStep.INFO
logger.debug('signup', next)
s.analytics?.logger.debug('signup', next)
if (s.activeStep !== next.activeStep) {
logger.debug('signup: step changed', {activeStep: next.activeStep})
s.analytics?.logger.debug('signup: step changed', {
activeStep: next.activeStep,
})
}
return next
@@ -252,6 +255,7 @@ SignupContext.displayName = 'SignupContext'
export const useSignupContext = () => React.useContext(SignupContext)
export function useSubmitSignup() {
const ax = useAnalytics()
const {_} = useLingui()
const {createAccount} = useSessionApi()
const onboardingDispatch = useOnboardingDispatch()
@@ -295,7 +299,7 @@ export function useSubmitSignup() {
!state.pendingSubmit?.verificationCode
) {
dispatch({type: 'setStep', value: SignupStep.CAPTCHA})
logger.error('Signup Flow Error', {
ax.logger.error('Signup Flow Error', {
errorMessage: 'Verification captcha code was not set.',
registrationHandle: state.handle,
})
@@ -358,7 +362,7 @@ export function useSubmitSignup() {
})
dispatch({type: 'setStep', value: isHandleError ? 2 : 1})
logger.error('Signup Flow Error', {
ax.logger.error('Signup Flow Error', {
errorMessage: error,
registrationHandle: state.handle,
})
@@ -13,7 +13,6 @@ import {useLingui} from '@lingui/react'
import {JOINED_THIS_WEEK} from '#/lib/constants'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {logEvent} from '#/lib/statsig/statsig'
import {createStarterPackGooglePlayUri} from '#/lib/strings/starter-pack'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useStarterPackQuery} from '#/state/queries/starter-packs'
@@ -36,6 +35,7 @@ import {Default as ProfileCard} from '#/components/ProfileCard'
import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_WEB, IS_WEB_MOBILE_ANDROID} from '#/env'
import * as bsky from '#/types/bsky'
@@ -119,6 +119,7 @@ function LandingScreenLoaded({
}) {
const {creator, listItemsSample, feeds} = starterPack
const {_, i18n} = useLingui()
const ax = useAnalytics()
const t = useTheme()
const activeStarterPack = useActiveStarterPack()
const setActiveStarterPack = useSetActiveStarterPack()
@@ -146,7 +147,7 @@ function LandingScreenLoaded({
} else {
onContinue()
}
logEvent('starterPack:ctaPress', {
ax.metric('starterPack:ctaPress', {
starterPack: starterPack.uri,
})
}
+15 -10
View File
@@ -23,7 +23,6 @@ import {
type CommonNavigatorParams,
type NavigationProp,
} from '#/lib/routes/types'
import {logEvent} from '#/lib/statsig/statsig'
import {cleanError} from '#/lib/strings/errors'
import {getStarterPackOgCard} from '#/lib/strings/starter-pack'
import {logger} from '#/logger'
@@ -33,8 +32,10 @@ import {getAllListMembers} from '#/state/queries/list-members'
import {useResolvedStarterPackShortLink} from '#/state/queries/resolve-short-link'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useShortenLink} from '#/state/queries/shorten-link'
import {useDeleteStarterPackMutation} from '#/state/queries/starter-packs'
import {useStarterPackQuery} from '#/state/queries/starter-packs'
import {
useDeleteStarterPackMutation,
useStarterPackQuery,
} from '#/state/queries/starter-packs'
import {useAgent, useSession} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {
@@ -71,6 +72,7 @@ import {ProfilesList} from '#/components/StarterPack/Main/ProfilesList'
import {QrCodeDialog} from '#/components/StarterPack/QrCodeDialog'
import {ShareDialog} from '#/components/StarterPack/ShareDialog'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_WEB} from '#/env'
import * as bsky from '#/types/bsky'
@@ -184,6 +186,7 @@ function StarterPackScreenLoaded({
const showFeedsTab = Boolean(starterPack.feeds?.length)
const showPostsTab = Boolean(starterPack.list)
const {_} = useLingui()
const ax = useAnalytics()
const tabs = [
...(showPeopleTab ? [_(msg`People`)] : []),
@@ -199,10 +202,10 @@ function StarterPackScreenLoaded({
const [imageLoaded, setImageLoaded] = React.useState(false)
React.useEffect(() => {
logEvent('starterPack:opened', {
ax.metric('starterPack:opened', {
starterPack: starterPack.uri,
})
}, [starterPack.uri])
}, [ax, starterPack.uri])
const onOpenShareDialog = React.useCallback(() => {
const rkey = new AtUri(starterPack.uri).rkey
@@ -243,7 +246,7 @@ function StarterPackScreenLoaded({
? ({headerHeight, scrollElRef}) => (
<ProfilesList
// Validated above
listUri={starterPack!.list!.uri}
listUri={starterPack.list!.uri}
headerHeight={headerHeight}
// @ts-expect-error
scrollElRef={scrollElRef}
@@ -266,7 +269,7 @@ function StarterPackScreenLoaded({
? ({headerHeight, scrollElRef}) => (
<PostsList
// Validated above
listUri={starterPack!.list!.uri}
listUri={starterPack.list!.uri}
headerHeight={headerHeight}
// @ts-expect-error
scrollElRef={scrollElRef}
@@ -315,6 +318,7 @@ function Header({
const {record, creator} = starterPack
const isOwn = creator?.did === currentAccount?.did
const joinedAllTimeCount = starterPack.joinedAllTimeCount ?? 0
const ax = useAnalytics()
const navigation = useNavigation<NavigationProp>()
@@ -385,7 +389,7 @@ function Header({
})
Toast.show(_(msg`All accounts have been followed!`))
captureAction(ProgressGuideAction.Follow, dids.length)
logEvent('starterPack:followAll', {
ax.metric('starterPack:followAll', {
logContext: 'StarterPackProfilesList',
starterPack: starterPack.uri,
count: dids.length,
@@ -516,6 +520,7 @@ function OverflowMenu({
}) {
const t = useTheme()
const {_} = useLingui()
const ax = useAnalytics()
const {gtMobile} = useBreakpoints()
const {currentAccount} = useSession()
const reportDialogControl = useReportDialogControl()
@@ -528,7 +533,7 @@ function OverflowMenu({
error: deleteError,
} = useDeleteStarterPackMutation({
onSuccess: () => {
logEvent('starterPack:delete', {})
ax.metric('starterPack:delete', {})
deleteDialogControl.close(() => {
if (navigation.canGoBack()) {
navigation.popToTop()
@@ -554,7 +559,7 @@ function OverflowMenu({
rkey: routeParams.rkey,
listUri: starterPack.list.uri,
})
logEvent('starterPack:delete', {})
ax.metric('starterPack:delete', {})
}
return (
+4 -3
View File
@@ -22,7 +22,6 @@ import {
type CommonNavigatorParams,
type NavigationProp,
} from '#/lib/routes/types'
import {logEvent} from '#/lib/statsig/statsig'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {enforceLen} from '#/lib/strings/helpers'
@@ -58,6 +57,7 @@ import {ListMaybePlaceholder} from '#/components/Lists'
import {Loader} from '#/components/Loader'
import {WizardEditListDialog} from '#/components/StarterPack/Wizard/WizardEditListDialog'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env'
import type * as bsky from '#/types/bsky'
import {Provider} from './State'
@@ -167,6 +167,7 @@ function WizardInner({
onSuccess?: () => void
}) {
const navigation = useNavigation<NavigationProp>()
const ax = useAnalytics()
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const [state, dispatch] = useWizardState()
@@ -222,7 +223,7 @@ function WizardInner({
const onSuccessCreate = (data: {uri: string; cid: string}) => {
const rkey = new AtUri(data.uri).rkey
logEvent('starterPack:create', {
ax.metric('starterPack:create', {
setName: state.name != null,
setDescription: state.description != null,
profilesCount: state.profiles.length,
@@ -236,7 +237,7 @@ function WizardInner({
onSuccess?.()
} else {
navigation.replace('StarterPack', {
name: profile!.handle,
name: profile.handle,
rkey,
new: true,
})
+12 -18
View File
@@ -21,8 +21,7 @@ import {
useSafeAreaFrame,
useSafeAreaInsets,
} from 'react-native-safe-area-context'
import {useEvent} from 'expo'
import {useEventListener} from 'expo'
import {useEvent, useEventListener} from 'expo'
import {Image, type ImageStyle} from 'expo-image'
import {LinearGradient} from 'expo-linear-gradient'
import {createVideoPlayer, type VideoPlayer, VideoView} from 'expo-video'
@@ -66,13 +65,12 @@ import {
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {
FeedFeedbackProvider,
useFeedFeedback,
useFeedFeedbackContext,
} from '#/state/feed-feedback'
import {useFeedFeedback} from '#/state/feed-feedback'
import {useFeedInfo} from '#/state/queries/feed'
import {usePostLikeMutationQueue} from '#/state/queries/post'
import {
type AuthorFilter,
type FeedPostSliceItem,
usePostFeedQuery,
} from '#/state/queries/post-feed'
@@ -100,6 +98,7 @@ import * as Hider from '#/components/moderation/Hider'
import {PostControls} from '#/components/PostControls'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_ANDROID} from '#/env'
import * as bsky from '#/types/bsky'
import {Scrubber, VIDEO_PLAYER_BOTTOM_INSET} from './components/Scrubber'
@@ -192,11 +191,9 @@ function Feed() {
const feedDesc = useMemo(() => {
switch (params.type) {
case 'feedgen':
return `feedgen|${params.uri as string}` as const
return `feedgen|${params.uri}` as const
case 'author':
return `author|${params.did as string}|${
params.filter as AuthorFilter
}` as const
return `author|${params.did}|${params.filter}` as const
default:
throw new Error(`Invalid video feed params ${JSON.stringify(params)}`)
}
@@ -490,6 +487,7 @@ let VideoItem = ({
feedContext: string | undefined
reqId: string | undefined
}): React.ReactNode => {
const ax = useAnalytics()
const postShadow = usePostShadow(post)
const {width, height} = useSafeAreaFrame()
const {sendInteraction, feedDescriptor} = useFeedFeedbackContext()
@@ -507,16 +505,12 @@ let VideoItem = ({
// Track post:view event
if (!hasTrackedView.current) {
hasTrackedView.current = true
logger.metric(
'post:view',
{
uri: post.uri,
authorDid: post.author.did,
logContext: 'ImmersiveVideo',
feedDescriptor,
},
{statsig: false},
)
ax.metric('post:view', {
uri: post.uri,
authorDid: post.author.did,
logContext: 'ImmersiveVideo',
feedDescriptor,
})
}
}
}, [