about summary refs log tree commit diff
diff options
context:
space:
mode:
authordan <dan.abramov@gmail.com>2024-04-18 04:39:29 +0100
committerGitHub <noreply@github.com>2024-04-18 04:39:29 +0100
commit02becdf4491cded0f0435e880e1ad4030d500403 (patch)
tree725ffa94609c380d4a9d8f25609642eb7d4b4748
parent086dc93a7a6e69b0df2ed084ee68bb4e26c13f88 (diff)
downloadvoidsky-02becdf4491cded0f0435e880e1ad4030d500403.tar.zst
[Statsig] Make gate checks lazily (#3594)
-rw-r--r--eslint/use-typed-gates.js1
-rw-r--r--src/lib/hooks/useOTAUpdates.ts4
-rw-r--r--src/lib/statsig/statsig.tsx32
-rw-r--r--src/screens/Profile/Header/ProfileHeaderStandard.tsx6
-rw-r--r--src/state/shell/selected-feed.tsx11
-rw-r--r--src/view/com/feeds/FeedPage.tsx6
-rw-r--r--src/view/com/post-thread/PostThreadFollowBtn.tsx4
-rw-r--r--src/view/com/util/List.tsx8
-rw-r--r--src/view/com/util/Views.jsx7
-rw-r--r--src/view/screens/Home.tsx19
-rw-r--r--src/view/screens/ModerationBlockedAccounts.tsx7
-rw-r--r--src/view/screens/ModerationMutedAccounts.tsx8
-rw-r--r--src/view/screens/Profile.tsx6
-rw-r--r--src/view/screens/Search/Search.tsx10
14 files changed, 67 insertions, 62 deletions
diff --git a/eslint/use-typed-gates.js b/eslint/use-typed-gates.js
index 6c0331afe..b245072ba 100644
--- a/eslint/use-typed-gates.js
+++ b/eslint/use-typed-gates.js
@@ -25,6 +25,7 @@ exports.create = function create(context) {
             "Use useGate() from '#/lib/statsig/statsig' instead of the one on npm.",
         })
       }
+      // TODO: Verify gate() call results aren't stored in variables.
     },
   }
 }
diff --git a/src/lib/hooks/useOTAUpdates.ts b/src/lib/hooks/useOTAUpdates.ts
index 70905c137..b8d331c6f 100644
--- a/src/lib/hooks/useOTAUpdates.ts
+++ b/src/lib/hooks/useOTAUpdates.ts
@@ -31,8 +31,8 @@ async function setExtraParams() {
 }
 
 export function useOTAUpdates() {
-  const shouldReceiveUpdates =
-    useGate('receive_updates') && isEnabled && !__DEV__
+  const gate = useGate()
+  const shouldReceiveUpdates = isEnabled && !__DEV__ && gate('receive_updates')
 
   const appState = React.useRef<AppStateStatus>('active')
   const lastMinimize = React.useRef(0)
diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx
index df540d79e..62dd79bb2 100644
--- a/src/lib/statsig/statsig.tsx
+++ b/src/lib/statsig/statsig.tsx
@@ -2,11 +2,7 @@ import React from 'react'
 import {Platform} from 'react-native'
 import {AppState, AppStateStatus} from 'react-native'
 import {sha256} from 'js-sha256'
-import {
-  Statsig,
-  StatsigProvider,
-  useGate as useStatsigGate,
-} from 'statsig-react-native-expo'
+import {Statsig, StatsigProvider} from 'statsig-react-native-expo'
 
 import {logger} from '#/logger'
 import {isWeb} from '#/platform/detection'
@@ -98,16 +94,24 @@ export function logEvent<E extends keyof LogEvents>(
   }
 }
 
-export function useGate(gateName: Gate): boolean {
-  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.')
+export function useGate(): (gateName: Gate) => boolean {
+  const cache = React.useRef<Map<Gate, boolean>>()
+  if (cache.current === undefined) {
+    cache.current = new Map()
   }
-  // This shouldn't technically be necessary but let's get a strong
-  // guarantee that a gate value can never change while mounted.
-  const [initialValue] = React.useState(value)
-  return initialValue
+  const gate = React.useCallback((gateName: Gate): boolean => {
+    // TODO: Replace local cache with a proper session one.
+    const cachedValue = cache.current!.get(gateName)
+    if (cachedValue !== undefined) {
+      return cachedValue
+    }
+    const value = Statsig.initializeCalled()
+      ? Statsig.checkGate(gateName)
+      : false
+    cache.current!.set(gateName, value)
+    return value
+  }, [])
+  return gate
 }
 
 function toStatsigUser(did: string | undefined): StatsigUser {
diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
index accef12ed..9e0361326 100644
--- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx
+++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
@@ -80,9 +80,7 @@ let ProfileHeaderStandard = ({
     })
   }, [track, openModal, profile])
 
