From 5bec5877172ac0f18c3c1bebec7ad6c46ca0ae39 Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 19 Mar 2024 20:11:26 +0000 Subject: [Statsig] Include OS and track app state changes (#3273) * Include platform in identify * Track back/foregrounding --- src/lib/statsig/events.ts | 2 ++ src/lib/statsig/statsig.tsx | 20 +++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) (limited to 'src/lib/statsig') diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index fa7e597fb..cb9f3fe92 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -2,6 +2,8 @@ export type LogEvents = { init: { initMs: number } + 'state:background': {} + 'state:foreground': {} 'feed:endReached': { feedType: string itemCount: number diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx index 5745d204a..3abec5c4f 100644 --- a/src/lib/statsig/statsig.tsx +++ b/src/lib/statsig/statsig.tsx @@ -1,9 +1,11 @@ import React from 'react' +import {Platform} from 'react-native' import { Statsig, StatsigProvider, useGate as useStatsigGate, } from 'statsig-react-native-expo' +import {AppState, AppStateStatus} from 'react-native' import {useSession} from '../../state/session' import {sha256} from 'js-sha256' import {LogEvents} from './events' @@ -58,9 +60,25 @@ function toStatsigUser(did: string | undefined) { if (did) { userID = sha256(did) } - return {userID} + return { + userID, + platform: Platform.OS, + } } +let lastState: AppStateStatus = AppState.currentState +AppState.addEventListener('change', (state: AppStateStatus) => { + if (state === lastState) { + return + } + lastState = state + if (state === 'active') { + logEvent('state:foreground', {}) + } else { + logEvent('state:background', {}) + } +}) + export function Provider({children}: {children: React.ReactNode}) { const {currentAccount} = useSession() const currentStatsigUser = React.useMemo( -- cgit 1.4.1 From ebf8644df9d677a57e565f1c8b2983e33dab5749 Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 19 Mar 2024 20:19:11 +0000 Subject: Track notification open (#3274) --- src/lib/notifications/notifications.ts | 2 ++ src/lib/statsig/events.ts | 1 + 2 files changed, 3 insertions(+) (limited to 'src/lib/statsig') diff --git a/src/lib/notifications/notifications.ts b/src/lib/notifications/notifications.ts index 62d0bfc4b..e811f690e 100644 --- a/src/lib/notifications/notifications.ts +++ b/src/lib/notifications/notifications.ts @@ -7,6 +7,7 @@ import {logger} from '#/logger' import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed' import {truncateAndInvalidate} from '#/state/queries/util' import {SessionAccount, getAgent} from '#/state/session' +import {logEvent} from '../statsig/statsig' const SERVICE_DID = (serviceUrl?: string) => serviceUrl?.includes('staging') @@ -123,6 +124,7 @@ export function init(queryClient: QueryClient) { logger.DebugContext.notifications, ) track('Notificatons:OpenApp') + logEvent('notifications:openApp', {}) truncateAndInvalidate(queryClient, RQKEY_NOTIFS()) resetToTab('NotificationsTab') // open notifications tab } diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index cb9f3fe92..b91a15ecb 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -2,6 +2,7 @@ export type LogEvents = { init: { initMs: number } + 'notifications:openApp': {} 'state:background': {} 'state:foreground': {} 'feed:endReached': { -- cgit 1.4.1 From b6c9d34e452405e8e735599967d6ebfb2abe99e9 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 20 Mar 2024 00:56:31 +0000 Subject: [Statsig] Track feed refresh (#3283) --- src/lib/statsig/events.ts | 4 ++++ src/view/com/feeds/FeedPage.tsx | 9 +++++++++ src/view/com/posts/Feed.tsx | 8 ++++++-- 3 files changed, 19 insertions(+), 2 deletions(-) (limited to 'src/lib/statsig') diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index b91a15ecb..420c58ed2 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -9,6 +9,10 @@ export type LogEvents = { feedType: string itemCount: number } + 'feed:refresh': { + feedType: string + reason: 'pull-to-refresh' | 'soft-reset' | 'load-latest' + } 'post:create': { imageCount: number isReply: boolean diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index e6b5d1fb6..2d0736b09 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -22,6 +22,7 @@ import {listenSoftReset} from '#/state/events' import {truncateAndInvalidate} from '#/state/queries/util' import {TabState, getTabState, getRootNavigation} from '#/lib/routes/helpers' import {isNative} from '#/platform/detection' +import {logEvent} from '#/lib/statsig/statsig' const POLL_FREQ = 60e3 // 60sec @@ -68,6 +69,10 @@ export function FeedPage({ scrollToTop() truncateAndInvalidate(queryClient, FEED_RQKEY(feed)) setHasNew(false) + logEvent('feed:refresh', { + feedType: feed.split('|')[0], + reason: 'soft-reset', + }) } }, [navigation, isPageFocused, scrollToTop, queryClient, feed, setHasNew]) @@ -89,6 +94,10 @@ export function FeedPage({ scrollToTop() truncateAndInvalidate(queryClient, FEED_RQKEY(feed)) setHasNew(false) + logEvent('feed:refresh', { + feedType: feed.split('|')[0], + reason: 'load-latest', + }) }, [scrollToTop, feed, queryClient, setHasNew]) return ( diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index b86646a4d..8afcce94f 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -90,6 +90,7 @@ let Feed = ({ const [isPTRing, setIsPTRing] = React.useState(false) const checkForNewRef = React.useRef<(() => void) | null>(null) const lastFetchRef = React.useRef(Date.now()) + const feedType = feed.split('|')[0] const opts = React.useMemo( () => ({enabled, ignoreFilterFor}), @@ -214,6 +215,10 @@ let Feed = ({ const onRefresh = React.useCallback(async () => { track('Feed:onRefresh') + logEvent('feed:refresh', { + feedType: feedType, + reason: 'pull-to-refresh', + }) setIsPTRing(true) try { await refetch() @@ -222,9 +227,8 @@ let Feed = ({ logger.error('Failed to refresh posts feed', {message: err}) } setIsPTRing(false) - }, [refetch, track, setIsPTRing, onHasNew]) + }, [refetch, track, setIsPTRing, onHasNew, feedType]) - const feedType = feed.split('|')[0] const onEndReached = React.useCallback(async () => { if (isFetching || !hasNextPage || isError) return -- cgit 1.4.1 From 3d8d1dd1737c11d924bec7d51ae4deb6cb6336b0 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 20 Mar 2024 03:24:05 +0000 Subject: [Statsig] Track login/logout (#3286) * [Statsig] Track login/logout * Fix missing attribution --- src/Navigation.tsx | 6 ++- src/lib/hooks/useAccountSwitcher.ts | 8 +++- src/lib/statsig/events.ts | 7 +++ src/screens/Deactivated.tsx | 4 +- src/state/session/index.tsx | 61 +++++++++++++++++---------- src/view/com/auth/login/ChooseAccountForm.tsx | 5 +++ src/view/com/auth/login/LoginForm.tsx | 13 +++--- src/view/com/modals/SwitchAccount.tsx | 6 ++- src/view/com/testing/TestCtrls.e2e.tsx | 28 +++++++----- src/view/screens/Settings/index.tsx | 8 +++- 10 files changed, 98 insertions(+), 48 deletions(-) (limited to 'src/lib/statsig') diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 3d6a15c4e..83aede722 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -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 + } } /** diff --git a/src/lib/hooks/useAccountSwitcher.ts b/src/lib/hooks/useAccountSwitcher.ts index 74b5674d5..eb1685a0a 100644 --- a/src/lib/hooks/useAccountSwitcher.ts +++ b/src/lib/hooks/useAccountSwitcher.ts @@ -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) diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index 420c58ed2..f57c8d416 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -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': {} diff --git a/src/screens/Deactivated.tsx b/src/screens/Deactivated.tsx index f4c201475..7e87973cb 100644 --- a/src/screens/Deactivated.tsx +++ b/src/screens/Deactivated.tsx @@ -147,7 +147,7 @@ export function Deactivated() { variant="ghost" size="large" label={_(msg`Log out`)} - onPress={logout}> + onPress={() => logout('Deactivated')}> Log out @@ -176,7 +176,7 @@ export function Deactivated() { variant="ghost" size="large" label={_(msg`Log out`)} - onPress={logout}> + onPress={() => logout('Deactivated')}> Log out diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 6b1474839..b6748bfad 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -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 - login: (props: { - service: string - identifier: string - password: string - }) => Promise + login: ( + props: { + service: string + identifier: string + password: string + }, + logContext: LogEvents['account:loggedIn']['logContext'], + ) => Promise /** * 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 + logout: ( + logContext: LogEvents['account:loggedOut']['logContext'], + ) => Promise /** * 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 resumeSession: (account?: SessionAccount) => Promise removeAccount: (account: SessionAccount) => void - selectAccount: (account: SessionAccount) => Promise + selectAccount: ( + account: SessionAccount, + logContext: LogEvents['account:loggedIn']['logContext'], + ) => Promise updateCurrentAccount: ( account: Partial< Pick @@ -286,7 +295,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) const login = React.useCallback( - 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(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( + 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( async account => { @@ -540,11 +554,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) const selectAccount = React.useCallback( - 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})) diff --git a/src/view/com/auth/login/ChooseAccountForm.tsx b/src/view/com/auth/login/ChooseAccountForm.tsx index d3b075fdb..e754c8483 100644 --- a/src/view/com/auth/login/ChooseAccountForm.tsx +++ b/src/view/com/auth/login/ChooseAccountForm.tsx @@ -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}`)) diff --git a/src/view/com/auth/login/LoginForm.tsx b/src/view/com/auth/login/LoginForm.tsx index 3202d69c5..92f495575 100644 --- a/src/view/com/auth/login/LoginForm.tsx +++ b/src/view/com/auth/login/LoginForm.tsx @@ -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) diff --git a/src/view/com/modals/SwitchAccount.tsx b/src/view/com/modals/SwitchAccount.tsx index 0658805bd..892b07c9a 100644 --- a/src/view/com/modals/SwitchAccount.tsx +++ b/src/view/com/modals/SwitchAccount.tsx @@ -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}`)} diff --git a/src/view/com/testing/TestCtrls.e2e.tsx b/src/view/com/testing/TestCtrls.e2e.tsx index e1e899488..1eb99c4f5 100644 --- a/src/view/com/testing/TestCtrls.e2e.tsx +++ b/src/view/com/testing/TestCtrls.e2e.tsx @@ -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 ( @@ -51,7 +57,7 @@ export function TestCtrls() { /> logout()} + onPress={() => logout('Settings')} accessibilityRole="button" style={BTN} /> diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx index 7e808f910..3967678b4 100644 --- a/src/view/screens/Settings/index.tsx +++ b/src/view/screens/Settings/index.tsx @@ -100,7 +100,9 @@ function SettingsAccountCard({account}: {account: SessionAccount}) { {isCurrentAccount ? ( { + 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}`)} -- cgit 1.4.1 From 20337ceef1bf86d5190bc240cf11ecf9bfa88042 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 20 Mar 2024 03:25:37 +0000 Subject: [Statsig] Track active time (#3289) --- src/lib/statsig/events.ts | 4 +++- src/lib/statsig/statsig.tsx | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) (limited to 'src/lib/statsig') diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index f57c8d416..b83095976 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -10,7 +10,9 @@ export type LogEvents = { logContext: 'SwitchAccount' | 'Settings' | 'Deactivated' } 'notifications:openApp': {} - 'state:background': {} + 'state:background': { + secondsActive: number + } 'state:foreground': {} 'feed:endReached': { feedType: string diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx index 3abec5c4f..9fa6cce2d 100644 --- a/src/lib/statsig/statsig.tsx +++ b/src/lib/statsig/statsig.tsx @@ -67,15 +67,24 @@ function toStatsigUser(did: string | undefined) { } let lastState: AppStateStatus = AppState.currentState +let lastActive = lastState === 'active' ? performance.now() : null AppState.addEventListener('change', (state: AppStateStatus) => { if (state === lastState) { return } lastState = state if (state === 'active') { + lastActive = performance.now() logEvent('state:foreground', {}) } else { - logEvent('state:background', {}) + let secondsActive = 0 + if (lastActive != null) { + secondsActive = Math.round((performance.now() - lastActive) / 1e3) + } + lastActive = null + logEvent('state:background', { + secondsActive, + }) } }) -- cgit 1.4.1