[Statsig] Track login/logout (#3286)

* [Statsig] Track login/logout

* Fix missing attribution
zio/stable
dan 2024-03-20 03:24:05 +00:00 committed by GitHub
parent 2e2fae378a
commit 3d8d1dd173
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 98 additions and 48 deletions

View File

@ -565,7 +565,11 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
}
function getCurrentRouteName() {
return navigationRef.getCurrentRoute()?.name
if (navigationRef.isReady()) {
return navigationRef.getCurrentRoute()?.name
} else {
return undefined
}
}
/**

View File

@ -6,6 +6,7 @@ import {useSessionApi, SessionAccount} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {useCloseAllActiveElements} from '#/state/util'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {LogEvents} from '../statsig/statsig'
export function useAccountSwitcher() {
const {track} = useAnalytics()
@ -14,7 +15,10 @@ export function useAccountSwitcher() {
const {requestSwitchToAccount} = useLoggedOutViewControls()
const onPressSwitchAccount = useCallback(
async (account: SessionAccount) => {
async (
account: SessionAccount,
logContext: LogEvents['account:loggedIn']['logContext'],
) => {
track('Settings:SwitchAccountButtonClicked')
try {
@ -28,7 +32,7 @@ export function useAccountSwitcher() {
// So we change the URL ourselves. The navigator will pick it up on remount.
history.pushState(null, '', '/')
}
await selectAccount(account)
await selectAccount(account, logContext)
setTimeout(() => {
Toast.show(`Signed in as @${account.handle}`)
}, 100)

View File

@ -2,6 +2,13 @@ export type LogEvents = {
init: {
initMs: number
}
'account:loggedIn': {
logContext: 'LoginForm' | 'SwitchAccount' | 'ChooseAccountForm' | 'Settings'
withPassword: boolean
}
'account:loggedOut': {
logContext: 'SwitchAccount' | 'Settings' | 'Deactivated'
}
'notifications:openApp': {}
'state:background': {}
'state:foreground': {}

View File

@ -147,7 +147,7 @@ export function Deactivated() {
variant="ghost"
size="large"
label={_(msg`Log out`)}
onPress={logout}>
onPress={() => logout('Deactivated')}>
<ButtonText style={[{color: t.palette.primary_500}]}>
<Trans>Log out</Trans>
</ButtonText>
@ -176,7 +176,7 @@ export function Deactivated() {
variant="ghost"
size="large"
label={_(msg`Log out`)}
onPress={logout}>
onPress={() => logout('Deactivated')}>
<ButtonText style={[{color: t.palette.primary_500}]}>
<Trans>Log out</Trans>
</ButtonText>

View File

@ -20,6 +20,7 @@ import {useCloseAllActiveElements} from '#/state/util'
import {track} from '#/lib/analytics/analytics'
import {hasProp} from '#/lib/type-guards'
import {readLabelers} from './agent-config'
import {logEvent, LogEvents} from '#/lib/statsig/statsig'
let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT
@ -54,17 +55,22 @@ export type ApiContext = {
verificationPhone?: string
verificationCode?: string
}) => Promise<void>
login: (props: {
service: string
identifier: string
password: string
}) => Promise<void>
login: (
props: {
service: string
identifier: string
password: string
},
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
/**
* A full logout. Clears the `currentAccount` from session, AND removes
* access tokens from all accounts, so that returning as any user will
* require a full login.
*/
logout: () => Promise<void>
logout: (
logContext: LogEvents['account:loggedOut']['logContext'],
) => Promise<void>
/**
* A partial logout. Clears the `currentAccount` from session, but DOES NOT
* clear access tokens from accounts, allowing the user to return to their
@ -76,7 +82,10 @@ export type ApiContext = {
initSession: (account: SessionAccount) => Promise<void>
resumeSession: (account?: SessionAccount) => Promise<void>
removeAccount: (account: SessionAccount) => void
selectAccount: (account: SessionAccount) => Promise<void>
selectAccount: (
account: SessionAccount,
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
updateCurrentAccount: (
account: Partial<
Pick<SessionAccount, 'handle' | 'email' | 'emailConfirmed'>
@ -286,7 +295,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
)
const login = React.useCallback<ApiContext['login']>(
async ({service, identifier, password}) => {
async ({service, identifier, password}, logContext) => {
logger.debug(`session: login`, {}, logger.DebugContext.session)
const agent = new BskyAgent({service})
@ -329,24 +338,29 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
logger.debug(`session: logged in`, {}, logger.DebugContext.session)
track('Sign In', {resumedSession: false})
logEvent('account:loggedIn', {logContext, withPassword: true})
},
[upsertAccount, queryClient, clearCurrentAccount],
)
const logout = React.useCallback<ApiContext['logout']>(async () => {
logger.debug(`session: logout`)
clearCurrentAccount()
setStateAndPersist(s => {
return {
...s,
accounts: s.accounts.map(a => ({
...a,
refreshJwt: undefined,
accessJwt: undefined,
})),
}
})
}, [clearCurrentAccount, setStateAndPersist])
const logout = React.useCallback<ApiContext['logout']>(
async logContext => {
logger.debug(`session: logout`)
clearCurrentAccount()
setStateAndPersist(s => {
return {
...s,
accounts: s.accounts.map(a => ({
...a,
refreshJwt: undefined,
accessJwt: undefined,
})),
}
})
logEvent('account:loggedOut', {logContext})
},
[clearCurrentAccount, setStateAndPersist],
)
const initSession = React.useCallback<ApiContext['initSession']>(
async account => {
@ -540,11 +554,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
)
const selectAccount = React.useCallback<ApiContext['selectAccount']>(
async account => {
async (account, logContext) => {
setState(s => ({...s, isSwitchingAccounts: true}))
try {
await initSession(account)
setState(s => ({...s, isSwitchingAccounts: false}))
logEvent('account:loggedIn', {logContext, withPassword: false})
} catch (e) {
// reset this in case of error
setState(s => ({...s, isSwitchingAccounts: false}))

View File

@ -16,6 +16,7 @@ import {useSession, useSessionApi, SessionAccount} from '#/state/session'
import {useProfileQuery} from '#/state/queries/profile'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import * as Toast from '#/view/com/util/Toast'
import {logEvent} from '#/lib/statsig/statsig'
function AccountItem({
account,
@ -102,6 +103,10 @@ export const ChooseAccountForm = ({
Toast.show(_(msg`Already signed in as @${account.handle}`))
} else {
await initSession(account)
logEvent('account:loggedIn', {
logContext: 'ChooseAccountForm',
withPassword: false,
})
track('Sign In', {resumedSession: true})
setTimeout(() => {
Toast.show(_(msg`Signed in as @${account.handle}`))

View File

@ -98,11 +98,14 @@ export const LoginForm = ({
}
// TODO remove double login
await login({
service: serviceUrl,
identifier: fullIdent,
password,
})
await login(
{
service: serviceUrl,
identifier: fullIdent,
password,
},
'LoginForm',
)
} catch (e: any) {
const errMsg = e.toString()
setIsProcessing(false)

View File

@ -39,7 +39,7 @@ function SwitchAccountCard({account}: {account: SessionAccount}) {
track('Settings:SignOutButtonClicked')
closeAllActiveElements()
// needs to be in timeout or the modal re-opens
setTimeout(() => logout(), 0)
setTimeout(() => logout('SwitchAccount'), 0)
}, [track, logout, closeAllActiveElements])
const contents = (
@ -95,7 +95,9 @@ function SwitchAccountCard({account}: {account: SessionAccount}) {
key={account.did}
style={[isSwitchingAccounts && styles.dimmed]}
onPress={
isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account)
isSwitchingAccounts
? undefined
: () => onPressSwitchAccount(account, 'SwitchAccount')
}
accessibilityRole="button"
accessibilityLabel={_(msg`Switch to ${account.handle}`)}

View File

@ -22,18 +22,24 @@ export function TestCtrls() {
const {mutate: setFeedViewPref} = useSetFeedViewPreferencesMutation()
const {setShowLoggedOut} = useLoggedOutViewControls()
const onPressSignInAlice = async () => {
await login({
service: 'http://localhost:3000',
identifier: 'alice.test',
password: 'hunter2',
})
await login(
{
service: 'http://localhost:3000',
identifier: 'alice.test',
password: 'hunter2',
},
'LoginForm',
)
}
const onPressSignInBob = async () => {
await login({
service: 'http://localhost:3000',
identifier: 'bob.test',
password: 'hunter2',
})
await login(
{
service: 'http://localhost:3000',
identifier: 'bob.test',
password: 'hunter2',
},
'LoginForm',
)
}
return (
<View style={{position: 'absolute', top: 100, right: 0, zIndex: 100}}>
@ -51,7 +57,7 @@ export function TestCtrls() {
/>
<Pressable
testID="e2eSignOut"
onPress={() => logout()}
onPress={() => logout('Settings')}
accessibilityRole="button"
style={BTN}
/>

View File

@ -100,7 +100,9 @@ function SettingsAccountCard({account}: {account: SessionAccount}) {
{isCurrentAccount ? (
<TouchableOpacity
testID="signOutBtn"
onPress={logout}
onPress={() => {
logout('Settings')
}}
accessibilityRole="button"
accessibilityLabel={_(msg`Sign out`)}
accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}>
@ -129,7 +131,9 @@ function SettingsAccountCard({account}: {account: SessionAccount}) {
testID={`switchToAccountBtn-${account.handle}`}
key={account.did}
onPress={
isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account)
isSwitchingAccounts
? undefined
: () => onPressSwitchAccount(account, 'Settings')
}
accessibilityRole="button"
accessibilityLabel={_(msg`Switch to ${account.handle}`)}