about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2025-03-27 20:17:07 +0200
committerGitHub <noreply@github.com>2025-03-27 20:17:07 +0200
commit5ceaee57938892157491ae2941d05f90c1d74149 (patch)
treed5c2df5937570fd4f3393ecf431e37c6675d80c7 /src
parent7d1ebf6a027085ddc10a7dad2075d5e52d314233 (diff)
downloadvoidsky-5ceaee57938892157491ae2941d05f90c1d74149.tar.zst
Instrument signup (#8037)
Diffstat (limited to 'src')
-rw-r--r--src/logger/metrics.ts17
-rw-r--r--src/screens/Signup/StepCaptcha/index.tsx5
-rw-r--r--src/screens/Signup/StepHandle.tsx26
-rw-r--r--src/screens/Signup/StepInfo/index.tsx15
-rw-r--r--src/screens/Signup/index.tsx28
-rw-r--r--src/screens/Signup/state.ts90
-rw-r--r--src/state/session/index.tsx35
-rw-r--r--src/state/session/types.ts23
8 files changed, 182 insertions, 57 deletions
diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts
index abb7b670f..33cdc25e5 100644
--- a/src/logger/metrics.ts
+++ b/src/logger/metrics.ts
@@ -51,6 +51,17 @@ export type MetricEvents = {
   }
   'signup:captchaSuccess': {}
   'signup:captchaFailure': {}
+  'signup:fieldError': {
+    field: string
+    errorCount: number
+    errorMessage: string
+    activeStep: number
+  }
+  'signup:backgrounded': {
+    activeStep: number
+    backgroundCount: number
+  }
+  'signup:handleTaken': {}
   'signin:hostingProviderPressed': {
     hostingProviderDidChange: boolean
   }
@@ -135,7 +146,11 @@ export type MetricEvents = {
 
   // Data events
   'account:create:begin': {}
-  'account:create:success': {}
+  'account:create:success': {
+    signupDuration: number
+    fieldErrorsTotal: number
+    backgroundCount: number
+  }
   'post:create': {
     imageCount: number
     isReply: boolean
diff --git a/src/screens/Signup/StepCaptcha/index.tsx b/src/screens/Signup/StepCaptcha/index.tsx
index 85558c5f5..388deecaf 100644
--- a/src/screens/Signup/StepCaptcha/index.tsx
+++ b/src/screens/Signup/StepCaptcha/index.tsx
@@ -4,7 +4,6 @@ import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {nanoid} from 'nanoid/non-secure'
 
-import {logEvent} from '#/lib/statsig/statsig'
 import {createFullHandle} from '#/lib/strings/handles'
 import {logger} from '#/logger'
 import {ScreenTransition} from '#/screens/Login/ScreenTransition'
@@ -40,7 +39,7 @@ export function StepCaptcha() {
   const onSuccess = React.useCallback(
     (code: string) => {
       setCompleted(true)
-      logEvent('signup:captchaSuccess', {})
+      logger.metric('signup:captchaSuccess', {}, {statsig: true})
       dispatch({
         type: 'submit',
         task: {verificationCode: code, mutableProcessed: false},
@@ -55,7 +54,7 @@ export function StepCaptcha() {
         type: 'setError',
         value: _(msg`Error receiving captcha response.`),
       })
-      logEvent('signup:captchaFailure', {})
+      logger.metric('signup:captchaFailure', {}, {statsig: true})
       logger.error('Signup Flow Error', {
         registrationHandle: state.handle,
         error,
diff --git a/src/screens/Signup/StepHandle.tsx b/src/screens/Signup/StepHandle.tsx
index 9192ba5a2..03fa28772 100644
--- a/src/screens/Signup/StepHandle.tsx
+++ b/src/screens/Signup/StepHandle.tsx
@@ -3,12 +3,12 @@ import {View} from 'react-native'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {logEvent} from '#/lib/statsig/statsig'
 import {
   createFullHandle,
   MAX_SERVICE_HANDLE_LENGTH,
   validateServiceHandle,
 } from '#/lib/strings/handles'
+import {logger} from '#/logger'
 import {useAgent} from '#/state/session'
 import {ScreenTransition} from '#/screens/Login/ScreenTransition'
 import {useSignupContext} from '#/screens/Signup/state'
@@ -53,7 +53,9 @@ export function StepHandle() {
         dispatch({
           type: 'setError',
           value: _(msg`That handle is already taken.`),
+          field: 'handle',
         })
+        logger.metric('signup:handleTaken', {})
         return
       }
     } catch (e) {
@@ -62,11 +64,15 @@ export function StepHandle() {
       dispatch({type: 'setIsLoading', value: false})
     }
 
-    logEvent('signup:nextPressed', {
-      activeStep: state.activeStep,
-      phoneVerificationRequired:
-        state.serviceDescription?.phoneVerificationRequired,
-    })
+    logger.metric(
+      'signup:nextPressed',
+      {
+        activeStep: state.activeStep,
+        phoneVerificationRequired:
+          state.serviceDescription?.phoneVerificationRequired,
+      },
+      {statsig: true},
+    )
     // phoneVerificationRequired is actually whether a captcha is required
     if (!state.serviceDescription?.phoneVerificationRequired) {
       dispatch({
@@ -92,9 +98,11 @@ export function StepHandle() {
       value: handle,
     })
     dispatch({type: 'prev'})
-    logEvent('signup:backPressed', {
-      activeStep: state.activeStep,
-    })
+    logger.metric(
+      'signup:backPressed',
+      {activeStep: state.activeStep},
+      {statsig: true},
+    )
   }, [dispatch, state.activeStep])
 
   const validCheck = validateServiceHandle(draftValue, state.userDomain)
diff --git a/src/screens/Signup/StepInfo/index.tsx b/src/screens/Signup/StepInfo/index.tsx
index a19a3ad4a..f24cd0e45 100644
--- a/src/screens/Signup/StepInfo/index.tsx
+++ b/src/screens/Signup/StepInfo/index.tsx
@@ -1,11 +1,10 @@
 import React, {useRef} from 'react'
-import {TextInput, View} from 'react-native'
+import {type TextInput, View} from 'react-native'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import * as EmailValidator from 'email-validator'
 import type tldts from 'tldts'
 
-import {logEvent} from '#/lib/statsig/statsig'
 import {isEmailMaybeInvalid} from '#/lib/strings/email'
 import {logger} from '#/logger'
 import {ScreenTransition} from '#/screens/Login/ScreenTransition'
@@ -13,7 +12,7 @@ import {is13, is18, useSignupContext} from '#/screens/Signup/state'
 import {Policies} from '#/screens/Signup/StepInfo/Policies'
 import {atoms as a, native} from '#/alf'
 import * as DateField from '#/components/forms/DateField'
-import {DateFieldRef} from '#/components/forms/DateField/types'
+import {type DateFieldRef} from '#/components/forms/DateField/types'
 import {FormError} from '#/components/forms/FormError'
 import {HostingProvider} from '#/components/forms/HostingProvider'
 import * as TextField from '#/components/forms/TextField'
@@ -134,9 +133,13 @@ export function StepInfo({
     dispatch({type: 'setEmail', value: email})
     dispatch({type: 'setPassword', value: password})
     dispatch({type: 'next'})
-    logEvent('signup:nextPressed', {
-      activeStep: state.activeStep,
-    })
+    logger.metric(
+      'signup:nextPressed',
+      {
+        activeStep: state.activeStep,
+      },
+      {statsig: true},
+    )
   }
 
   return (
diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx
index e82d0da1c..c98040010 100644
--- a/src/screens/Signup/index.tsx
+++ b/src/screens/Signup/index.tsx
@@ -1,5 +1,5 @@
-import React from 'react'
-import {View} from 'react-native'
+import {useEffect, useReducer, useState} from 'react'
+import {AppState, type AppStateStatus, View} from 'react-native'
 import Animated, {FadeIn, LayoutAnimationConfig} from 'react-native-reanimated'
 import {AppBskyGraphStarterpack} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
@@ -31,7 +31,7 @@ import * as bsky from '#/types/bsky'
 export function Signup({onPressBack}: {onPressBack: () => void}) {
   const {_} = useLingui()
   const t = useTheme()
-  const [state, dispatch] = React.useReducer(reducer, initialState)
+  const [state, dispatch] = useReducer(reducer, initialState)
   const {gtMobile} = useBreakpoints()
   const submit = useSubmitSignup()
 
@@ -44,7 +44,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
     uri: activeStarterPack?.uri,
   })
 
-  const [isFetchedAtMount] = React.useState(starterPack != null)
+  const [isFetchedAtMount] = useState(starterPack != null)
   const showStarterPackCard =
     activeStarterPack?.uri && !isFetchingStarterPack && starterPack
 
@@ -55,7 +55,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
     refetch,
   } = useServiceQuery(state.serviceUrl)
 
-  React.useEffect(() => {
+  useEffect(() => {
     if (isFetching) {
       dispatch({type: 'setIsLoading', value: true})
     } else if (!isFetching) {
@@ -63,7 +63,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
     }
   }, [isFetching])
 
-  React.useEffect(() => {
+  useEffect(() => {
     if (isError) {
       dispatch({type: 'setServiceDescription', value: undefined})
       dispatch({
@@ -78,7 +78,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
     }
   }, [_, serviceInfo, isError])
 
-  React.useEffect(() => {
+  useEffect(() => {
     if (state.pendingSubmit) {
       if (!state.pendingSubmit.mutableProcessed) {
         state.pendingSubmit.mutableProcessed = true
@@ -87,6 +87,20 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
     }
   }, [state, dispatch, submit])
 
+  // Track app backgrounding during signup
+  useEffect(() => {
+    const subscription = AppState.addEventListener(
+      'change',
+      (nextAppState: AppStateStatus) => {
+        if (nextAppState === 'background') {
+          dispatch({type: 'incrementBackgroundCount'})
+        }
+      },
+    )
+
+    return () => subscription.remove()
+  }, [])
+
   return (
     <SignupContext.Provider value={{state, dispatch}}>
       <LoggedOutLayout
diff --git a/src/screens/Signup/state.ts b/src/screens/Signup/state.ts
index 3daf36a9b..48ea4ccd9 100644
--- a/src/screens/Signup/state.ts
+++ b/src/screens/Signup/state.ts
@@ -2,7 +2,7 @@ import React, {useCallback} from 'react'
 import {LayoutAnimation} from 'react-native'
 import {
   ComAtprotoServerCreateAccount,
-  ComAtprotoServerDescribeServer,
+  type ComAtprotoServerDescribeServer,
 } from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
@@ -56,6 +56,11 @@ export type SignupState = {
   isLoading: boolean
 
   pendingSubmit: null | SubmitTask
+
+  // Tracking
+  signupStartTime: number
+  fieldErrors: Record<ErrorField, number>
+  backgroundCount: number
 }
 
 export type SignupAction =
@@ -74,6 +79,7 @@ export type SignupAction =
   | {type: 'clearError'}
   | {type: 'setIsLoading'; value: boolean}
   | {type: 'submit'; task: SubmitTask}
+  | {type: 'incrementBackgroundCount'}
 
 export const initialState: SignupState = {
   hasPrev: false,
@@ -93,6 +99,17 @@ export const initialState: SignupState = {
   isLoading: false,
 
   pendingSubmit: null,
+
+  // Tracking
+  signupStartTime: Date.now(),
+  fieldErrors: {
+    'invite-code': 0,
+    email: 0,
+    handle: 0,
+    password: 0,
+    'date-of-birth': 0,
+  },
+  backgroundCount: 0,
 }
 
 export function is13(date: Date) {
@@ -169,6 +186,23 @@ export function reducer(s: SignupState, a: SignupAction): SignupState {
     case 'setError': {
       next.error = a.value
       next.errorField = a.field
+
+      // Track field errors
+      if (a.field) {
+        next.fieldErrors[a.field] = (next.fieldErrors[a.field] || 0) + 1
+
+        // Log the field error
+        logger.metric(
+          'signup:fieldError',
+          {
+            field: a.field,
+            errorCount: next.fieldErrors[a.field],
+            errorMessage: a.value,
+            activeStep: next.activeStep,
+          },
+          {statsig: true},
+        )
+      }
       break
     }
     case 'clearError': {
@@ -180,6 +214,20 @@ export function reducer(s: SignupState, a: SignupAction): SignupState {
       next.pendingSubmit = a.task
       break
     }
+    case 'incrementBackgroundCount': {
+      next.backgroundCount = s.backgroundCount + 1
+
+      // Log background/foreground event during signup
+      logger.metric(
+        'signup:backgrounded',
+        {
+          activeStep: next.activeStep,
+          backgroundCount: next.backgroundCount,
+        },
+        {statsig: true},
+      )
+      break
+    }
   }
 
   next.hasPrev = next.activeStep !== SignupStep.INFO
@@ -212,6 +260,7 @@ export function useSubmitSignup() {
         return dispatch({
           type: 'setError',
           value: _(msg`Please enter your email.`),
+          field: 'email',
         })
       }
       if (!EmailValidator.validate(state.email)) {
@@ -219,6 +268,7 @@ export function useSubmitSignup() {
         return dispatch({
           type: 'setError',
           value: _(msg`Your email appears to be invalid.`),
+          field: 'email',
         })
       }
       if (!state.password) {
@@ -226,6 +276,7 @@ export function useSubmitSignup() {
         return dispatch({
           type: 'setError',
           value: _(msg`Please choose your password.`),
+          field: 'password',
         })
       }
       if (!state.handle) {
@@ -233,6 +284,7 @@ export function useSubmitSignup() {
         return dispatch({
           type: 'setError',
           value: _(msg`Please choose your handle.`),
+          field: 'handle',
         })
       }
       if (
@@ -253,15 +305,26 @@ export function useSubmitSignup() {
       dispatch({type: 'setIsLoading', value: true})
 
       try {
-        await createAccount({
-          service: state.serviceUrl,
-          email: state.email,
-          handle: createFullHandle(state.handle, state.userDomain),
-          password: state.password,
-          birthDate: state.dateOfBirth,
-          inviteCode: state.inviteCode.trim(),
-          verificationCode: state.pendingSubmit?.verificationCode,
-        })
+        await createAccount(
+          {
+            service: state.serviceUrl,
+            email: state.email,
+            handle: createFullHandle(state.handle, state.userDomain),
+            password: state.password,
+            birthDate: state.dateOfBirth,
+            inviteCode: state.inviteCode.trim(),
+            verificationCode: state.pendingSubmit?.verificationCode,
+          },
+          {
+            signupDuration: Date.now() - state.signupStartTime,
+            fieldErrorsTotal: Object.values(state.fieldErrors).reduce(
+              (a, b) => a + b,
+              0,
+            ),
+            backgroundCount: state.backgroundCount,
+          },
+        )
+
         /*
          * Must happen last so that if the user has multiple tabs open and
          * createAccount fails, one tab is not stuck in onboarding — Eric
@@ -275,6 +338,7 @@ export function useSubmitSignup() {
             value: _(
               msg`Invite code not accepted. Check that you input it correctly and try again.`,
             ),
+            field: 'invite-code',
           })
           dispatch({type: 'setStep', value: SignupStep.INFO})
           return
@@ -284,7 +348,11 @@ export function useSubmitSignup() {
         const isHandleError = error.toLowerCase().includes('handle')
 
         dispatch({type: 'setIsLoading', value: false})
-        dispatch({type: 'setError', value: error})
+        dispatch({
+          type: 'setError',
+          value: error,
+          field: isHandleError ? 'handle' : undefined,
+        })
         dispatch({type: 'setStep', value: isHandleError ? 2 : 1})
 
         logger.error('Signup Flow Error', {
diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx
index 03a8a936a..45384c4f5 100644
--- a/src/state/session/index.tsx
+++ b/src/state/session/index.tsx
@@ -1,7 +1,6 @@
 import React from 'react'
-import {AtpSessionEvent, BskyAgent} from '@atproto/api'
+import {type AtpSessionEvent, type BskyAgent} from '@atproto/api'
 
-import {logEvent} from '#/lib/statsig/statsig'
 import {isWeb} from '#/platform/detection'
 import * as persisted from '#/state/persisted'
 import {useCloseAllActiveElements} from '#/state/util'
@@ -9,7 +8,7 @@ import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
 import {emitSessionDropped} from '../events'
 import {
   agentToSessionAccount,
-  BskyAppAgent,
+  type BskyAppAgent,
   createAgentAndCreateAccount,
   createAgentAndLogin,
   createAgentAndResume,
@@ -20,7 +19,11 @@ import {getInitialState, reducer} from './reducer'
 export {isSignupQueued} from './util'
 import {addSessionDebugLog} from './logging'
 export type {SessionAccount} from '#/state/session/types'
-import {SessionApiContext, SessionStateContext} from '#/state/session/types'
+import {logger} from '#/logger'
+import {
+  type SessionApiContext,
+  type SessionStateContext,
+} from '#/state/session/types'
 
 const StateContext = React.createContext<SessionStateContext>({
   accounts: [],
@@ -65,10 +68,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   )
 
   const createAccount = React.useCallback<SessionApiContext['createAccount']>(
-    async params => {
+    async (params, metrics) => {
       addSessionDebugLog({type: 'method:start', method: 'createAccount'})
       const signal = cancelPendingTask()
-      logEvent('account:create:begin', {})
+      logger.metric('account:create:begin', {}, {statsig: true})
       const {agent, account} = await createAgentAndCreateAccount(
         params,
         onAgentSessionChange,
@@ -82,7 +85,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
         newAgent: agent,
         newAccount: account,
       })
-      logEvent('account:create:success', {})
+      logger.metric('account:create:success', metrics, {statsig: true})
       addSessionDebugLog({type: 'method:end', method: 'createAccount', account})
     },
     [onAgentSessionChange, cancelPendingTask],
@@ -105,7 +108,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
         newAgent: agent,
         newAccount: account,
       })
-      logEvent('account:loggedIn', {logContext, withPassword: true})
+      logger.metric(
+        'account:loggedIn',
+        {logContext, withPassword: true},
+        {statsig: true},
+      )
       addSessionDebugLog({type: 'method:end', method: 'login', account})
     },
     [onAgentSessionChange, cancelPendingTask],
@@ -120,7 +127,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       dispatch({
         type: 'logged-out-current-account',
       })
-      logEvent('account:loggedOut', {logContext, scope: 'current'})
+      logger.metric(
+        'account:loggedOut',
+        {logContext, scope: 'current'},
+        {statsig: true},
+      )
       addSessionDebugLog({type: 'method:end', method: 'logout'})
     },
     [cancelPendingTask],
@@ -135,7 +146,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       dispatch({
         type: 'logged-out-every-account',
       })
-      logEvent('account:loggedOut', {logContext, scope: 'every'})
+      logger.metric(
+        'account:loggedOut',
+        {logContext, scope: 'every'},
+        {statsig: true},
+      )
       addSessionDebugLog({type: 'method:end', method: 'logout'})
     },
     [cancelPendingTask],
diff --git a/src/state/session/types.ts b/src/state/session/types.ts
index d32259de9..9aadf9d05 100644
--- a/src/state/session/types.ts
+++ b/src/state/session/types.ts
@@ -10,16 +10,19 @@ export type SessionStateContext = {
 }
 
 export type SessionApiContext = {
-  createAccount: (props: {
-    service: string
-    email: string
-    password: string
-    handle: string
-    birthDate: Date
-    inviteCode?: string
-    verificationPhone?: string
-    verificationCode?: string
-  }) => Promise<void>
+  createAccount: (
+    props: {
+      service: string
+      email: string
+      password: string
+      handle: string
+      birthDate: Date
+      inviteCode?: string
+      verificationPhone?: string
+      verificationCode?: string
+    },
+    metrics: LogEvents['account:create:success'],
+  ) => Promise<void>
   login: (
     props: {
       service: string