[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
+4 -3
View File
@@ -11,7 +11,6 @@ import {Image} from 'expo-image'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logEvent} from '#/lib/statsig/statsig'
import {cleanError} from '#/lib/strings/errors'
import {
type Gif,
@@ -30,6 +29,7 @@ import {useThrottledValue} from '#/components/hooks/useThrottledValue'
import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
import {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
import {useAnalytics} from '#/analytics'
import {IS_WEB} from '#/env'
export function GifSelectDialog({
@@ -280,14 +280,15 @@ export function GifPreview({
gif: Gif
onSelectGif: (gif: Gif) => void
}) {
const ax = useAnalytics()
const {gtTablet} = useBreakpoints()
const {_} = useLingui()
const t = useTheme()
const onPress = useCallback(() => {
logEvent('composer:gif:select', {})
ax.metric('composer:gif:select', {})
onSelectGif(gif)
}, [onSelectGif, gif])
}, [ax, onSelectGif, gif])
return (
<Button
@@ -11,7 +11,6 @@ import {useQueryClient} from '@tanstack/react-query'
import {useHaptics} from '#/lib/haptics'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {useMyListsQuery} from '#/state/queries/my-lists'
import {useGetPost} from '#/state/queries/post'
@@ -51,6 +50,7 @@ import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/ico
import {CloseQuote_Stroke2_Corner1_Rounded as QuoteIcon} from '#/components/icons/Quote'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_IOS} from '#/env'
export type PostInteractionSettingsFormProps = {
@@ -80,8 +80,9 @@ export function PostInteractionSettingsControlledDialog({
}: PostInteractionSettingsFormProps & {
control: Dialog.DialogControlProps
}) {
const ax = useAnalytics()
const onClose = useNonReactiveCallback(() => {
logger.metric('composer:threadgate:save', {
ax.metric('composer:threadgate:save', {
hasChanged: !!rest.isDirty,
persist: !!rest.persist,
replyOptions:
@@ -161,6 +162,7 @@ export function PostInteractionSettingsDialog(
export function PostInteractionSettingsDialogControlledInner(
props: PostInteractionSettingsDialogProps,
) {
const ax = useAnalytics()
const {_} = useLingui()
const {currentAccount} = useSession()
const [isSaving, setIsSaving] = useState(false)
@@ -229,7 +231,7 @@ export function PostInteractionSettingsDialogControlledInner(
props.control.close()
} catch (e: any) {
logger.error(`Failed to save post interaction settings`, {
ax.logger.error(`Failed to save post interaction settings`, {
source: 'PostInteractionSettingsDialogControlledInner',
safeMessage: e.message,
})
@@ -244,6 +246,7 @@ export function PostInteractionSettingsDialogControlledInner(
}
}, [
_,
ax,
props.postUri,
props.rootPostUri,
props.control,
@@ -689,6 +692,7 @@ export function usePrefetchPostInteractionSettings({
postUri: string
rootPostUri: string
}) {
const ax = useAnalytics()
const queryClient = useQueryClient()
const agent = useAgent()
const getPost = useGetPost()
@@ -712,9 +716,9 @@ export function usePrefetchPostInteractionSettings({
}),
])
} catch (e: any) {
logger.error(`Failed to prefetch post interaction settings`, {
ax.logger.error(`Failed to prefetch post interaction settings`, {
safeMessage: e.message,
})
}
}, [queryClient, agent, postUri, rootPostUri, getPost])
}, [ax, queryClient, agent, postUri, rootPostUri, getPost])
}
+5 -5
View File
@@ -1,11 +1,9 @@
import {useCallback, useImperativeHandle, useRef, useState} from 'react'
import {View} from 'react-native'
import {useWindowDimensions} from 'react-native'
import {useWindowDimensions, View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {BSKY_SERVICE} from '#/lib/constants'
import {logger} from '#/logger'
import * as persisted from '#/state/persisted'
import {useSession} from '#/state/session'
import {atoms as a, platform, useBreakpoints, useTheme, web} from '#/alf'
@@ -17,6 +15,7 @@ import * as TextField from '#/components/forms/TextField'
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
type SegmentedControlOptions = typeof BSKY_SERVICE | 'custom'
@@ -27,6 +26,7 @@ export function ServerInputDialog({
control: Dialog.DialogOuterProps['control']
onSelect: (url: string) => void
}) {
const ax = useAnalytics()
const {height} = useWindowDimensions()
const formRef = useRef<DialogInnerRef>(null)
@@ -43,10 +43,10 @@ export function ServerInputDialog({
setPreviousCustomAddress(result)
}
}
logger.metric('signin:hostingProviderPressed', {
ax.metric('signin:hostingProviderPressed', {
hostingProviderDidChange: fixedOption !== BSKY_SERVICE,
})
}, [onSelect, fixedOption])
}, [ax, onSelect, fixedOption])
return (
<Dialog.Outer
+5 -4
View File
@@ -11,7 +11,6 @@ import {useQueryClient} from '@tanstack/react-query'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {type NavigationProp} from '#/lib/routes/types'
import {logger} from '#/logger'
import {
invalidateActorStarterPacksWithMembershipQuery,
useActorStarterPacksWithMembershipsQuery,
@@ -31,6 +30,7 @@ import {StarterPack} from '#/components/icons/StarterPack'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_WEB} from '#/env'
import * as bsky from '#/types/bsky'
@@ -244,8 +244,9 @@ function StarterPackItem({
starterPackWithMembership: StarterPackWithMembership
targetDid: string
}) {
const {_} = useLingui()
const t = useTheme()
const ax = useAnalytics()
const {_} = useLingui()
const queryClient = useQueryClient()
const starterPack = starterPackWithMembership.starterPack
@@ -304,7 +305,7 @@ function StarterPackItem({
listUri: listUri,
actorDid: targetDid,
})
logger.metric('starterPack:addUser', {starterPack: starterPackUri})
ax.metric('starterPack:addUser', {starterPack: starterPackUri})
} else {
if (!starterPackWithMembership.listItem?.uri) {
console.error('Cannot remove: missing membership URI')
@@ -316,7 +317,7 @@ function StarterPackItem({
actorDid: targetDid,
membershipUri: starterPackWithMembership.listItem.uri,
})
logger.metric('starterPack:removeUser', {starterPack: starterPackUri})
ax.metric('starterPack:removeUser', {starterPack: starterPackUri})
}
}
@@ -5,7 +5,6 @@ import {LinearGradient} from 'expo-linear-gradient'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {atoms as a, useTheme, web} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {isFindContactsFeatureEnabled} from '#/components/contacts/country-allowlist'
@@ -16,8 +15,8 @@ import {
isExistingUserAsOf,
} from '#/components/dialogs/nuxs/utils'
import {Text} from '#/components/Typography'
import {IS_NATIVE, IS_WEB} from '#/env'
import {IS_E2E} from '#/env'
import {useAnalytics} from '#/analytics'
import {IS_E2E, IS_NATIVE, IS_WEB} from '#/env'
import {navigate} from '#/Navigation'
export const enabled = createIsEnabledCheck(props => {
@@ -35,6 +34,7 @@ export const enabled = createIsEnabledCheck(props => {
export function FindContactsAnnouncement() {
const t = useTheme()
const {_} = useLingui()
const ax = useAnalytics()
const nuxDialogs = useNuxDialogContext()
const control = Dialog.useDialogControl()
@@ -115,7 +115,7 @@ export function FindContactsAnnouncement() {
size="large"
color="primary"
onPress={() => {
logger.metric('contacts:nux:ctaPressed', {})
ax.metric('contacts:nux:ctaPressed', {})
control.close(() => {
navigate('FindContactsFlow')
})
@@ -5,7 +5,6 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {urls} from '#/lib/constants'
import {logger} from '#/logger'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
@@ -14,11 +13,13 @@ import {Sparkle_Stroke2_Corner0_Rounded as SparkleIcon} from '#/components/icons
import {VerifierCheck} from '#/components/icons/VerifierCheck'
import {Link} from '#/components/Link'
import {Span, Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env'
export function InitialVerificationAnnouncement() {
const t = useTheme()
const {_} = useLingui()
const ax = useAnalytics()
const {gtMobile} = useBreakpoints()
const nuxDialogs = useNuxDialogContext()
const control = Dialog.useDialogControl()
@@ -161,13 +162,9 @@ export function InitialVerificationAnnouncement() {
color="primary"
style={[a.justify_center, a.w_full]}
onPress={() => {
logger.metric(
'verification:learn-more',
{
location: 'initialAnnouncementeNux',
},
{statsig: false},
)
ax.metric('verification:learn-more', {
location: 'initialAnnouncementeNux',
})
}}>
<ButtonText>
<Trans>Read blog post</Trans>
@@ -24,7 +24,7 @@ export const enabled = createIsEnabledCheck(props => {
'2026-01-16T00:00:00.000Z',
props.currentProfile.createdAt,
) &&
!props.gate('disable_live_now_beta')
!props.features.enabled(props.features.DisableLiveNowBeta)
)
})
+4 -4
View File
@@ -8,7 +8,6 @@ import {
} from 'react'
import {type AppBskyActorDefs} from '@atproto/api'
import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {Nux, useNuxs, useResetNuxs, useSaveNux} from '#/state/queries/nuxs'
@@ -25,6 +24,7 @@ import {
} from '#/components/dialogs/nuxs/LiveNowBetaDialog'
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
import {type EnabledCheckProps} from '#/components/dialogs/nuxs/utils'
import {useAnalytics} from '#/analytics'
import {useGeolocation} from '#/geolocation'
type Context = {
@@ -88,7 +88,7 @@ function Inner({
currentProfile: AppBskyActorDefs.ProfileViewDetailed
preferences: UsePreferencesQueryResponse
}) {
const gate = useGate()
const ax = useAnalytics()
const geolocation = useGeolocation()
const {nuxs} = useNuxs()
const [snoozed, setSnoozed] = useState(() => {
@@ -133,7 +133,7 @@ function Inner({
if (
enabled &&
!enabled({
gate,
features: ax.features,
currentAccount,
currentProfile,
preferences,
@@ -165,11 +165,11 @@ function Inner({
break
}
}, [
ax.features,
nuxs,
snoozed,
snoozeNuxDialog,
saveNux,
gate,
currentAccount,
currentProfile,
preferences,
+2 -2
View File
@@ -1,12 +1,12 @@
import {type AppBskyActorDefs} from '@atproto/api'
import {type useGate} from '#/lib/statsig/statsig'
import {type UsePreferencesQueryResponse} from '#/state/queries/preferences'
import {type SessionAccount} from '#/state/session'
import {type AnalyticsContextType} from '#/analytics'
import {type Geolocation} from '#/geolocation'
export type EnabledCheckProps = {
gate: ReturnType<typeof useGate>
features: AnalyticsContextType['features']
currentAccount: SessionAccount
currentProfile: AppBskyActorDefs.ProfileViewDetailed
preferences: UsePreferencesQueryResponse