[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
+9 -10
View File
@@ -7,8 +7,8 @@ import {
PUBLIC_BSKY_SERVICE,
} from '#/lib/constants'
import {createFullHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
import {useDebouncedValue} from '#/components/live/utils'
import {useAnalytics} from '#/analytics'
import * as bsky from '#/types/bsky'
import {Agent} from '../session/agent'
@@ -36,6 +36,7 @@ export function useHandleAvailabilityQuery(
},
debounceDelayMs = 500,
) {
const ax = useAnalytics()
const name = username.trim()
const debouncedHandle = useDebouncedValue(name, debounceDelayMs)
@@ -51,11 +52,16 @@ export function useHandleAvailabilityQuery(
),
queryFn: async () => {
const handle = createFullHandle(name, serviceDomain)
return await checkHandleAvailability(handle, serviceDid, {
const res = await checkHandleAvailability(handle, serviceDid, {
email,
birthDate,
typeahead: true,
})
if (res.available) {
ax.metric('signup:handleAvailable', {typeahead: true})
} else {
ax.metric('signup:handleTaken', {typeahead: true})
}
return res
},
}),
}
@@ -67,11 +73,9 @@ export async function checkHandleAvailability(
{
email,
birthDate,
typeahead,
}: {
email?: string
birthDate?: string
typeahead?: boolean
},
) {
if (serviceDid === BSKY_SERVICE_DID) {
@@ -89,8 +93,6 @@ export async function checkHandleAvailability(
ComAtprotoTempCheckHandleAvailability.isResultAvailable,
)
) {
logger.metric('signup:handleAvailable', {typeahead}, {statsig: true})
return {available: true} as const
} else if (
bsky.dangerousIsType<ComAtprotoTempCheckHandleAvailability.ResultUnavailable>(
@@ -98,7 +100,6 @@ export async function checkHandleAvailability(
ComAtprotoTempCheckHandleAvailability.isResultUnavailable,
)
) {
logger.metric('signup:handleTaken', {typeahead}, {statsig: true})
return {
available: false,
suggestions: data.result.suggestions,
@@ -117,11 +118,9 @@ export async function checkHandleAvailability(
})
if (res.data.did) {
logger.metric('signup:handleTaken', {typeahead}, {statsig: true})
return {available: false} as const
}
} catch {}
logger.metric('signup:handleAvailable', {typeahead}, {statsig: true})
return {available: true} as const
}
}
+16 -14
View File
@@ -3,12 +3,12 @@ import {type AppBskyActorDefs, type AppBskyFeedDefs, AtUri} from '@atproto/api'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
import {type LogEvents, toClout} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {updatePostShadow} from '#/state/cache/post-shadow'
import {type Shadow} from '#/state/cache/types'
import {useAgent, useSession} from '#/state/session'
import * as userActionHistory from '#/state/userActionHistory'
import {useAnalytics} from '#/analytics'
import {type Metrics, toClout} from '#/analytics/metrics'
import {useIsThreadMuted, useSetThreadMute} from '../cache/thread-mutes'
import {findProfileQueryData} from './profile'
@@ -103,8 +103,7 @@ export function usePostLikeMutationQueue(
post: Shadow<AppBskyFeedDefs.PostView>,
viaRepost: {uri: string; cid: string} | undefined,
feedDescriptor: string | undefined,
logContext: LogEvents['post:like']['logContext'] &
LogEvents['post:unlike']['logContext'],
logContext: Metrics['post:like']['logContext'],
) {
const queryClient = useQueryClient()
const postUri = post.uri
@@ -164,13 +163,14 @@ export function usePostLikeMutationQueue(
function usePostLikeMutation(
feedDescriptor: string | undefined,
logContext: LogEvents['post:like']['logContext'],
logContext: Metrics['post:like']['logContext'],
post: Shadow<AppBskyFeedDefs.PostView>,
) {
const {currentAccount} = useSession()
const queryClient = useQueryClient()
const postAuthor = post.author
const agent = useAgent()
const ax = useAnalytics()
return useMutation<
{uri: string}, // responds with the uri of the like
Error,
@@ -181,7 +181,7 @@ function usePostLikeMutation(
if (currentAccount) {
ownProfile = findProfileQueryData(queryClient, currentAccount.did)
}
logger.metric('post:like', {
ax.metric('post:like', {
uri,
authorDid: postAuthor.did,
logContext,
@@ -207,13 +207,14 @@ function usePostLikeMutation(
function usePostUnlikeMutation(
feedDescriptor: string | undefined,
logContext: LogEvents['post:unlike']['logContext'],
logContext: Metrics['post:unlike']['logContext'],
post: Shadow<AppBskyFeedDefs.PostView>,
) {
const agent = useAgent()
const ax = useAnalytics()
return useMutation<void, Error, {postUri: string; likeUri: string}>({
mutationFn: ({postUri, likeUri}) => {
logger.metric('post:unlike', {
ax.metric('post:unlike', {
uri: postUri,
authorDid: post.author.did,
logContext,
@@ -228,8 +229,7 @@ export function usePostRepostMutationQueue(
post: Shadow<AppBskyFeedDefs.PostView>,
viaRepost: {uri: string; cid: string} | undefined,
feedDescriptor: string | undefined,
logContext: LogEvents['post:repost']['logContext'] &
LogEvents['post:unrepost']['logContext'],
logContext: Metrics['post:repost']['logContext'],
) {
const queryClient = useQueryClient()
const postUri = post.uri
@@ -291,17 +291,18 @@ export function usePostRepostMutationQueue(
function usePostRepostMutation(
feedDescriptor: string | undefined,
logContext: LogEvents['post:repost']['logContext'],
logContext: Metrics['post:repost']['logContext'],
post: Shadow<AppBskyFeedDefs.PostView>,
) {
const agent = useAgent()
const ax = useAnalytics()
return useMutation<
{uri: string}, // responds with the uri of the repost
Error,
{uri: string; cid: string; via?: {uri: string; cid: string}} // the post's uri and cid, and the repost uri/cid if present
>({
mutationFn: ({uri, cid, via}) => {
logger.metric('post:repost', {
ax.metric('post:repost', {
uri,
authorDid: post.author.did,
logContext,
@@ -314,13 +315,14 @@ function usePostRepostMutation(
function usePostUnrepostMutation(
feedDescriptor: string | undefined,
logContext: LogEvents['post:unrepost']['logContext'],
logContext: Metrics['post:unrepost']['logContext'],
post: Shadow<AppBskyFeedDefs.PostView>,
) {
const agent = useAgent()
const ax = useAnalytics()
return useMutation<void, Error, {postUri: string; repostUri: string}>({
mutationFn: ({postUri, repostUri}) => {
logger.metric('post:unrepost', {
ax.metric('post:unrepost', {
uri: postUri,
authorDid: post.author.did,
logContext,
+6 -8
View File
@@ -9,7 +9,6 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {replaceEqualDeep} from '#/lib/functions'
import {getAge} from '#/lib/strings/time'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {
DEFAULT_HOME_FEED_PREFS,
@@ -24,6 +23,7 @@ import {useAgent} from '#/state/session'
import {saveLabelers} from '#/state/session/agent-config'
import {useAgeAssurance} from '#/ageAssurance'
import {makeAgeRestrictedModerationPrefs} from '#/ageAssurance/util'
import {useAnalytics} from '#/analytics'
export * from '#/state/queries/preferences/const'
export * from '#/state/queries/preferences/moderation'
@@ -110,6 +110,7 @@ export function useClearPreferencesMutation() {
}
export function usePreferencesSetContentLabelMutation() {
const ax = useAnalytics()
const agent = useAgent()
const queryClient = useQueryClient()
@@ -120,11 +121,7 @@ export function usePreferencesSetContentLabelMutation() {
>({
mutationFn: async ({label, visibility, labelerDid}) => {
await agent.setContentLabelPref(label, visibility, labelerDid)
logger.metric(
'moderation:changeLabelPreference',
{preference: visibility},
{statsig: true},
)
ax.metric('moderation:changeLabelPreference', {preference: visibility})
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -417,6 +414,7 @@ export function useSetActiveProgressGuideMutation() {
}
export function useSetVerificationPrefsMutation() {
const ax = useAnalytics()
const queryClient = useQueryClient()
const agent = useAgent()
@@ -424,9 +422,9 @@ export function useSetVerificationPrefsMutation() {
mutationFn: async prefs => {
await agent.setVerificationPrefs(prefs)
if (prefs.hideBadges) {
logger.metric('verification:settings:hideBadges', {}, {statsig: true})
ax.metric('verification:settings:hideBadges', {})
} else {
logger.metric('verification:settings:unHideBadges', {}, {statsig: true})
ax.metric('verification:settings:unHideBadges', {})
}
// triggers a refetch
await queryClient.invalidateQueries({
@@ -2,13 +2,13 @@ import {useCallback, useMemo, useRef, useState} from 'react'
import {type AppBskyUnspeccedGetPostThreadV2} from '@atproto/api'
import debounce from 'lodash.debounce'
import {OnceKey, useCallOnce} from '#/lib/hooks/useCallOnce'
import {logger} from '#/logger'
import {useCallOnce} from '#/lib/once'
import {
usePreferencesQuery,
useSetThreadViewPreferencesMutation,
} from '#/state/queries/preferences'
import {type ThreadViewPreferences} from '#/state/queries/preferences/types'
import {useAnalytics} from '#/analytics'
import {type Literal} from '#/types/utils'
export type ThreadSortOption = Literal<
@@ -28,9 +28,10 @@ export type ThreadPreferences = {
export function useThreadPreferences({
save,
}: {save?: boolean} = {}): ThreadPreferences {
const ax = useAnalytics()
const {data: preferences} = usePreferencesQuery()
const serverPrefs = preferences?.threadViewPrefs
const once = useCallOnce(OnceKey.PreferencesThread)
const once = useCallOnce()
/*
* Create local state representations of server state
@@ -61,7 +62,7 @@ export function useThreadPreferences({
)
once(() => {
logger.metric('thread:preferences:load', {
ax.metric('thread:preferences:load', {
sort: serverPrefs.sort,
view: serverPrefs.lab_treeViewEnabled ? 'tree' : 'linear',
})
@@ -76,12 +77,12 @@ export function useThreadPreferences({
try {
setIsSaving(true)
await mutateAsync(prefs)
logger.metric('thread:preferences:update', {
ax.metric('thread:preferences:update', {
sort: prefs.sort,
view: prefs.lab_treeViewEnabled ? 'tree' : 'linear',
})
} catch (e) {
logger.error('useThreadPreferences failed to save', {
ax.logger.error('useThreadPreferences failed to save', {
safeMessage: e,
})
} finally {
+9 -7
View File
@@ -22,7 +22,6 @@ import {
import {uploadBlob} from '#/lib/api'
import {until} from '#/lib/async/until'
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
import {logEvent, type LogEvents, toClout} from '#/lib/statsig/statsig'
import {updateProfileShadow} from '#/state/cache/profile-shadow'
import {type Shadow} from '#/state/cache/types'
import {type ImageMeta} from '#/state/gallery'
@@ -36,6 +35,8 @@ import {
import {useUpdateProfileVerificationCache} from '#/state/queries/verification/useUpdateProfileVerificationCache'
import {useAgent, useSession} from '#/state/session'
import * as userActionHistory from '#/state/userActionHistory'
import {useAnalytics} from '#/analytics'
import {type Metrics, toClout} from '#/analytics/metrics'
import type * as bsky from '#/types/bsky'
import {
ProgressGuideAction,
@@ -243,8 +244,7 @@ export function useProfileUpdateMutation() {
export function useProfileFollowMutationQueue(
profile: Shadow<bsky.profile.AnyProfileView>,
logContext: LogEvents['profile:follow']['logContext'] &
LogEvents['profile:follow']['logContext'],
logContext: Metrics['profile:follow']['logContext'],
position?: number,
contextProfileDid?: string,
) {
@@ -364,11 +364,12 @@ export function useProfileFollowMutationQueue(
}
function useProfileFollowMutation(
logContext: LogEvents['profile:follow']['logContext'],
logContext: Metrics['profile:follow']['logContext'],
profile: Shadow<bsky.profile.AnyProfileView>,
position?: number,
contextProfileDid?: string,
) {
const ax = useAnalytics()
const {currentAccount} = useSession()
const agent = useAgent()
const queryClient = useQueryClient()
@@ -381,7 +382,7 @@ function useProfileFollowMutation(
ownProfile = findProfileQueryData(queryClient, currentAccount.did)
}
captureAction(ProgressGuideAction.Follow)
logEvent('profile:follow', {
ax.metric('profile:follow', {
logContext,
didBecomeMutual: profile.viewer
? Boolean(profile.viewer.followedBy)
@@ -401,12 +402,13 @@ function useProfileFollowMutation(
}
function useProfileUnfollowMutation(
logContext: LogEvents['profile:unfollow']['logContext'],
logContext: Metrics['profile:unfollow']['logContext'],
) {
const ax = useAnalytics()
const agent = useAgent()
return useMutation<void, Error, {did: string; followUri: string}>({
mutationFn: async ({followUri}) => {
logEvent('profile:unfollow', {logContext})
ax.metric('profile:unfollow', {logContext})
return await agent.deleteFollow(followUri)
},
})
@@ -2,12 +2,13 @@ import {type AppBskyActorGetProfile} from '@atproto/api'
import {useMutation} from '@tanstack/react-query'
import {until} from '#/lib/async/until'
import {logger} from '#/logger'
import {useUpdateProfileVerificationCache} from '#/state/queries/verification/useUpdateProfileVerificationCache'
import {useAgent, useSession} from '#/state/session'
import {useAnalytics} from '#/analytics'
import type * as bsky from '#/types/bsky'
export function useVerificationCreateMutation() {
const ax = useAnalytics()
const agent = useAgent()
const {currentAccount} = useSession()
const updateProfileVerificationCache = useUpdateProfileVerificationCache()
@@ -46,7 +47,7 @@ export function useVerificationCreateMutation() {
)
},
async onSuccess(_, {profile}) {
logger.metric('verification:create', {}, {statsig: true})
ax.metric('verification:create', {})
await updateProfileVerificationCache({profile})
},
})
@@ -6,12 +6,13 @@ import {
import {useMutation} from '@tanstack/react-query'
import {until} from '#/lib/async/until'
import {logger} from '#/logger'
import {useUpdateProfileVerificationCache} from '#/state/queries/verification/useUpdateProfileVerificationCache'
import {useAgent, useSession} from '#/state/session'
import {useAnalytics} from '#/analytics'
import type * as bsky from '#/types/bsky'
export function useVerificationsRemoveMutation() {
const ax = useAnalytics()
const agent = useAgent()
const {currentAccount} = useSession()
const updateProfileVerificationCache = useUpdateProfileVerificationCache()
@@ -56,7 +57,7 @@ export function useVerificationsRemoveMutation() {
)
},
async onSuccess(_, {profile}) {
logger.metric('verification:revoke', {}, {statsig: true})
ax.metric('verification:revoke', {})
await updateProfileVerificationCache({profile})
},
})