[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
+38 -40
View File
@@ -11,7 +11,6 @@ import {type AppBskyFeedDefs} from '@atproto/api'
import throttle from 'lodash.throttle'
import {PROD_FEEDS, STAGING_FEEDS} from '#/lib/constants'
import {Logger} from '#/logger'
import {
type FeedSourceFeedInfo,
type FeedSourceInfo,
@@ -22,6 +21,7 @@ import {
type FeedPostSliceItem,
} from '#/state/queries/post-feed'
import {getItemsForFeedback} from '#/view/com/posts/PostFeed'
import {useAnalytics} from '#/analytics'
import {useAgent} from './session'
export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS]
@@ -42,8 +42,6 @@ export const THIRD_PARTY_ALLOWED_INTERACTIONS = new Set<
'app.bsky.feed.defs#interactionSeen',
])
const logger = Logger.create(Logger.Context.FeedFeedback)
export type StateContext = {
enabled: boolean
onItemSeen: (item: any) => void
@@ -65,6 +63,8 @@ export function useFeedFeedback(
feedSourceInfo: FeedSourceInfo | undefined,
hasSession: boolean,
) {
const ax = useAnalytics()
const logger = ax.logger.useChild(ax.logger.Context.FeedFeedback)
const agent = useAgent()
const feed =
@@ -85,12 +85,45 @@ export function useFeedFeedback(
WeakSet<FeedPostSliceItem | AppBskyFeedDefs.Interaction>
>(new WeakSet())
const flushEvents = useCallback(
(stats: AggregatedStats | null, feedDescriptor: string) => {
if (stats === null) {
return
}
if (stats.clickthroughCount > 0) {
ax.metric('feed:clickthrough', {
count: stats.clickthroughCount,
feed: feedDescriptor,
})
stats.clickthroughCount = 0
}
if (stats.engagedCount > 0) {
ax.metric('feed:engaged', {
count: stats.engagedCount,
feed: feedDescriptor,
})
stats.engagedCount = 0
}
if (stats.seenCount > 0) {
ax.metric('feed:seen', {
count: stats.seenCount,
feed: feedDescriptor,
})
stats.seenCount = 0
}
},
[ax],
)
const aggregatedStats = useRef<AggregatedStats | null>(null)
const throttledFlushAggregatedStats = useMemo(
() =>
throttle(
() =>
flushToStatsig(
flushEvents(
aggregatedStats.current,
feed?.feedDescriptor ?? 'unknown',
),
@@ -100,7 +133,7 @@ export function useFeedFeedback(
trailing: true,
},
),
[feed?.feedDescriptor],
[feed?.feedDescriptor, flushEvents],
)
const sendToFeedNoDelay = useCallback(() => {
@@ -130,7 +163,6 @@ export function useFeedFeedback(
)
.catch(() => {}) // ignore upstream errors
// Send to Statsig
if (aggregatedStats.current === null) {
aggregatedStats.current = createAggregatedStats()
}
@@ -299,37 +331,3 @@ function sendOrAggregateInteractionsForStats(
}
}
}
function flushToStatsig(stats: AggregatedStats | null, feedDescriptor: string) {
if (stats === null) {
return
}
if (stats.clickthroughCount > 0) {
logger.metric('feed:clickthrough', {
count: stats.clickthroughCount,
feed: feedDescriptor,
})
stats.clickthroughCount = 0
}
if (stats.engagedCount > 0) {
logger.metric('feed:engaged', {
count: stats.engagedCount,
feed: feedDescriptor,
})
stats.engagedCount = 0
}
if (stats.seenCount > 0) {
logger.metric(
'feed:seen',
{
count: stats.seenCount,
feed: feedDescriptor,
},
{statsig: false},
)
stats.seenCount = 0
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ import {type ChatBskyConvoDefs} from '@atproto/api'
import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {useAppState} from '#/lib/hooks/useAppState'
import {useAppState} from '#/lib/appState'
import {Convo} from '#/state/messages/convo/agent'
import {
type ConvoParams,
+12 -1
View File
@@ -2,6 +2,7 @@ import React from 'react'
import {type AppLanguage} from '#/locale/languages'
import * as persisted from '#/state/persisted'
import {AnalyticsContext, utils} from '#/analytics'
type SetStateCb = (
s: persisted.Schema['languagePrefs'],
@@ -124,7 +125,17 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
return (
<stateContext.Provider value={state}>
<apiContext.Provider value={api}>{children}</apiContext.Provider>
<apiContext.Provider value={api}>
<AnalyticsContext
metadata={utils.useMeta({
preferences: {
appLanguage: state.appLanguage,
contentLanguages: state.contentLanguages,
},
})}>
{children}
</AnalyticsContext>
</apiContext.Provider>
</stateContext.Provider>
)
}
+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})
},
})
+3 -7
View File
@@ -1,9 +1,9 @@
import {createContext, useContext, useMemo} from 'react'
import {useGate} from '#/lib/statsig/statsig'
import {useLanguagePrefs} from '#/state/preferences/languages'
import {useServiceConfigQuery} from '#/state/queries/service-config'
import {useSession} from '#/state/session'
import {useAnalytics} from '#/analytics'
import {IS_DEV} from '#/env'
import {device} from '#/storage'
@@ -52,10 +52,6 @@ export function Provider({children}: {children: React.ReactNode}) {
return {enabled: Boolean(cachedEnabled)}
}
/*
* Doing an extra check here to reduce hits to statsig. If it's disabled on
* the server, we can exit early.
*/
const enabled = Boolean(config?.topicsEnabled)
// update cache
@@ -107,10 +103,10 @@ export function useLiveNowConfig(): LiveNowConfig {
}
export function useCanGoLive() {
const gate = useGate()
const ax = useAnalytics()
const {hasSession} = useSession()
if (!hasSession) return false
return IS_DEV ? true : !gate('disable_live_now_beta')
return IS_DEV ? true : !ax.features.enabled(ax.features.DisableLiveNowBeta)
}
export function useCheckEmailConfirmed() {
+8 -6
View File
@@ -22,7 +22,6 @@ import {
PUBLIC_BSKY_SERVICE,
TIMELINE_SAVED_FEED,
} from '#/lib/constants'
import {tryFetchGates} from '#/lib/statsig/statsig'
import {getAge} from '#/lib/strings/time'
import {logger} from '#/logger'
import {snoozeBirthdateUpdateAllowedForDid} from '#/state/birthdate'
@@ -32,6 +31,7 @@ import {
setBirthdateForDid,
setCreatedAtForDid,
} from '#/ageAssurance/data'
import {features} from '#/analytics'
import {emitNetworkConfirmed, emitNetworkLost} from '../events'
import {addSessionErrorLog} from './logging'
import {
@@ -63,7 +63,9 @@ export async function createAgentAndResume(
if (storedAccount.pdsUrl) {
agent.sessionManager.pdsUrl = new URL(storedAccount.pdsUrl)
}
const gates = tryFetchGates(storedAccount.did, 'prefer-low-latency')
const gates = features.refresh({
strategy: 'prefer-low-latency',
})
const moderation = configureModerationForAccount(agent, storedAccount)
const prevSession: AtpSessionData = sessionAccountToSession(storedAccount)
if (isSessionExpired(storedAccount)) {
@@ -123,7 +125,7 @@ export async function createAgentAndLogin(
})
const account = agentToSessionAccountOrThrow(agent)
const gates = tryFetchGates(account.did, 'prefer-fresh-gates')
const gates = features.refresh({strategy: 'prefer-fresh-gates'})
const moderation = configureModerationForAccount(agent, account)
const aa = prefetchAgeAssuranceData({agent})
@@ -171,7 +173,7 @@ export async function createAgentAndCreateAccount(
verificationCode,
})
const account = agentToSessionAccountOrThrow(agent)
const gates = tryFetchGates(account.did, 'prefer-fresh-gates')
const gates = features.refresh({strategy: 'prefer-fresh-gates'})
const moderation = configureModerationForAccount(agent, account)
const createdAt = new Date().toISOString()
@@ -322,7 +324,7 @@ export function agentToSessionAccount(
return undefined
}
return {
service: agent.service.toString(),
service: agent.serviceUrl.toString(),
did: agent.session.did,
handle: agent.session.handle,
email: agent.session.email,
@@ -332,7 +334,7 @@ export function agentToSessionAccount(
accessJwt: agent.session.accessJwt,
signupQueued: isSignupQueued(agent.session.accessJwt),
active: agent.session.active,
status: agent.session.status as SessionAccount['status'],
status: agent.session.status,
pdsUrl: agent.pdsUrl?.toString(),
isSelfHosted: !agent.serviceUrl.toString().startsWith(BSKY_SERVICE),
}
+38 -14
View File
@@ -4,6 +4,7 @@ import {type AtpSessionEvent, type BskyAgent} from '@atproto/api'
import * as persisted from '#/state/persisted'
import {useCloseAllActiveElements} from '#/state/util'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {AnalyticsContext, useAnalyticsBase, utils} from '#/analytics'
import {IS_WEB} from '#/env'
import {emitSessionDropped} from '../events'
import {
@@ -15,11 +16,9 @@ import {
sessionAccountToSession,
} from './agent'
import {type Action, getInitialState, reducer, type State} from './reducer'
export {isSignupQueued} from './util'
import {addSessionDebugLog} from './logging'
export type {SessionAccount} from '#/state/session/types'
import {logger} from '#/logger'
import {
type SessionApiContext,
type SessionStateContext,
@@ -93,6 +92,7 @@ class SessionStore {
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const ax = useAnalyticsBase()
const cancelPendingTask = useOneTaskAtATime()
const [store] = React.useState(() => new SessionStore())
const state = React.useSyncExternalStore(store.subscribe, store.getState)
@@ -119,7 +119,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
async (params, metrics) => {
addSessionDebugLog({type: 'method:start', method: 'createAccount'})
const signal = cancelPendingTask()
logger.metric('account:create:begin', {}, {statsig: true})
ax.metric('account:create:begin', {})
const {agent, account} = await createAgentAndCreateAccount(
params,
onAgentSessionChange,
@@ -133,10 +133,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
newAgent: agent,
newAccount: account,
})
logger.metric('account:create:success', metrics, {statsig: true})
ax.metric('account:create:success', metrics, {
session: utils.accountToSessionMetadata(account),
})
addSessionDebugLog({type: 'method:end', method: 'createAccount', account})
},
[store, onAgentSessionChange, cancelPendingTask],
[ax, store, onAgentSessionChange, cancelPendingTask],
)
const login = React.useCallback<SessionApiContext['login']>(
@@ -156,14 +158,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
newAgent: agent,
newAccount: account,
})
logger.metric(
ax.metric(
'account:loggedIn',
{logContext, withPassword: true},
{statsig: true},
{session: utils.accountToSessionMetadata(account)},
)
addSessionDebugLog({type: 'method:end', method: 'login', account})
},
[store, onAgentSessionChange, cancelPendingTask],
[ax, store, onAgentSessionChange, cancelPendingTask],
)
const logoutCurrentAccount = React.useCallback<
@@ -176,10 +178,16 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
store.dispatch({
type: 'logged-out-current-account',
})
logger.metric(
ax.metric(
'account:loggedOut',
{logContext, scope: 'current'},
{statsig: true},
{
session: utils.accountToSessionMetadata(
prevState.accounts.find(
a => a.did === prevState.currentAgentState.did,
),
),
},
)
addSessionDebugLog({type: 'method:end', method: 'logout'})
if (prevState.currentAgentState.did) {
@@ -188,7 +196,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
// reset onboarding flow on logout
onboardingDispatch({type: 'skip'})
},
[store, cancelPendingTask, onboardingDispatch],
[ax, store, cancelPendingTask, onboardingDispatch],
)
const logoutEveryAccount = React.useCallback<
@@ -197,13 +205,20 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
logContext => {
addSessionDebugLog({type: 'method:start', method: 'logout'})
cancelPendingTask()
const prevState = store.getState()
store.dispatch({
type: 'logged-out-every-account',
})
logger.metric(
ax.metric(
'account:loggedOut',
{logContext, scope: 'every'},
{statsig: true},
{
session: utils.accountToSessionMetadata(
prevState.accounts.find(
a => a.did === prevState.currentAgentState.did,
),
),
},
)
addSessionDebugLog({type: 'method:end', method: 'logout'})
clearAgeAssuranceData()
@@ -359,7 +374,16 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
return (
<AgentContext.Provider value={agent}>
<StateContext.Provider value={stateContext}>
<ApiContext.Provider value={api}>{children}</ApiContext.Provider>
<ApiContext.Provider value={api}>
<AnalyticsContext
metadata={utils.useMeta({
session: utils.accountToSessionMetadata(
stateContext.currentAccount,
),
})}>
{children}
</AnalyticsContext>
</ApiContext.Provider>
</StateContext.Provider>
</AgentContext.Provider>
)
+5 -5
View File
@@ -1,5 +1,5 @@
import {type LogEvents} from '#/lib/statsig/statsig'
import {type PersistedAccount} from '#/state/persisted'
import {type Metrics} from '#/analytics/metrics'
export type SessionAccount = PersistedAccount
@@ -21,7 +21,7 @@ export type SessionApiContext = {
verificationPhone?: string
verificationCode?: string
},
metrics: LogEvents['account:create:success'],
metrics: Metrics['account:create:success'],
) => Promise<void>
login: (
props: {
@@ -30,13 +30,13 @@ export type SessionApiContext = {
password: string
authFactorToken?: string | undefined
},
logContext: LogEvents['account:loggedIn']['logContext'],
logContext: Metrics['account:loggedIn']['logContext'],
) => Promise<void>
logoutCurrentAccount: (
logContext: LogEvents['account:loggedOut']['logContext'],
logContext: Metrics['account:loggedOut']['logContext'],
) => void
logoutEveryAccount: (
logContext: LogEvents['account:loggedOut']['logContext'],
logContext: Metrics['account:loggedOut']['logContext'],
) => void
resumeSession: (
account: SessionAccount,
+4 -3
View File
@@ -2,11 +2,11 @@ import React, {useMemo} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logEvent} from '#/lib/statsig/statsig'
import {
ProgressGuideToast,
type ProgressGuideToastRef,
} from '#/components/ProgressGuide/Toast'
import {useAnalytics} from '#/analytics'
import {
usePreferencesQuery,
useSetActiveProgressGuideMutation,
@@ -71,6 +71,7 @@ export function useProgressGuideControls() {
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const ax = useAnalytics()
const {_} = useLingui()
const {data: preferences} = usePreferencesQuery()
const {mutateAsync, variables, isPending} =
@@ -140,7 +141,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
endProgressGuide() {
setLocalGuideState(undefined)
mutateAsync(undefined)
logEvent('progressGuide:hide', {})
ax.metric('progressGuide:hide', {})
},
captureAction(action: ProgressGuideAction, count = 1) {
@@ -202,7 +203,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
mutateAsync(guide?.isComplete ? undefined : guide)
},
}
}, [activeProgressGuide, mutateAsync, setLocalGuideState])
}, [ax, activeProgressGuide, mutateAsync, setLocalGuideState])
return (
<ProgressGuideContext.Provider value={localGuideState}>