-  const autoExpandSuggestionsOnProfileFollow = useGate(
-    'autoexpand_suggestions_on_profile_follow',
-  )
+  const gate = useGate()
   const onPressFollow = () => {
     requireAuth(async () => {
       try {
@@ -96,7 +94,7 @@ let ProfileHeaderStandard = ({
             )}`,
           ),
         )
-        if (isWeb && autoExpandSuggestionsOnProfileFollow) {
+        if (isWeb && gate('autoexpand_suggestions_on_profile_follow')) {
           setShowSuggestedFollows(true)
         }
       } catch (e: any) {
diff --git a/src/state/shell/selected-feed.tsx b/src/state/shell/selected-feed.tsx
index 5c0ac0b02..dca3445f3 100644
--- a/src/state/shell/selected-feed.tsx
+++ b/src/state/shell/selected-feed.tsx
@@ -1,5 +1,6 @@
 import React from 'react'
 
+import {Gate} from '#/lib/statsig/gates'
 import {useGate} from '#/lib/statsig/statsig'
 import {isWeb} from '#/platform/detection'
 import * as persisted from '#/state/persisted'
@@ -10,7 +11,7 @@ type SetContext = (v: string) => void
 const stateContext = React.createContext<StateContext>('home')
 const setContext = React.createContext<SetContext>((_: string) => {})
 
-function getInitialFeed(startSessionWithFollowing: boolean) {
+function getInitialFeed(gate: (gateName: Gate) => boolean) {
   if (isWeb) {
     if (window.location.pathname === '/') {
       const params = new URLSearchParams(window.location.search)
@@ -26,7 +27,7 @@ function getInitialFeed(startSessionWithFollowing: boolean) {
       return feedFromSession
     }
   }
-  if (!startSessionWithFollowing) {
+  if (!gate('start_session_with_following')) {
     const feedFromPersisted = persisted.get('lastSelectedHomeFeed')
     if (feedFromPersisted) {
       // Fall back to the last chosen one across all tabs.
@@ -37,10 +38,8 @@ function getInitialFeed(startSessionWithFollowing: boolean) {
 }
 
 export function Provider({children}: React.PropsWithChildren<{}>) {
-  const startSessionWithFollowing = useGate('start_session_with_following')
-  const [state, setState] = React.useState(() =>
-    getInitialFeed(startSessionWithFollowing),
-  )
+  const gate = useGate()
+  const [state, setState] = React.useState(() => getInitialFeed(gate))
 
   const saveState = React.useCallback((feed: string) => {
     setState(feed)
diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx
index 25c7e1006..2b8fde632 100644
--- a/src/view/com/feeds/FeedPage.tsx
+++ b/src/view/com/feeds/FeedPage.tsx
@@ -53,6 +53,7 @@ export function FeedPage({
   const headerOffset = useHeaderOffset()
   const scrollElRef = React.useRef<ListMethods>(null)
   const [hasNew, setHasNew] = React.useState(false)
+  const gate = useGate()
 
   const scrollToTop = React.useCallback(() => {
     scrollElRef.current?.scrollToOffset({
@@ -105,9 +106,10 @@ export function FeedPage({
 
   let feedPollInterval
   if (
-    useGate('disable_poll_on_discover') &&
     feed === // Discover
-      'feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot'
+      'feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot' &&
+    // TODO: This gate check is still too early. Move it to where the polling happens.
+    gate('disable_poll_on_discover')
   ) {
     feedPollInterval = undefined
   } else {
diff --git a/src/view/com/post-thread/PostThreadFollowBtn.tsx b/src/view/com/post-thread/PostThreadFollowBtn.tsx
index 8b297121e..7c9a54451 100644
--- a/src/view/com/post-thread/PostThreadFollowBtn.tsx
+++ b/src/view/com/post-thread/PostThreadFollowBtn.tsx
@@ -48,7 +48,7 @@ function PostThreadFollowBtnLoaded({
     'PostThreadItem',
   )
   const requireAuth = useRequireAuth()
-  const showFollowBackLabel = useGate('show_follow_back_label')
+  const gate = useGate()
 
   const isFollowing = !!profile.viewer?.following
   const isFollowedBy = !!profile.viewer?.followedBy
@@ -140,7 +140,7 @@ function PostThreadFollowBtnLoaded({
             style={[!isFollowing ? palInverted.text : pal.text, s.bold]}
             numberOfLines={1}>
             {!isFollowing ? (
-              showFollowBackLabel && isFollowedBy ? (
+              isFollowedBy && gate('show_follow_back_label') ? (
                 <Trans>Follow Back</Trans>
               ) : (
                 <Trans>Follow</Trans>
diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx
index b3bde2a11..5729a43a5 100644
--- a/src/view/com/util/List.tsx
+++ b/src/view/com/util/List.tsx
@@ -40,8 +40,8 @@ function ListImpl<ItemT>(
   const isScrolledDown = useSharedValue(false)
   const contextScrollHandlers = useScrollHandlers()
   const pal = usePalette('default')
-  const showsVerticalScrollIndicator =
-    !useGate('hide_vertical_scroll_indicators') || isWeb
+  const gate = useGate()
+
   function handleScrolledDownChange(didScrollDown: boolean) {
     onScrolledDownChange?.(didScrollDown)
   }
@@ -97,7 +97,9 @@ function ListImpl<ItemT>(
       scrollEventThrottle={1}
       style={style}
       ref={ref}
-      showsVerticalScrollIndicator={showsVerticalScrollIndicator}
+      showsVerticalScrollIndicator={
+        isWeb || !gate('hide_vertical_scroll_indicators')
+      }
     />
   )
 }
diff --git a/src/view/com/util/Views.jsx b/src/view/com/util/Views.jsx
index 6850f42a4..75f2b5081 100644
--- a/src/view/com/util/Views.jsx
+++ b/src/view/com/util/Views.jsx
@@ -10,14 +10,11 @@ export function CenteredView(props) {
 }
 
 export function ScrollView(props) {
-  const showsVerticalScrollIndicator = !useGate(
-    'hide_vertical_scroll_indicators',
-  )
-
+  const gate = useGate()
   return (
     <Animated.ScrollView
       {...props}
-      showsVerticalScrollIndicator={showsVerticalScrollIndicator}
+      showsVerticalScrollIndicator={!gate('hide_vertical_scroll_indicators')}
     />
   )
 }
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index b55053af0..fbaa49a32 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -111,21 +111,20 @@ function HomeScreenReady({
     }),
   )
 
-  const disableMinShellOnForegrounding = useGate(
-    'disable_min_shell_on_foregrounding',
-  )
+  const gate = useGate()
   React.useEffect(() => {
-    if (disableMinShellOnForegrounding) {
-      const listener = AppState.addEventListener('change', nextAppState => {
-        if (nextAppState === 'active') {
+    const listener = AppState.addEventListener('change', nextAppState => {
+      if (nextAppState === 'active') {
+        // TODO: Check if minimal shell is on before logging an exposure.
+        if (gate('disable_min_shell_on_foregrounding')) {
           setMinimalShellMode(false)
         }
-      })
-      return () => {
-        listener.remove()
       }
+    })
+    return () => {
+      listener.remove()
     }
-  }, [setMinimalShellMode, disableMinShellOnForegrounding])
+  }, [setMinimalShellMode, gate])
 
   const onPageSelected = React.useCallback(
     (index: number) => {
diff --git a/src/view/screens/ModerationBlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx
index 7b68c2256..b7ce8cdd0 100644
--- a/src/view/screens/ModerationBlockedAccounts.tsx
+++ b/src/view/screens/ModerationBlockedAccounts.tsx
@@ -38,8 +38,7 @@ export function ModerationBlockedAccounts({}: Props) {
   const setMinimalShellMode = useSetMinimalShellMode()
   const {isTabletOrDesktop} = useWebMediaQueries()
   const {screen} = useAnalytics()
-  const showsVerticalScrollIndicator =
-    !useGate('hide_vertical_scroll_indicators') || isWeb
+  const gate = useGate()
 
   const [isPTRing, setIsPTRing] = React.useState(false)
   const {
@@ -169,7 +168,9 @@ export function ModerationBlockedAccounts({}: Props) {
           )}
           // @ts-ignore our .web version only -prf
           desktopFixedHeight
-          showsVerticalScrollIndicator={showsVerticalScrollIndicator}
+          showsVerticalScrollIndicator={
+            isWeb || !gate('hide_vertical_scroll_indicators')
+          }
         />
       )}
     </CenteredView>
diff --git a/src/view/screens/ModerationMutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx
index 22dd5a278..4d7ca6294 100644
--- a/src/view/screens/ModerationMutedAccounts.tsx
+++ b/src/view/screens/ModerationMutedAccounts.tsx
@@ -38,8 +38,8 @@ export function ModerationMutedAccounts({}: Props) {
   const setMinimalShellMode = useSetMinimalShellMode()
   const {isTabletOrDesktop} = useWebMediaQueries()
   const {screen} = useAnalytics()
-  const showsVerticalScrollIndicator =
-    !useGate('hide_vertical_scroll_indicators') || isWeb
+  const gate = useGate()
+
   const [isPTRing, setIsPTRing] = React.useState(false)
   const {
     data,
@@ -167,7 +167,9 @@ export function ModerationMutedAccounts({}: Props) {
           )}
           // @ts-ignore our .web version only -prf
           desktopFixedHeight
-          showsVerticalScrollIndicator={showsVerticalScrollIndicator}
+          showsVerticalScrollIndicator={
+            isWeb || !gate('hide_vertical_scroll_indicators')
+          }
         />
       )}
     </CenteredView>
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index f71e1330e..c7f5a6627 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -143,7 +143,7 @@ function ProfileScreenLoaded({
   const setMinimalShellMode = useSetMinimalShellMode()
   const {openComposer} = useComposerControls()
   const {screen, track} = useAnalytics()
-  const shouldUseScrollableHeader = useGate('new_profile_scroll_component')
+  const gate = useGate()
   const {
     data: labelerInfo,
     error: labelerError,
@@ -317,7 +317,7 @@ function ProfileScreenLoaded({
   // =
 
   const renderHeader = React.useCallback(() => {
-    if (shouldUseScrollableHeader) {
+    if (gate('new_profile_scroll_component')) {
       return (
         <ExpoScrollForwarderView scrollViewTag={scrollViewTag}>
           <ProfileHeader
@@ -343,7 +343,7 @@ function ProfileScreenLoaded({
       )
     }
   }, [
-    shouldUseScrollableHeader,
+    gate,
     scrollViewTag,
     profile,
     labelerInfo,
diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx
index f5ebd155c..0b11ff767 100644
--- a/src/view/screens/Search/Search.tsx
+++ b/src/view/screens/Search/Search.tsx
@@ -210,7 +210,8 @@ function useSuggestedFollowsV2(): [
 
 function SearchScreenSuggestedFollows() {
   const pal = usePalette('default')
-  const useSuggestedFollows = useGate('use_new_suggestions_endpoint')
+  const gate = useGate()
+  const useSuggestedFollows = gate('use_new_suggestions_endpoint')
     ? // Conditional hook call here is *only* OK because useGate()
       // result won't change until a remount.
       useSuggestedFollowsV2
@@ -406,8 +407,7 @@ export function SearchScreenInner({
   const {isDesktop} = useWebMediaQueries()
   const [activeTab, setActiveTab] = React.useState(0)
   const {_} = useLingui()
-
-  const isNewSearch = useGate('new_search')
+  const gate = useGate()
 
   const onPageSelected = React.useCallback(
     (index: number) => {
@@ -420,7 +420,7 @@ export function SearchScreenInner({
 
   const sections = React.useMemo(() => {
     if (!query) return []
-    if (isNewSearch) {
+    if (gate('new_search')) {
       if (hasSession) {
         return [
           {
@@ -487,7 +487,7 @@ export function SearchScreenInner({
         ]
       }
     }
-  }, [hasSession, isNewSearch, _, query, activeTab])
+  }, [hasSession, gate, _, query, activeTab])
 
   if (hasSession) {
     return query ? (