Bluesky app fork with some witchin' additions 💫

[Statsig] Make gate checks lazily (#3594)

authored by danabra.mov and committed by GitHub 02becdf4 086dc93a

+1
eslint/use-typed-gates.js
··· 25 25 "Use useGate() from '#/lib/statsig/statsig' instead of the one on npm.", 26 26 }) 27 27 } 28 + // TODO: Verify gate() call results aren't stored in variables. 28 29 }, 29 30 } 30 31 }
+2 -2
src/lib/hooks/useOTAUpdates.ts
··· 31 31 } 32 32 33 33 export function useOTAUpdates() { 34 - const shouldReceiveUpdates = 35 - useGate('receive_updates') && isEnabled && !__DEV__ 34 + const gate = useGate() 35 + const shouldReceiveUpdates = isEnabled && !__DEV__ && gate('receive_updates') 36 36 37 37 const appState = React.useRef<AppStateStatus>('active') 38 38 const lastMinimize = React.useRef(0)
+18 -14
src/lib/statsig/statsig.tsx
··· 2 2 import {Platform} from 'react-native' 3 3 import {AppState, AppStateStatus} from 'react-native' 4 4 import {sha256} from 'js-sha256' 5 - import { 6 - Statsig, 7 - StatsigProvider, 8 - useGate as useStatsigGate, 9 - } from 'statsig-react-native-expo' 5 + import {Statsig, StatsigProvider} from 'statsig-react-native-expo' 10 6 11 7 import {logger} from '#/logger' 12 8 import {isWeb} from '#/platform/detection' ··· 98 94 } 99 95 } 100 96 101 - export function useGate(gateName: Gate): boolean { 102 - const {isLoading, value} = useStatsigGate(gateName) 103 - if (isLoading) { 104 - // This should not happen because of waitForInitialization={true}. 105 - console.error('Did not expected isLoading to ever be true.') 97 + export function useGate(): (gateName: Gate) => boolean { 98 + const cache = React.useRef<Map<Gate, boolean>>() 99 + if (cache.current === undefined) { 100 + cache.current = new Map() 106 101 } 107 - // This shouldn't technically be necessary but let's get a strong 108 - // guarantee that a gate value can never change while mounted. 109 - const [initialValue] = React.useState(value) 110 - return initialValue 102 + const gate = React.useCallback((gateName: Gate): boolean => { 103 + // TODO: Replace local cache with a proper session one. 104 + const cachedValue = cache.current!.get(gateName) 105 + if (cachedValue !== undefined) { 106 + return cachedValue 107 + } 108 + const value = Statsig.initializeCalled() 109 + ? Statsig.checkGate(gateName) 110 + : false 111 + cache.current!.set(gateName, value) 112 + return value 113 + }, []) 114 + return gate 111 115 } 112 116 113 117 function toStatsigUser(did: string | undefined): StatsigUser {
+2 -4
src/screens/Profile/Header/ProfileHeaderStandard.tsx
··· 80 80 }) 81 81 }, [track, openModal, profile]) 82 82 83 - const autoExpandSuggestionsOnProfileFollow = useGate( 84 - 'autoexpand_suggestions_on_profile_follow', 85 - ) 83 + const gate = useGate() 86 84 const onPressFollow = () => { 87 85 requireAuth(async () => { 88 86 try { ··· 96 94 )}`, 97 95 ), 98 96 ) 99 - if (isWeb && autoExpandSuggestionsOnProfileFollow) { 97 + if (isWeb && gate('autoexpand_suggestions_on_profile_follow')) { 100 98 setShowSuggestedFollows(true) 101 99 } 102 100 } catch (e: any) {
+5 -6
src/state/shell/selected-feed.tsx
··· 1 1 import React from 'react' 2 2 3 + import {Gate} from '#/lib/statsig/gates' 3 4 import {useGate} from '#/lib/statsig/statsig' 4 5 import {isWeb} from '#/platform/detection' 5 6 import * as persisted from '#/state/persisted' ··· 10 11 const stateContext = React.createContext<StateContext>('home') 11 12 const setContext = React.createContext<SetContext>((_: string) => {}) 12 13 13 - function getInitialFeed(startSessionWithFollowing: boolean) { 14 + function getInitialFeed(gate: (gateName: Gate) => boolean) { 14 15 if (isWeb) { 15 16 if (window.location.pathname === '/') { 16 17 const params = new URLSearchParams(window.location.search) ··· 26 27 return feedFromSession 27 28 } 28 29 } 29 - if (!startSessionWithFollowing) { 30 + if (!gate('start_session_with_following')) { 30 31 const feedFromPersisted = persisted.get('lastSelectedHomeFeed') 31 32 if (feedFromPersisted) { 32 33 // Fall back to the last chosen one across all tabs. ··· 37 38 } 38 39 39 40 export function Provider({children}: React.PropsWithChildren<{}>) { 40 - const startSessionWithFollowing = useGate('start_session_with_following') 41 - const [state, setState] = React.useState(() => 42 - getInitialFeed(startSessionWithFollowing), 43 - ) 41 + const gate = useGate() 42 + const [state, setState] = React.useState(() => getInitialFeed(gate)) 44 43 45 44 const saveState = React.useCallback((feed: string) => { 46 45 setState(feed)
+4 -2
src/view/com/feeds/FeedPage.tsx
··· 53 53 const headerOffset = useHeaderOffset() 54 54 const scrollElRef = React.useRef<ListMethods>(null) 55 55 const [hasNew, setHasNew] = React.useState(false) 56 + const gate = useGate() 56 57 57 58 const scrollToTop = React.useCallback(() => { 58 59 scrollElRef.current?.scrollToOffset({ ··· 105 106 106 107 let feedPollInterval 107 108 if ( 108 - useGate('disable_poll_on_discover') && 109 109 feed === // Discover 110 - 'feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot' 110 + 'feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot' && 111 + // TODO: This gate check is still too early. Move it to where the polling happens. 112 + gate('disable_poll_on_discover') 111 113 ) { 112 114 feedPollInterval = undefined 113 115 } else {
+2 -2
src/view/com/post-thread/PostThreadFollowBtn.tsx
··· 48 48 'PostThreadItem', 49 49 ) 50 50 const requireAuth = useRequireAuth() 51 - const showFollowBackLabel = useGate('show_follow_back_label') 51 + const gate = useGate() 52 52 53 53 const isFollowing = !!profile.viewer?.following 54 54 const isFollowedBy = !!profile.viewer?.followedBy ··· 140 140 style={[!isFollowing ? palInverted.text : pal.text, s.bold]} 141 141 numberOfLines={1}> 142 142 {!isFollowing ? ( 143 - showFollowBackLabel && isFollowedBy ? ( 143 + isFollowedBy && gate('show_follow_back_label') ? ( 144 144 <Trans>Follow Back</Trans> 145 145 ) : ( 146 146 <Trans>Follow</Trans>
+5 -3
src/view/com/util/List.tsx
··· 40 40 const isScrolledDown = useSharedValue(false) 41 41 const contextScrollHandlers = useScrollHandlers() 42 42 const pal = usePalette('default') 43 - const showsVerticalScrollIndicator = 44 - !useGate('hide_vertical_scroll_indicators') || isWeb 43 + const gate = useGate() 44 + 45 45 function handleScrolledDownChange(didScrollDown: boolean) { 46 46 onScrolledDownChange?.(didScrollDown) 47 47 } ··· 97 97 scrollEventThrottle={1} 98 98 style={style} 99 99 ref={ref} 100 - showsVerticalScrollIndicator={showsVerticalScrollIndicator} 100 + showsVerticalScrollIndicator={ 101 + isWeb || !gate('hide_vertical_scroll_indicators') 102 + } 101 103 /> 102 104 ) 103 105 }
+2 -5
src/view/com/util/Views.jsx
··· 10 10 } 11 11 12 12 export function ScrollView(props) { 13 - const showsVerticalScrollIndicator = !useGate( 14 - 'hide_vertical_scroll_indicators', 15 - ) 16 - 13 + const gate = useGate() 17 14 return ( 18 15 <Animated.ScrollView 19 16 {...props} 20 - showsVerticalScrollIndicator={showsVerticalScrollIndicator} 17 + showsVerticalScrollIndicator={!gate('hide_vertical_scroll_indicators')} 21 18 /> 22 19 ) 23 20 }
+9 -10
src/view/screens/Home.tsx
··· 111 111 }), 112 112 ) 113 113 114 - const disableMinShellOnForegrounding = useGate( 115 - 'disable_min_shell_on_foregrounding', 116 - ) 114 + const gate = useGate() 117 115 React.useEffect(() => { 118 - if (disableMinShellOnForegrounding) { 119 - const listener = AppState.addEventListener('change', nextAppState => { 120 - if (nextAppState === 'active') { 116 + const listener = AppState.addEventListener('change', nextAppState => { 117 + if (nextAppState === 'active') { 118 + // TODO: Check if minimal shell is on before logging an exposure. 119 + if (gate('disable_min_shell_on_foregrounding')) { 121 120 setMinimalShellMode(false) 122 121 } 123 - }) 124 - return () => { 125 - listener.remove() 126 122 } 123 + }) 124 + return () => { 125 + listener.remove() 127 126 } 128 - }, [setMinimalShellMode, disableMinShellOnForegrounding]) 127 + }, [setMinimalShellMode, gate]) 129 128 130 129 const onPageSelected = React.useCallback( 131 130 (index: number) => {
+4 -3
src/view/screens/ModerationBlockedAccounts.tsx
··· 38 38 const setMinimalShellMode = useSetMinimalShellMode() 39 39 const {isTabletOrDesktop} = useWebMediaQueries() 40 40 const {screen} = useAnalytics() 41 - const showsVerticalScrollIndicator = 42 - !useGate('hide_vertical_scroll_indicators') || isWeb 41 + const gate = useGate() 43 42 44 43 const [isPTRing, setIsPTRing] = React.useState(false) 45 44 const { ··· 169 168 )} 170 169 // @ts-ignore our .web version only -prf 171 170 desktopFixedHeight 172 - showsVerticalScrollIndicator={showsVerticalScrollIndicator} 171 + showsVerticalScrollIndicator={ 172 + isWeb || !gate('hide_vertical_scroll_indicators') 173 + } 173 174 /> 174 175 )} 175 176 </CenteredView>
+5 -3
src/view/screens/ModerationMutedAccounts.tsx
··· 38 38 const setMinimalShellMode = useSetMinimalShellMode() 39 39 const {isTabletOrDesktop} = useWebMediaQueries() 40 40 const {screen} = useAnalytics() 41 - const showsVerticalScrollIndicator = 42 - !useGate('hide_vertical_scroll_indicators') || isWeb 41 + const gate = useGate() 42 + 43 43 const [isPTRing, setIsPTRing] = React.useState(false) 44 44 const { 45 45 data, ··· 167 167 )} 168 168 // @ts-ignore our .web version only -prf 169 169 desktopFixedHeight 170 - showsVerticalScrollIndicator={showsVerticalScrollIndicator} 170 + showsVerticalScrollIndicator={ 171 + isWeb || !gate('hide_vertical_scroll_indicators') 172 + } 171 173 /> 172 174 )} 173 175 </CenteredView>
+3 -3
src/view/screens/Profile.tsx
··· 143 143 const setMinimalShellMode = useSetMinimalShellMode() 144 144 const {openComposer} = useComposerControls() 145 145 const {screen, track} = useAnalytics() 146 - const shouldUseScrollableHeader = useGate('new_profile_scroll_component') 146 + const gate = useGate() 147 147 const { 148 148 data: labelerInfo, 149 149 error: labelerError, ··· 317 317 // = 318 318 319 319 const renderHeader = React.useCallback(() => { 320 - if (shouldUseScrollableHeader) { 320 + if (gate('new_profile_scroll_component')) { 321 321 return ( 322 322 <ExpoScrollForwarderView scrollViewTag={scrollViewTag}> 323 323 <ProfileHeader ··· 343 343 ) 344 344 } 345 345 }, [ 346 - shouldUseScrollableHeader, 346 + gate, 347 347 scrollViewTag, 348 348 profile, 349 349 labelerInfo,
+5 -5
src/view/screens/Search/Search.tsx
··· 210 210 211 211 function SearchScreenSuggestedFollows() { 212 212 const pal = usePalette('default') 213 - const useSuggestedFollows = useGate('use_new_suggestions_endpoint') 213 + const gate = useGate() 214 + const useSuggestedFollows = gate('use_new_suggestions_endpoint') 214 215 ? // Conditional hook call here is *only* OK because useGate() 215 216 // result won't change until a remount. 216 217 useSuggestedFollowsV2 ··· 406 407 const {isDesktop} = useWebMediaQueries() 407 408 const [activeTab, setActiveTab] = React.useState(0) 408 409 const {_} = useLingui() 409 - 410 - const isNewSearch = useGate('new_search') 410 + const gate = useGate() 411 411 412 412 const onPageSelected = React.useCallback( 413 413 (index: number) => { ··· 420 420 421 421 const sections = React.useMemo(() => { 422 422 if (!query) return [] 423 - if (isNewSearch) { 423 + if (gate('new_search')) { 424 424 if (hasSession) { 425 425 return [ 426 426 { ··· 487 487 ] 488 488 } 489 489 } 490 - }, [hasSession, isNewSearch, _, query, activeTab]) 490 + }, [hasSession, gate, _, query, activeTab]) 491 491 492 492 if (hasSession) { 493 493 return query ? (