Clean up growthbook code, integrate into init and sessions
This commit is contained in:
+61
-58
@@ -17,11 +17,15 @@ import * as Sentry from '@sentry/react-native'
|
||||
import {KeyboardControllerProvider} from '#/lib/hooks/useEnableKeyboardController'
|
||||
import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder'
|
||||
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 {ThemeProvider} from '#/lib/ThemeContext'
|
||||
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 MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
@@ -111,10 +115,9 @@ function InnerApp() {
|
||||
useEffect(() => {
|
||||
async function onLaunch(account?: SessionAccount) {
|
||||
try {
|
||||
await growthbookInitializer
|
||||
if (account) {
|
||||
await resumeSession(account)
|
||||
} else {
|
||||
await tryFetchGates(undefined, 'prefer-fresh-gates')
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`session: resume failed`, {message: e})
|
||||
@@ -144,57 +147,59 @@ function InnerApp() {
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<PolicyUpdateOverlayProvider>
|
||||
<StatsigProvider>
|
||||
<LiveEventsProvider>
|
||||
<AgeAssuranceV2Provider>
|
||||
<ComposerProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceAccountManager>
|
||||
<EmailVerificationProvider>
|
||||
<HideBottomBarBorderProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<GlobalGestureEventsProvider>
|
||||
<IntentDialogProvider>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<ToastOutlet />
|
||||
</IntentDialogProvider>
|
||||
</GlobalGestureEventsProvider>
|
||||
</GestureHandlerRootView>
|
||||
</HideBottomBarBorderProvider>
|
||||
</EmailVerificationProvider>
|
||||
</ServiceAccountManager>
|
||||
</ProgressGuideProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</ComposerProvider>
|
||||
</AgeAssuranceV2Provider>
|
||||
</LiveEventsProvider>
|
||||
</StatsigProvider>
|
||||
</PolicyUpdateOverlayProvider>
|
||||
</QueryProvider>
|
||||
<GrowthBookProvider>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<PolicyUpdateOverlayProvider>
|
||||
<StatsigProvider>
|
||||
<LiveEventsProvider>
|
||||
<AgeAssuranceV2Provider>
|
||||
<ComposerProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceAccountManager>
|
||||
<EmailVerificationProvider>
|
||||
<HideBottomBarBorderProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<GlobalGestureEventsProvider>
|
||||
<IntentDialogProvider>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<ToastOutlet />
|
||||
</IntentDialogProvider>
|
||||
</GlobalGestureEventsProvider>
|
||||
</GestureHandlerRootView>
|
||||
</HideBottomBarBorderProvider>
|
||||
</EmailVerificationProvider>
|
||||
</ServiceAccountManager>
|
||||
</ProgressGuideProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</ComposerProvider>
|
||||
</AgeAssuranceV2Provider>
|
||||
</LiveEventsProvider>
|
||||
</StatsigProvider>
|
||||
</PolicyUpdateOverlayProvider>
|
||||
</QueryProvider>
|
||||
</GrowthBookProvider>
|
||||
</React.Fragment>
|
||||
</VideoVolumeProvider>
|
||||
</Splash>
|
||||
@@ -238,9 +243,7 @@ function App() {
|
||||
<StarterPackProvider>
|
||||
<SafeAreaProvider
|
||||
initialMetrics={initialWindowMetrics}>
|
||||
<LoggingProvider>
|
||||
<InnerApp />
|
||||
</LoggingProvider>
|
||||
<InnerApp />
|
||||
</SafeAreaProvider>
|
||||
</StarterPackProvider>
|
||||
</BottomSheetProvider>
|
||||
|
||||
+59
-54
@@ -12,8 +12,11 @@ import {QueryProvider} from '#/lib/react-query'
|
||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||
import I18nProvider from '#/locale/i18nProvider'
|
||||
import {logger, Provider as LoggingProvider} from '#/logger'
|
||||
import {initializer as growthbookInitializer} from '#/logger/growthbook/context'
|
||||
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 MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
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 VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {ToastOutlet} from '#/components/Toast'
|
||||
import {Provider as AgeAssuranceV2Provider} from '#/ageAssurance'
|
||||
import {prefetchAgeAssuranceConfig} from '#/ageAssurance'
|
||||
import {
|
||||
prefetchAgeAssuranceConfig,
|
||||
Provider as AgeAssuranceV2Provider,
|
||||
} from '#/ageAssurance'
|
||||
import {
|
||||
prefetchLiveEvents,
|
||||
Provider as LiveEventsProvider,
|
||||
@@ -121,53 +126,55 @@ function InnerApp() {
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<PolicyUpdateOverlayProvider>
|
||||
<StatsigProvider>
|
||||
<LiveEventsProvider>
|
||||
<AgeAssuranceV2Provider>
|
||||
<ComposerProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceConfigProvider>
|
||||
<EmailVerificationProvider>
|
||||
<HideBottomBarBorderProvider>
|
||||
<IntentDialogProvider>
|
||||
<Shell />
|
||||
<ToastOutlet />
|
||||
</IntentDialogProvider>
|
||||
</HideBottomBarBorderProvider>
|
||||
</EmailVerificationProvider>
|
||||
</ServiceConfigProvider>
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</ComposerProvider>
|
||||
</AgeAssuranceV2Provider>
|
||||
</LiveEventsProvider>
|
||||
</StatsigProvider>
|
||||
</PolicyUpdateOverlayProvider>
|
||||
</QueryProvider>
|
||||
<GrowthBookProvider>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<PolicyUpdateOverlayProvider>
|
||||
<StatsigProvider>
|
||||
<LiveEventsProvider>
|
||||
<AgeAssuranceV2Provider>
|
||||
<ComposerProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceConfigProvider>
|
||||
<EmailVerificationProvider>
|
||||
<HideBottomBarBorderProvider>
|
||||
<IntentDialogProvider>
|
||||
<Shell />
|
||||
<ToastOutlet />
|
||||
</IntentDialogProvider>
|
||||
</HideBottomBarBorderProvider>
|
||||
</EmailVerificationProvider>
|
||||
</ServiceConfigProvider>
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</ComposerProvider>
|
||||
</AgeAssuranceV2Provider>
|
||||
</LiveEventsProvider>
|
||||
</StatsigProvider>
|
||||
</PolicyUpdateOverlayProvider>
|
||||
</QueryProvider>
|
||||
</GrowthBookProvider>
|
||||
</React.Fragment>
|
||||
</ActiveVideoProvider>
|
||||
</VideoVolumeProvider>
|
||||
@@ -207,9 +214,7 @@ function App() {
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<StarterPackProvider>
|
||||
<LoggingProvider>
|
||||
<InnerApp />
|
||||
</LoggingProvider>
|
||||
<InnerApp />
|
||||
</StarterPackProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {t} from '@lingui/macro'
|
||||
|
||||
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {logEvent} from '#/logger/growthbook/context'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
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]}
|
||||
onPress={e => {
|
||||
e.stopPropagation()
|
||||
logEvent('discover_debug:copy_feed_context', {feedContext})
|
||||
Clipboard.setStringAsync(feedContext)
|
||||
Toast.show(t`Copied to clipboard`)
|
||||
}}>
|
||||
|
||||
Vendored
+4
-3
@@ -87,13 +87,14 @@ export const CHAT_PROXY_DID: Did =
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
|
||||
@@ -1,23 +1,57 @@
|
||||
import {useMemo, useEffect} from 'react'
|
||||
import {useCallback, useEffect} from 'react'
|
||||
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 {
|
||||
GrowthBook,
|
||||
GrowthBookProvider,
|
||||
useFeatureIsOn,
|
||||
} from '@growthbook/growthbook-react'
|
||||
|
||||
import * as env from '#/env'
|
||||
import {useSession, SessionAccount} from '#/state/session'
|
||||
import {useGeolocation} from '#/geolocation'
|
||||
import * as persisted from '#/state/persisted'
|
||||
getAndMigrateStableId,
|
||||
getSessionId,
|
||||
getStableId,
|
||||
getStableIdOrThrow,
|
||||
} from '#/logger/growthbook/identifiers'
|
||||
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
|
||||
}
|
||||
type UserAttributes = {
|
||||
id: string
|
||||
pds: string | undefined
|
||||
type UserAttributes = GrowthBookDefaultUserAttributes & {
|
||||
// do not use `id`, GrowthBook will think it's the same as `device_id`
|
||||
did: string
|
||||
isBskyPds: boolean
|
||||
platform: string
|
||||
appVersion: string
|
||||
bundleIdentifier: string
|
||||
@@ -27,71 +61,158 @@ type UserAttributes = {
|
||||
appLanguage: 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({
|
||||
apiHost: env.GROWTHBOOK_API_HOST,
|
||||
clientKey: env.GROWTHBOOK_CLIENT_KEY,
|
||||
plugins: [growthbookTrackingPlugin()],
|
||||
trackingCallback: (experiment, result) => {
|
||||
// TODO
|
||||
console.log('Experiment Viewed', {
|
||||
experimentId: experiment.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}) {
|
||||
const geo = useGeolocation()
|
||||
const {currentAccount} = useSession()
|
||||
const defaultAttributes = useMemo<DefaultAttributes>(
|
||||
() => ({
|
||||
country: geo.countryCode || 'unknown',
|
||||
}),
|
||||
[geo],
|
||||
)
|
||||
|
||||
/**
|
||||
* Decorate existing attributes with any new default attributes
|
||||
*/
|
||||
useEffect(() => {
|
||||
// cache outside react
|
||||
unsafeGeolocation = geo
|
||||
const attr = {
|
||||
...gb.getAttributes(),
|
||||
...defaultAttributes,
|
||||
...getDefaultAttributes(),
|
||||
}
|
||||
gb.setAttributes(attr)
|
||||
console.debug(`update attributes`, {attributes: attr})
|
||||
}, [defaultAttributes])
|
||||
}, [geo])
|
||||
|
||||
/**
|
||||
* Update user attributes on session change, and clear them on logout
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (currentAccount) {
|
||||
const attr = getUserAttributes(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])
|
||||
setAttributesForAccount(currentAccount)
|
||||
}, [currentAccount])
|
||||
|
||||
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: undefined): null
|
||||
export function getUserAttributes(
|
||||
@@ -100,8 +221,9 @@ export function getUserAttributes(
|
||||
if (!account) return null
|
||||
const languagePrefs = persisted.get('languagePrefs')
|
||||
return {
|
||||
id: account.did,
|
||||
pds: account.pdsUrl,
|
||||
user_id: account.did,
|
||||
did: account.did,
|
||||
isBskyPds: account.service.startsWith(BSKY_SERVICE),
|
||||
platform: Platform.OS,
|
||||
appVersion: env.RELEASE_VERSION,
|
||||
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
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {Provider as GrowthbookProvider} from '#/logger/growthbook/context'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {add} from '#/logger/logDump'
|
||||
import {type MetricEvents} from '#/logger/metrics'
|
||||
@@ -172,10 +171,3 @@ export class Logger {
|
||||
* `logger.error(error[, metadata])`
|
||||
*/
|
||||
export const logger = Logger.create(Logger.Context.Default)
|
||||
|
||||
/**
|
||||
* Logger context provider
|
||||
*/
|
||||
export function Provider({children}: {children: React.ReactNode}) {
|
||||
return <GrowthbookProvider>{children}</GrowthbookProvider>
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ import {
|
||||
PUBLIC_BSKY_SERVICE,
|
||||
TIMELINE_SAVED_FEED,
|
||||
} from '#/lib/constants'
|
||||
import {tryFetchGates} from '#/lib/statsig/statsig'
|
||||
import {getAge} from '#/lib/strings/time'
|
||||
import {logger} from '#/logger'
|
||||
import {refresh as refreshGates} from '#/logger/growthbook/context'
|
||||
import {snoozeBirthdateUpdateAllowedForDid} from '#/state/birthdate'
|
||||
import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders'
|
||||
import {
|
||||
@@ -63,7 +63,10 @@ export async function createAgentAndResume(
|
||||
if (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 prevSession: AtpSessionData = sessionAccountToSession(storedAccount)
|
||||
if (isSessionExpired(storedAccount)) {
|
||||
@@ -123,7 +126,7 @@ export async function createAgentAndLogin(
|
||||
})
|
||||
|
||||
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 aa = prefetchAgeAssuranceData({agent})
|
||||
|
||||
@@ -171,7 +174,7 @@ export async function createAgentAndCreateAccount(
|
||||
verificationCode,
|
||||
})
|
||||
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 createdAt = new Date().toISOString()
|
||||
@@ -322,7 +325,7 @@ export function agentToSessionAccount(
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
service: agent.service.toString(),
|
||||
service: agent.serviceUrl.toString(),
|
||||
did: agent.session.did,
|
||||
handle: agent.session.handle,
|
||||
email: agent.session.email,
|
||||
@@ -332,7 +335,7 @@ export function agentToSessionAccount(
|
||||
accessJwt: agent.session.accessJwt,
|
||||
signupQueued: isSignupQueued(agent.session.accessJwt),
|
||||
active: agent.session.active,
|
||||
status: agent.session.status as SessionAccount['status'],
|
||||
status: agent.session.status,
|
||||
pdsUrl: agent.pdsUrl?.toString(),
|
||||
isSelfHosted: !agent.serviceUrl.toString().startsWith(BSKY_SERVICE),
|
||||
}
|
||||
|
||||
@@ -5,6 +5,12 @@ import {type Geolocation} from '#/geolocation/types'
|
||||
* Device data that's specific to the device and does not vary based account
|
||||
*/
|
||||
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'
|
||||
fontFamily: 'system' | 'theme'
|
||||
lastNuxDialog: string | undefined
|
||||
|
||||
Reference in New Issue
Block a user