about summary refs log tree commit diff
path: root/src/lib/statsig
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/statsig')
-rw-r--r--src/lib/statsig/events.ts63
-rw-r--r--src/lib/statsig/statsig.tsx58
-rw-r--r--src/lib/statsig/statsig.web.tsx75
3 files changed, 115 insertions, 81 deletions
diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts
new file mode 100644
index 000000000..b83095976
--- /dev/null
+++ b/src/lib/statsig/events.ts
@@ -0,0 +1,63 @@
+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': {
+    secondsActive: number
+  }
+  'state:foreground': {}
+  'feed:endReached': {
+    feedType: string
+    itemCount: number
+  }
+  'feed:refresh': {
+    feedType: string
+    reason: 'pull-to-refresh' | 'soft-reset' | 'load-latest'
+  }
+  'post:create': {
+    imageCount: number
+    isReply: boolean
+    hasLink: boolean
+    hasQuote: boolean
+    langs: string
+    logContext: 'Composer'
+  }
+  'post:like': {
+    logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
+  }
+  'post:repost': {
+    logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
+  }
+  'post:unlike': {
+    logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
+  }
+  'post:unrepost': {
+    logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
+  }
+  'profile:follow': {
+    logContext:
+      | 'RecommendedFollowsItem'
+      | 'PostThreadItem'
+      | 'ProfileCard'
+      | 'ProfileHeader'
+      | 'ProfileHeaderSuggestedFollows'
+      | 'ProfileMenu'
+  }
+  'profile:unfollow': {
+    logContext:
+      | 'RecommendedFollowsItem'
+      | 'PostThreadItem'
+      | 'ProfileCard'
+      | 'ProfileHeader'
+      | 'ProfileHeaderSuggestedFollows'
+      | 'ProfileMenu'
+  }
+}
diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx
index 6d9ebeb09..9fa6cce2d 100644
--- a/src/lib/statsig/statsig.tsx
+++ b/src/lib/statsig/statsig.tsx
@@ -1,11 +1,16 @@
 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'
+
+export type {LogEvents}
 
 const statsigOptions = {
   environment: {
@@ -17,12 +22,28 @@ const statsigOptions = {
   initTimeoutMs: 1,
 }
 
-export function logEvent(
-  eventName: string,
-  value?: string | number | null,
-  metadata?: Record<string, string> | null,
+type FlatJSONRecord = Record<
+  string,
+  string | number | boolean | null | undefined
+>
+
+let getCurrentRouteName: () => string | null | undefined = () => null
+
+export function attachRouteToLogEvents(
+  getRouteName: () => string | null | undefined,
+) {
+  getCurrentRouteName = getRouteName
+}
+
+export function logEvent<E extends keyof LogEvents>(
+  eventName: E & string,
+  rawMetadata: LogEvents[E] & FlatJSONRecord,
 ) {
-  Statsig.logEvent(eventName, value, metadata)
+  const fullMetadata = {
+    ...rawMetadata,
+  } as Record<string, string> // Statsig typings are unnecessarily strict here.
+  fullMetadata.routeName = getCurrentRouteName() ?? '(Uninitialized)'
+  Statsig.logEvent(eventName, null, fullMetadata)
 }
 
 export function useGate(gateName: string) {
@@ -39,9 +60,34 @@ function toStatsigUser(did: string | undefined) {
   if (did) {
     userID = sha256(did)
   }
-  return {userID}
+  return {
+    userID,
+    platform: Platform.OS,
+  }
 }
 
+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 {
+    let secondsActive = 0
+    if (lastActive != null) {
+      secondsActive = Math.round((performance.now() - lastActive) / 1e3)
+    }
+    lastActive = null
+    logEvent('state:background', {
+      secondsActive,
+    })
+  }
+})
+
 export function Provider({children}: {children: React.ReactNode}) {
   const {currentAccount} = useSession()
   const currentStatsigUser = React.useMemo(
diff --git a/src/lib/statsig/statsig.web.tsx b/src/lib/statsig/statsig.web.tsx
deleted file mode 100644
index d1c912019..000000000
--- a/src/lib/statsig/statsig.web.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import React from 'react'
-import {
-  Statsig,
-  StatsigProvider,
-  useGate as useStatsigGate,
-} from 'statsig-react'
-import {useSession} from '../../state/session'
-import {sha256} from 'js-sha256'
-
-const statsigOptions = {
-  environment: {
-    tier: process.env.NODE_ENV === 'development' ? 'development' : 'production',
-  },
-  // Don't block on waiting for network. The fetched config will kick in on next load.
-  // This ensures the UI is always consistent and doesn't update mid-session.
-  // Note this makes cold load (no local storage) and private mode return `false` for all gates.
-  initTimeoutMs: 1,
-}
-
-export function logEvent(
-  eventName: string,
-  value?: string | number | null,
-  metadata?: Record<string, string> | null,
-) {
-  Statsig.logEvent(eventName, value, metadata)
-}
-
-export function useGate(gateName: string) {
-  const {isLoading, value} = useStatsigGate(gateName)
-  if (isLoading) {
-    // This should not happen because of waitForInitialization={true}.
-    console.error('Did not expected isLoading to ever be true.')
-  }
-  return value
-}
-
-function toStatsigUser(did: string | undefined) {
-  let userID: string | undefined
-  if (did) {
-    userID = sha256(did)
-  }
-  return {userID}
-}
-
-export function Provider({children}: {children: React.ReactNode}) {
-  const {currentAccount} = useSession()
-  const currentStatsigUser = React.useMemo(
-    () => toStatsigUser(currentAccount?.did),
-    [currentAccount?.did],
-  )
-
-  React.useEffect(() => {
-    function refresh() {
-      // Intentionally refetching the config using the JS SDK rather than React SDK
-      // so that the new config is stored in cache but isn't used during this session.
-      // It will kick in for the next reload.
-      Statsig.updateUser(currentStatsigUser)
-    }
-    const id = setInterval(refresh, 3 * 60e3 /* 3 min */)
-    return () => clearInterval(id)
-  }, [currentStatsigUser])
-
-  return (
-    <StatsigProvider
-      sdkKey="client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV"
-      mountKey={currentStatsigUser.userID}
-      user={currentStatsigUser}
-      // This isn't really blocking due to short initTimeoutMs above.
-      // However, it ensures `isLoading` is always `false`.
-      waitForInitialization={true}
-      options={statsigOptions}>
-      {children}
-    </StatsigProvider>
-  )
-}