Clean up growthbook code, integrate into init and sessions

This commit is contained in:
Eric Bailey
2026-01-20 11:03:26 -06:00
parent c005e02713
commit 443141d3dd
11 changed files with 355 additions and 172 deletions
+61 -58
View File
@@ -17,11 +17,15 @@ import * as Sentry from '@sentry/react-native'
import {KeyboardControllerProvider} from '#/lib/hooks/useEnableKeyboardController' import {KeyboardControllerProvider} from '#/lib/hooks/useEnableKeyboardController'
import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder' import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder'
import {QueryProvider} from '#/lib/react-query' import {QueryProvider} from '#/lib/react-query'
import {Provider as StatsigProvider, tryFetchGates} from '#/lib/statsig/statsig' import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {s} from '#/lib/styles' import {s} from '#/lib/styles'
import {ThemeProvider} from '#/lib/ThemeContext' import {ThemeProvider} from '#/lib/ThemeContext'
import I18nProvider from '#/locale/i18nProvider' import I18nProvider from '#/locale/i18nProvider'
import {logger, Provider as LoggingProvider} from '#/logger' import {logger} from '#/logger'
import {
initializer as growthbookInitializer,
Provider as GrowthBookProvider,
} from '#/logger/growthbook/context'
import {Provider as A11yProvider} from '#/state/a11y' import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as DialogStateProvider} from '#/state/dialogs'
@@ -111,10 +115,9 @@ function InnerApp() {
useEffect(() => { useEffect(() => {
async function onLaunch(account?: SessionAccount) { async function onLaunch(account?: SessionAccount) {
try { try {
await growthbookInitializer
if (account) { if (account) {
await resumeSession(account) await resumeSession(account)
} else {
await tryFetchGates(undefined, 'prefer-fresh-gates')
} }
} catch (e) { } catch (e) {
logger.error(`session: resume failed`, {message: e}) logger.error(`session: resume failed`, {message: e})
@@ -144,57 +147,59 @@ function InnerApp() {
<React.Fragment <React.Fragment
// Resets the entire tree below when it changes: // Resets the entire tree below when it changes:
key={currentAccount?.did}> key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}> <GrowthBookProvider>
<PolicyUpdateOverlayProvider> <QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider> <PolicyUpdateOverlayProvider>
<LiveEventsProvider> <StatsigProvider>
<AgeAssuranceV2Provider> <LiveEventsProvider>
<ComposerProvider> <AgeAssuranceV2Provider>
<MessagesProvider> <ComposerProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */} <MessagesProvider>
<LabelDefsProvider> {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<ModerationOptsProvider> <LabelDefsProvider>
<LoggedOutViewProvider> <ModerationOptsProvider>
<SelectedFeedProvider> <LoggedOutViewProvider>
<HiddenRepliesProvider> <SelectedFeedProvider>
<HomeBadgeProvider> <HiddenRepliesProvider>
<UnreadNotifsProvider> <HomeBadgeProvider>
<BackgroundNotificationPreferencesProvider> <UnreadNotifsProvider>
<MutedThreadsProvider> <BackgroundNotificationPreferencesProvider>
<ProgressGuideProvider> <MutedThreadsProvider>
<ServiceAccountManager> <ProgressGuideProvider>
<EmailVerificationProvider> <ServiceAccountManager>
<HideBottomBarBorderProvider> <EmailVerificationProvider>
<GestureHandlerRootView <HideBottomBarBorderProvider>
style={s.h100pct}> <GestureHandlerRootView
<GlobalGestureEventsProvider> style={s.h100pct}>
<IntentDialogProvider> <GlobalGestureEventsProvider>
<TestCtrls /> <IntentDialogProvider>
<Shell /> <TestCtrls />
<ToastOutlet /> <Shell />
</IntentDialogProvider> <ToastOutlet />
</GlobalGestureEventsProvider> </IntentDialogProvider>
</GestureHandlerRootView> </GlobalGestureEventsProvider>
</HideBottomBarBorderProvider> </GestureHandlerRootView>
</EmailVerificationProvider> </HideBottomBarBorderProvider>
</ServiceAccountManager> </EmailVerificationProvider>
</ProgressGuideProvider> </ServiceAccountManager>
</MutedThreadsProvider> </ProgressGuideProvider>
</BackgroundNotificationPreferencesProvider> </MutedThreadsProvider>
</UnreadNotifsProvider> </BackgroundNotificationPreferencesProvider>
</HomeBadgeProvider> </UnreadNotifsProvider>
</HiddenRepliesProvider> </HomeBadgeProvider>
</SelectedFeedProvider> </HiddenRepliesProvider>
</LoggedOutViewProvider> </SelectedFeedProvider>
</ModerationOptsProvider> </LoggedOutViewProvider>
</LabelDefsProvider> </ModerationOptsProvider>
</MessagesProvider> </LabelDefsProvider>
</ComposerProvider> </MessagesProvider>
</AgeAssuranceV2Provider> </ComposerProvider>
</LiveEventsProvider> </AgeAssuranceV2Provider>
</StatsigProvider> </LiveEventsProvider>
</PolicyUpdateOverlayProvider> </StatsigProvider>
</QueryProvider> </PolicyUpdateOverlayProvider>
</QueryProvider>
</GrowthBookProvider>
</React.Fragment> </React.Fragment>
</VideoVolumeProvider> </VideoVolumeProvider>
</Splash> </Splash>
@@ -238,9 +243,7 @@ function App() {
<StarterPackProvider> <StarterPackProvider>
<SafeAreaProvider <SafeAreaProvider
initialMetrics={initialWindowMetrics}> initialMetrics={initialWindowMetrics}>
<LoggingProvider> <InnerApp />
<InnerApp />
</LoggingProvider>
</SafeAreaProvider> </SafeAreaProvider>
</StarterPackProvider> </StarterPackProvider>
</BottomSheetProvider> </BottomSheetProvider>
+59 -54
View File
@@ -12,8 +12,11 @@ import {QueryProvider} from '#/lib/react-query'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig' import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {ThemeProvider} from '#/lib/ThemeContext' import {ThemeProvider} from '#/lib/ThemeContext'
import I18nProvider from '#/locale/i18nProvider' import I18nProvider from '#/locale/i18nProvider'
import {logger, Provider as LoggingProvider} from '#/logger' import {logger} from '#/logger'
import {initializer as growthbookInitializer} from '#/logger/growthbook/context' import {
initializer as growthbookInitializer,
Provider as GrowthBookProvider,
} from '#/logger/growthbook/context'
import {Provider as A11yProvider} from '#/state/a11y' import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as DialogStateProvider} from '#/state/dialogs'
@@ -56,8 +59,10 @@ import {Provider as PortalProvider} from '#/components/Portal'
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext' import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
import {ToastOutlet} from '#/components/Toast' import {ToastOutlet} from '#/components/Toast'
import {Provider as AgeAssuranceV2Provider} from '#/ageAssurance' import {
import {prefetchAgeAssuranceConfig} from '#/ageAssurance' prefetchAgeAssuranceConfig,
Provider as AgeAssuranceV2Provider,
} from '#/ageAssurance'
import { import {
prefetchLiveEvents, prefetchLiveEvents,
Provider as LiveEventsProvider, Provider as LiveEventsProvider,
@@ -121,53 +126,55 @@ function InnerApp() {
<React.Fragment <React.Fragment
// Resets the entire tree below when it changes: // Resets the entire tree below when it changes:
key={currentAccount?.did}> key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}> <GrowthBookProvider>
<PolicyUpdateOverlayProvider> <QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider> <PolicyUpdateOverlayProvider>
<LiveEventsProvider> <StatsigProvider>
<AgeAssuranceV2Provider> <LiveEventsProvider>
<ComposerProvider> <AgeAssuranceV2Provider>
<MessagesProvider> <ComposerProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */} <MessagesProvider>
<LabelDefsProvider> {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<ModerationOptsProvider> <LabelDefsProvider>
<LoggedOutViewProvider> <ModerationOptsProvider>
<SelectedFeedProvider> <LoggedOutViewProvider>
<HiddenRepliesProvider> <SelectedFeedProvider>
<HomeBadgeProvider> <HiddenRepliesProvider>
<UnreadNotifsProvider> <HomeBadgeProvider>
<BackgroundNotificationPreferencesProvider> <UnreadNotifsProvider>
<MutedThreadsProvider> <BackgroundNotificationPreferencesProvider>
<SafeAreaProvider> <MutedThreadsProvider>
<ProgressGuideProvider> <SafeAreaProvider>
<ServiceConfigProvider> <ProgressGuideProvider>
<EmailVerificationProvider> <ServiceConfigProvider>
<HideBottomBarBorderProvider> <EmailVerificationProvider>
<IntentDialogProvider> <HideBottomBarBorderProvider>
<Shell /> <IntentDialogProvider>
<ToastOutlet /> <Shell />
</IntentDialogProvider> <ToastOutlet />
</HideBottomBarBorderProvider> </IntentDialogProvider>
</EmailVerificationProvider> </HideBottomBarBorderProvider>
</ServiceConfigProvider> </EmailVerificationProvider>
</ProgressGuideProvider> </ServiceConfigProvider>
</SafeAreaProvider> </ProgressGuideProvider>
</MutedThreadsProvider> </SafeAreaProvider>
</BackgroundNotificationPreferencesProvider> </MutedThreadsProvider>
</UnreadNotifsProvider> </BackgroundNotificationPreferencesProvider>
</HomeBadgeProvider> </UnreadNotifsProvider>
</HiddenRepliesProvider> </HomeBadgeProvider>
</SelectedFeedProvider> </HiddenRepliesProvider>
</LoggedOutViewProvider> </SelectedFeedProvider>
</ModerationOptsProvider> </LoggedOutViewProvider>
</LabelDefsProvider> </ModerationOptsProvider>
</MessagesProvider> </LabelDefsProvider>
</ComposerProvider> </MessagesProvider>
</AgeAssuranceV2Provider> </ComposerProvider>
</LiveEventsProvider> </AgeAssuranceV2Provider>
</StatsigProvider> </LiveEventsProvider>
</PolicyUpdateOverlayProvider> </StatsigProvider>
</QueryProvider> </PolicyUpdateOverlayProvider>
</QueryProvider>
</GrowthBookProvider>
</React.Fragment> </React.Fragment>
</ActiveVideoProvider> </ActiveVideoProvider>
</VideoVolumeProvider> </VideoVolumeProvider>
@@ -207,9 +214,7 @@ function App() {
<LightboxStateProvider> <LightboxStateProvider>
<PortalProvider> <PortalProvider>
<StarterPackProvider> <StarterPackProvider>
<LoggingProvider> <InnerApp />
<InnerApp />
</LoggingProvider>
</StarterPackProvider> </StarterPackProvider>
</PortalProvider> </PortalProvider>
</LightboxStateProvider> </LightboxStateProvider>
@@ -4,6 +4,7 @@ import {t} from '@lingui/macro'
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants' import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
import {useGate} from '#/lib/statsig/statsig' import {useGate} from '#/lib/statsig/statsig'
import {logEvent} from '#/logger/growthbook/context'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
@@ -32,6 +33,7 @@ export function DiscoverDebug({
style={[a.absolute, {zIndex: 1000, maxWidth: 65, bottom: -4}, a.left_0]} style={[a.absolute, {zIndex: 1000, maxWidth: 65, bottom: -4}, a.left_0]}
onPress={e => { onPress={e => {
e.stopPropagation() e.stopPropagation()
logEvent('discover_debug:copy_feed_context', {feedContext})
Clipboard.setStringAsync(feedContext) Clipboard.setStringAsync(feedContext)
Toast.show(t`Copied to clipboard`) Toast.show(t`Copied to clipboard`)
}}> }}>
+4 -3
View File
@@ -87,13 +87,14 @@ export const CHAT_PROXY_DID: Did =
/** /**
* Growthbook API host * Growthbook API host
*/ */
export const GROWTHBOOK_API_HOST: string | undefined = process.env.EXPO_PUBLIC_GROWTHBOOK_API_HOST export const GROWTHBOOK_API_HOST: string | undefined =
process.env.EXPO_PUBLIC_GROWTHBOOK_API_HOST
/** /**
* Growthbook client key * Growthbook client key
*/ */
export const GROWTHBOOK_CLIENT_KEY: string | undefined = process.env.EXPO_PUBLIC_GROWTHBOOK_CLIENT_KEY export const GROWTHBOOK_CLIENT_KEY: string =
process.env.EXPO_PUBLIC_GROWTHBOOK_CLIENT_KEY || 'sdk-7gkUkGy9wguUjyFe'
/** /**
* Sentry DSN for telemetry * Sentry DSN for telemetry
+165 -43
View File
@@ -1,23 +1,57 @@
import {useMemo, useEffect} from 'react' import {useCallback, useEffect} from 'react'
import {Platform} from 'react-native' import {Platform} from 'react-native'
import {growthbookTrackingPlugin} from '@growthbook/growthbook/plugins'
import {GrowthBook, GrowthBookProvider} from '@growthbook/growthbook-react'
import {BSKY_SERVICE} from '#/lib/constants'
import { import {
GrowthBook, getAndMigrateStableId,
GrowthBookProvider, getSessionId,
useFeatureIsOn, getStableId,
} from '@growthbook/growthbook-react' getStableIdOrThrow,
} from '#/logger/growthbook/identifiers'
import * as env from '#/env'
import {useSession, SessionAccount} from '#/state/session'
import {useGeolocation} from '#/geolocation'
import * as persisted from '#/state/persisted'
import * as referrer from '#/logger/growthbook/util/referrer' import * as referrer from '#/logger/growthbook/util/referrer'
import * as persisted from '#/state/persisted'
import {type SessionAccount, useSession} from '#/state/session'
import * as env from '#/env'
import {type Geolocation, useGeolocation} from '#/geolocation'
type DefaultAttributes = { const TIMEOUT_INIT = 500 // TODO should base on p99 or something
const TIMEOUT_PREFER_LOW_LATENCY = 250
const TIMEOUT_PREFER_FRESH_GATES = 1500
/**
* We vary the amount of time we wait for GrowthBook to fetch feature
* gates based on the strategy specified.
*
* TODO examples
*/
type FeatureFetchStrategy = 'prefer-low-latency' | 'prefer-fresh-gates'
/**
* These are fields that are handled specially by GrowthBook
*/
type GrowthBookDefaultAttributes = {
/** Special GrowthBook field */
device_id: string
/** Special GrowthBook field */
session_id: string
}
/**
* These are user fields that are handled specially by GrowthBook
*/
type GrowthBookDefaultUserAttributes = {
/** Special GrowthBook field */
user_id: string
}
type DefaultAttributes = GrowthBookDefaultAttributes & {
/** Custom field provided by our Geolocation context */
country: string country: string
} }
type UserAttributes = { type UserAttributes = GrowthBookDefaultUserAttributes & {
id: string // do not use `id`, GrowthBook will think it's the same as `device_id`
pds: string | undefined did: string
isBskyPds: boolean
platform: string platform: string
appVersion: string appVersion: string
bundleIdentifier: string bundleIdentifier: string
@@ -27,71 +61,158 @@ type UserAttributes = {
appLanguage: string appLanguage: string
contentLanguages: string[] contentLanguages: string[]
} }
type Attributes = DefaultAttributes & UserAttributes
/**
* We cache the geolocation outside of React to use when setting
* default attributes outside React, such as during `refresh()`.
*/
let unsafeGeolocation: Geolocation | null = null
const gb = new GrowthBook({ const gb = new GrowthBook({
apiHost: env.GROWTHBOOK_API_HOST, apiHost: env.GROWTHBOOK_API_HOST,
clientKey: env.GROWTHBOOK_CLIENT_KEY, clientKey: env.GROWTHBOOK_CLIENT_KEY,
plugins: [growthbookTrackingPlugin()],
trackingCallback: (experiment, result) => { trackingCallback: (experiment, result) => {
// TODO
console.log('Experiment Viewed', { console.log('Experiment Viewed', {
experimentId: experiment.key, experimentId: experiment.key,
variationId: result.key, variationId: result.key,
}) })
gb.logEvent('Experiment Viewed', {
experimentId: experiment.key,
variationId: result.key,
})
}, },
attributes: {
device_id: getStableId() || 'unset',
session_id: getSessionId(),
} satisfies GrowthBookDefaultAttributes,
}) })
export const initializer = gb.init({ /**
timeout: 1e3, * Initializer promise that must be awaited before using the GrowthBook
* instance or rendering the `Provider`. Note: this may not be fully
* initialized if it takes longer than `TIMEOUT_INIT` to initialize. In that
* case, we may see a flash of uncustomized content until the initialization
* completes.
*/
export const initializer = new Promise<void>(async y => {
/*
* This _must_ happen first to ensure continuity of the device ID from
* StatSig to GrowthBook
*/
const id = await getAndMigrateStableId()
const attr: GrowthBookDefaultAttributes = {
device_id: id,
session_id: getSessionId(),
}
gb.setAttributes(attr)
await gb.init({timeout: TIMEOUT_INIT})
y()
}) })
export function useGate(gate: string): boolean { /**
return useFeatureIsOn(gate) * Refresh feature gates from GrowthBook. Updates attributes based on the
* provided account, if any.
*/
export async function refresh({
account,
strategy,
}: {
account?: SessionAccount
strategy: FeatureFetchStrategy
}) {
setAttributesForAccount(account)
await gb.refreshFeatures({
timeout:
strategy === 'prefer-low-latency'
? TIMEOUT_PREFER_LOW_LATENCY
: TIMEOUT_PREFER_FRESH_GATES,
})
} }
/**
* Log a custom event to our backend, using GrowthBook's event logging system.
*/
export function logEvent(eventName: string, metadata?: Record<string, any>) {
gb.logEvent(eventName, metadata)
}
/**
* Hook to check if a feature gate is enabled
*/
export function useGate() {
return useCallback((gate: string): boolean => {
return gb.isOn(gate)
}, [])
}
/**
* Main provider for GrowthBook and feature flag context. Should be rendered
* _after_ `initializer` is complete, and _after_ the Session provider.
*/
export function Provider({children}: {children: React.ReactNode}) { export function Provider({children}: {children: React.ReactNode}) {
const geo = useGeolocation() const geo = useGeolocation()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const defaultAttributes = useMemo<DefaultAttributes>(
() => ({
country: geo.countryCode || 'unknown',
}),
[geo],
)
/** /**
* Decorate existing attributes with any new default attributes * Decorate existing attributes with any new default attributes
*/ */
useEffect(() => { useEffect(() => {
// cache outside react
unsafeGeolocation = geo
const attr = { const attr = {
...gb.getAttributes(), ...gb.getAttributes(),
...defaultAttributes, ...getDefaultAttributes(),
} }
gb.setAttributes(attr) gb.setAttributes(attr)
console.debug(`update attributes`, {attributes: attr}) console.debug(`update attributes`, {attributes: attr})
}, [defaultAttributes]) }, [geo])
/** /**
* Update user attributes on session change, and clear them on logout * Update user attributes on session change, and clear them on logout
*/ */
useEffect(() => { useEffect(() => {
if (currentAccount) { setAttributesForAccount(currentAccount)
const attr = getUserAttributes(currentAccount) }, [currentAccount])
gb.setAttributes({
...defaultAttributes,
...getUserAttributes(currentAccount),
})
console.debug(`has session, set attributes`, {attributes: attr})
} else {
gb.setAttributes(defaultAttributes)
console.debug(`no session, reset attributes`, {
attributes: defaultAttributes,
})
}
}, [defaultAttributes, currentAccount])
return <GrowthBookProvider growthbook={gb}>{children}</GrowthBookProvider> return <GrowthBookProvider growthbook={gb}>{children}</GrowthBookProvider>
} }
/**
* Get the default attributes that should always be set
* on the GrowthBook instance
*/
function getDefaultAttributes() {
return {
device_id: getStableIdOrThrow(),
session_id: getSessionId(),
country: unsafeGeolocation?.countryCode || 'unknown',
}
}
/**
* Set attributes on the global GrowthBook instance. If an account is provided,
* set user attributes as well. Otherwise, clear user attributes.
*/
function setAttributesForAccount(account?: SessionAccount) {
if (account) {
const attr: Attributes = {
...getDefaultAttributes(),
...(getUserAttributes(account) || {}),
}
gb.setAttributes(attr)
console.debug(`setAttributesForAccount: has account`, {attributes: attr})
} else {
const attr = getDefaultAttributes()
gb.setAttributes(attr)
console.debug(`setAttributesForAccount: no account`, {attributes: attr})
}
}
/**
* Converts a SessionAccount into user attributes for GrowthBook
*/
export function getUserAttributes(account: SessionAccount): UserAttributes export function getUserAttributes(account: SessionAccount): UserAttributes
export function getUserAttributes(account: undefined): null export function getUserAttributes(account: undefined): null
export function getUserAttributes( export function getUserAttributes(
@@ -100,8 +221,9 @@ export function getUserAttributes(
if (!account) return null if (!account) return null
const languagePrefs = persisted.get('languagePrefs') const languagePrefs = persisted.get('languagePrefs')
return { return {
id: account.did, user_id: account.did,
pds: account.pdsUrl, did: account.did,
isBskyPds: account.service.startsWith(BSKY_SERVICE),
platform: Platform.OS, platform: Platform.OS,
appVersion: env.RELEASE_VERSION, appVersion: env.RELEASE_VERSION,
bundleIdentifier: env.BUNDLE_IDENTIFIER, bundleIdentifier: env.BUNDLE_IDENTIFIER,
@@ -0,0 +1,24 @@
import uuid from 'react-native-uuid'
import AsyncStorage from '@react-native-async-storage/async-storage'
import {device} from '#/storage'
const LEGACY_STABLE_ID = 'STATSIG_LOCAL_STORAGE_STABLE_ID'
export async function getAndMigrateStableId() {
const id = (await AsyncStorage.getItem(LEGACY_STABLE_ID)) || uuid.v4()
device.set(['stableId'], id)
return id
}
export function getStableId() {
return device.get(['stableId'])
}
export function getStableIdOrThrow() {
const id = device.get(['stableId'])
if (!id) {
throw new Error(`stableId is not set, call getAndMigrateStableId first`)
}
return id
}
@@ -0,0 +1,10 @@
import uuid from 'react-native-uuid'
export * from '#/logger/growthbook/identifiers/common'
// TODO probably want to clear this
const sessionId = uuid.v4()
/**
* Stable session ID, persisted for the duration of the user's session
*/
export const getSessionId = () => sessionId
@@ -0,0 +1,15 @@
import uuid from 'react-native-uuid'
export * from '#/logger/growthbook/identifiers/common'
/**
* Stable session ID, persisted for the duration of the user's session
*/
export const getSessionId = () => {
let id = sessionStorage.getItem('BSKY_SESSION_ID')
if (!id) {
id = uuid.v4()
sessionStorage.setItem('BSKY_SESSION_ID', id)
}
return id
}
-8
View File
@@ -1,6 +1,5 @@
import {nanoid} from 'nanoid/non-secure' import {nanoid} from 'nanoid/non-secure'
import {Provider as GrowthbookProvider} from '#/logger/growthbook/context'
import {logEvent} from '#/lib/statsig/statsig' import {logEvent} from '#/lib/statsig/statsig'
import {add} from '#/logger/logDump' import {add} from '#/logger/logDump'
import {type MetricEvents} from '#/logger/metrics' import {type MetricEvents} from '#/logger/metrics'
@@ -172,10 +171,3 @@ export class Logger {
* `logger.error(error[, metadata])` * `logger.error(error[, metadata])`
*/ */
export const logger = Logger.create(Logger.Context.Default) export const logger = Logger.create(Logger.Context.Default)
/**
* Logger context provider
*/
export function Provider({children}: {children: React.ReactNode}) {
return <GrowthbookProvider>{children}</GrowthbookProvider>
}
+9 -6
View File
@@ -22,9 +22,9 @@ import {
PUBLIC_BSKY_SERVICE, PUBLIC_BSKY_SERVICE,
TIMELINE_SAVED_FEED, TIMELINE_SAVED_FEED,
} from '#/lib/constants' } from '#/lib/constants'
import {tryFetchGates} from '#/lib/statsig/statsig'
import {getAge} from '#/lib/strings/time' import {getAge} from '#/lib/strings/time'
import {logger} from '#/logger' import {logger} from '#/logger'
import {refresh as refreshGates} from '#/logger/growthbook/context'
import {snoozeBirthdateUpdateAllowedForDid} from '#/state/birthdate' import {snoozeBirthdateUpdateAllowedForDid} from '#/state/birthdate'
import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders' import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders'
import { import {
@@ -63,7 +63,10 @@ export async function createAgentAndResume(
if (storedAccount.pdsUrl) { if (storedAccount.pdsUrl) {
agent.sessionManager.pdsUrl = new URL(storedAccount.pdsUrl) agent.sessionManager.pdsUrl = new URL(storedAccount.pdsUrl)
} }
const gates = tryFetchGates(storedAccount.did, 'prefer-low-latency') const gates = refreshGates({
account: storedAccount,
strategy: 'prefer-low-latency',
})
const moderation = configureModerationForAccount(agent, storedAccount) const moderation = configureModerationForAccount(agent, storedAccount)
const prevSession: AtpSessionData = sessionAccountToSession(storedAccount) const prevSession: AtpSessionData = sessionAccountToSession(storedAccount)
if (isSessionExpired(storedAccount)) { if (isSessionExpired(storedAccount)) {
@@ -123,7 +126,7 @@ export async function createAgentAndLogin(
}) })
const account = agentToSessionAccountOrThrow(agent) const account = agentToSessionAccountOrThrow(agent)
const gates = tryFetchGates(account.did, 'prefer-fresh-gates') const gates = refreshGates({account, strategy: 'prefer-fresh-gates'})
const moderation = configureModerationForAccount(agent, account) const moderation = configureModerationForAccount(agent, account)
const aa = prefetchAgeAssuranceData({agent}) const aa = prefetchAgeAssuranceData({agent})
@@ -171,7 +174,7 @@ export async function createAgentAndCreateAccount(
verificationCode, verificationCode,
}) })
const account = agentToSessionAccountOrThrow(agent) const account = agentToSessionAccountOrThrow(agent)
const gates = tryFetchGates(account.did, 'prefer-fresh-gates') const gates = refreshGates({account, strategy: 'prefer-fresh-gates'})
const moderation = configureModerationForAccount(agent, account) const moderation = configureModerationForAccount(agent, account)
const createdAt = new Date().toISOString() const createdAt = new Date().toISOString()
@@ -322,7 +325,7 @@ export function agentToSessionAccount(
return undefined return undefined
} }
return { return {
service: agent.service.toString(), service: agent.serviceUrl.toString(),
did: agent.session.did, did: agent.session.did,
handle: agent.session.handle, handle: agent.session.handle,
email: agent.session.email, email: agent.session.email,
@@ -332,7 +335,7 @@ export function agentToSessionAccount(
accessJwt: agent.session.accessJwt, accessJwt: agent.session.accessJwt,
signupQueued: isSignupQueued(agent.session.accessJwt), signupQueued: isSignupQueued(agent.session.accessJwt),
active: agent.session.active, active: agent.session.active,
status: agent.session.status as SessionAccount['status'], status: agent.session.status,
pdsUrl: agent.pdsUrl?.toString(), pdsUrl: agent.pdsUrl?.toString(),
isSelfHosted: !agent.serviceUrl.toString().startsWith(BSKY_SERVICE), isSelfHosted: !agent.serviceUrl.toString().startsWith(BSKY_SERVICE),
} }
+6
View File
@@ -5,6 +5,12 @@ import {type Geolocation} from '#/geolocation/types'
* Device data that's specific to the device and does not vary based account * Device data that's specific to the device and does not vary based account
*/ */
export type Device = { export type Device = {
/**
* Formerly managed by StatSig, this is the migrated stable ID for the
* device, used with our logging and metrics tracking.
*/
stableId: string | undefined
fontScale: '-2' | '-1' | '0' | '1' | '2' fontScale: '-2' | '-1' | '0' | '1' | '2'
fontFamily: 'system' | 'theme' fontFamily: 'system' | 'theme'
lastNuxDialog: string | undefined lastNuxDialog: string | undefined