Merge remote-tracking branch 'origin/main' into halo/base
* origin/main: [Statsig] Slightly block the UI on gates (#3608) [Statsig] Prefetch configs for other accounts (#3607)
This commit is contained in:
+72
-21
@@ -8,6 +8,8 @@ import {logger} from '#/logger'
|
|||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {IS_TESTFLIGHT} from 'lib/app-info'
|
import {IS_TESTFLIGHT} from 'lib/app-info'
|
||||||
import {useSession} from '../../state/session'
|
import {useSession} from '../../state/session'
|
||||||
|
import {timeout} from '../async/timeout'
|
||||||
|
import {useNonReactiveCallback} from '../hooks/useNonReactiveCallback'
|
||||||
import {LogEvents} from './events'
|
import {LogEvents} from './events'
|
||||||
import {Gate} from './gates'
|
import {Gate} from './gates'
|
||||||
|
|
||||||
@@ -34,19 +36,23 @@ if (isWeb && typeof window !== 'undefined') {
|
|||||||
|
|
||||||
export type {LogEvents}
|
export type {LogEvents}
|
||||||
|
|
||||||
const statsigOptions = {
|
function createStatsigOptions(prefetchUsers: StatsigUser[]) {
|
||||||
environment: {
|
return {
|
||||||
tier:
|
environment: {
|
||||||
process.env.NODE_ENV === 'development'
|
tier:
|
||||||
? 'development'
|
process.env.NODE_ENV === 'development'
|
||||||
: IS_TESTFLIGHT
|
? 'development'
|
||||||
? 'staging'
|
: IS_TESTFLIGHT
|
||||||
: 'production',
|
? 'staging'
|
||||||
},
|
: '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.
|
// Don't block on waiting for network. The fetched config will kick in on next load.
|
||||||
// Note this makes cold load (no local storage) and private mode return `false` for all gates.
|
// This ensures the UI is always consistent and doesn't update mid-session.
|
||||||
initTimeoutMs: 1,
|
// Note this makes cold load (no local storage) and private mode return `false` for all gates.
|
||||||
|
initTimeoutMs: 1,
|
||||||
|
// Get fresh flags for other accounts as well, if any.
|
||||||
|
prefetchUsers,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type FlatJSONRecord = Record<
|
type FlatJSONRecord = Record<
|
||||||
@@ -159,10 +165,51 @@ AppState.addEventListener('change', (state: AppStateStatus) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export async function tryFetchGates(
|
||||||
|
did: string,
|
||||||
|
strategy: 'prefer-low-latency' | 'prefer-fresh-gates',
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
let timeoutMs = 250 // Don't block the UI if we can't do this fast.
|
||||||
|
if (strategy === 'prefer-fresh-gates') {
|
||||||
|
// Use this for less common operations where the user would be OK with a delay.
|
||||||
|
timeoutMs = 1500
|
||||||
|
}
|
||||||
|
// Note: This condition is currently false the very first render because
|
||||||
|
// Statsig has not initialized yet. In the future, we can fix this by
|
||||||
|
// doing the initialization ourselves instead of relying on the provider.
|
||||||
|
if (Statsig.initializeCalled()) {
|
||||||
|
await Promise.race([
|
||||||
|
timeout(timeoutMs),
|
||||||
|
Statsig.prefetchUsers([toStatsigUser(did)]),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Don't leak errors to the calling code, this is meant to be always safe.
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function Provider({children}: {children: React.ReactNode}) {
|
export function Provider({children}: {children: React.ReactNode}) {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount, accounts} = useSession()
|
||||||
const did = currentAccount?.did
|
const did = currentAccount?.did
|
||||||
const currentStatsigUser = React.useMemo(() => toStatsigUser(did), [did])
|
const currentStatsigUser = React.useMemo(() => toStatsigUser(did), [did])
|
||||||
|
|
||||||
|
const otherDidsConcatenated = accounts
|
||||||
|
.map(account => account.did)
|
||||||
|
.filter(accountDid => accountDid !== did)
|
||||||
|
.join(' ') // We're only interested in DID changes.
|
||||||
|
const otherStatsigUsers = React.useMemo(
|
||||||
|
() => otherDidsConcatenated.split(' ').map(toStatsigUser),
|
||||||
|
[otherDidsConcatenated],
|
||||||
|
)
|
||||||
|
const statsigOptions = React.useMemo(
|
||||||
|
() => createStatsigOptions(otherStatsigUsers),
|
||||||
|
[otherStatsigUsers],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Have our own cache in front of Statsig.
|
||||||
|
// This ensures the results remain stable until the active DID changes.
|
||||||
const [gateCache, setGateCache] = React.useState(() => new Map())
|
const [gateCache, setGateCache] = React.useState(() => new Map())
|
||||||
const [prevDid, setPrevDid] = React.useState(did)
|
const [prevDid, setPrevDid] = React.useState(did)
|
||||||
if (did !== prevDid) {
|
if (did !== prevDid) {
|
||||||
@@ -170,15 +217,19 @@ export function Provider({children}: {children: React.ReactNode}) {
|
|||||||
setGateCache(new Map())
|
setGateCache(new Map())
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
// Periodically poll Statsig to get the current rule evaluations for all stored accounts.
|
||||||
function refresh() {
|
// These changes are prefetched and stored, but don't get applied until the active DID changes.
|
||||||
// This will not affect the current session.
|
// This ensures that when you switch an account, it already has fresh results by then.
|
||||||
// Statsig will put the results into local storage and we'll pick it up on next load.
|
const handleIntervalTick = useNonReactiveCallback(() => {
|
||||||
Statsig.updateUser(currentStatsigUser)
|
if (Statsig.initializeCalled()) {
|
||||||
|
// Note: Only first five will be taken into account by Statsig.
|
||||||
|
Statsig.prefetchUsers([currentStatsigUser, ...otherStatsigUsers])
|
||||||
}
|
}
|
||||||
const id = setInterval(refresh, 3 * 60e3 /* 3 min */)
|
})
|
||||||
|
React.useEffect(() => {
|
||||||
|
const id = setInterval(handleIntervalTick, 60e3 /* 1 min */)
|
||||||
return () => clearInterval(id)
|
return () => clearInterval(id)
|
||||||
}, [currentStatsigUser])
|
}, [handleIntervalTick])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GateCache.Provider value={gateCache}>
|
<GateCache.Provider value={gateCache}>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {jwtDecode} from 'jwt-decode'
|
|||||||
import {track} from '#/lib/analytics/analytics'
|
import {track} from '#/lib/analytics/analytics'
|
||||||
import {networkRetry} from '#/lib/async/retry'
|
import {networkRetry} from '#/lib/async/retry'
|
||||||
import {IS_TEST_USER} from '#/lib/constants'
|
import {IS_TEST_USER} from '#/lib/constants'
|
||||||
import {logEvent, LogEvents} from '#/lib/statsig/statsig'
|
import {logEvent, LogEvents, tryFetchGates} from '#/lib/statsig/statsig'
|
||||||
import {hasProp} from '#/lib/type-guards'
|
import {hasProp} from '#/lib/type-guards'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
@@ -243,6 +243,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
if (!agent.session) {
|
if (!agent.session) {
|
||||||
throw new Error(`session: createAccount failed to establish a session`)
|
throw new Error(`session: createAccount failed to establish a session`)
|
||||||
}
|
}
|
||||||
|
const fetchingGates = tryFetchGates(
|
||||||
|
agent.session.did,
|
||||||
|
'prefer-fresh-gates',
|
||||||
|
)
|
||||||
|
|
||||||
const deactivated = isSessionDeactivated(agent.session.accessJwt)
|
const deactivated = isSessionDeactivated(agent.session.accessJwt)
|
||||||
if (!deactivated) {
|
if (!deactivated) {
|
||||||
@@ -283,6 +287,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
__globalAgent = agent
|
__globalAgent = agent
|
||||||
|
await fetchingGates
|
||||||
upsertAccount(account)
|
upsertAccount(account)
|
||||||
|
|
||||||
logger.debug(`session: created account`, {}, logger.DebugContext.session)
|
logger.debug(`session: created account`, {}, logger.DebugContext.session)
|
||||||
@@ -303,6 +308,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
if (!agent.session) {
|
if (!agent.session) {
|
||||||
throw new Error(`session: login failed to establish a session`)
|
throw new Error(`session: login failed to establish a session`)
|
||||||
}
|
}
|
||||||
|
const fetchingGates = tryFetchGates(
|
||||||
|
agent.session.did,
|
||||||
|
'prefer-fresh-gates',
|
||||||
|
)
|
||||||
|
|
||||||
const account: SessionAccount = {
|
const account: SessionAccount = {
|
||||||
service: agent.service.toString(),
|
service: agent.service.toString(),
|
||||||
@@ -330,6 +339,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
__globalAgent = agent
|
__globalAgent = agent
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (IS_DEV && isWeb) window.agent = agent
|
if (IS_DEV && isWeb) window.agent = agent
|
||||||
|
await fetchingGates
|
||||||
upsertAccount(account)
|
upsertAccount(account)
|
||||||
|
|
||||||
logger.debug(`session: logged in`, {}, logger.DebugContext.session)
|
logger.debug(`session: logged in`, {}, logger.DebugContext.session)
|
||||||
@@ -362,6 +372,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
const initSession = React.useCallback<ApiContext['initSession']>(
|
const initSession = React.useCallback<ApiContext['initSession']>(
|
||||||
async account => {
|
async account => {
|
||||||
logger.debug(`session: initSession`, {}, logger.DebugContext.session)
|
logger.debug(`session: initSession`, {}, logger.DebugContext.session)
|
||||||
|
const fetchingGates = tryFetchGates(account.did, 'prefer-low-latency')
|
||||||
|
|
||||||
const agent = new BskyAgent({
|
const agent = new BskyAgent({
|
||||||
service: account.service,
|
service: account.service,
|
||||||
@@ -406,6 +417,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
|
|
||||||
agent.session = prevSession
|
agent.session = prevSession
|
||||||
__globalAgent = agent
|
__globalAgent = agent
|
||||||
|
await fetchingGates
|
||||||
upsertAccount(account)
|
upsertAccount(account)
|
||||||
|
|
||||||
if (prevSession.deactivated) {
|
if (prevSession.deactivated) {
|
||||||
@@ -442,6 +454,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
try {
|
try {
|
||||||
const freshAccount = await resumeSessionWithFreshAccount()
|
const freshAccount = await resumeSessionWithFreshAccount()
|
||||||
__globalAgent = agent
|
__globalAgent = agent
|
||||||
|
await fetchingGates
|
||||||
upsertAccount(freshAccount)
|
upsertAccount(freshAccount)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/*
|
/*
|
||||||
|
|||||||
Reference in New Issue
Block a user