[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
+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`)}>