Clean up growthbook code, integrate into init and sessions
This commit is contained in:
+9
-6
@@ -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,6 +147,7 @@ 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}>
|
||||||
|
<GrowthBookProvider>
|
||||||
<QueryProvider currentDid={currentAccount?.did}>
|
<QueryProvider currentDid={currentAccount?.did}>
|
||||||
<PolicyUpdateOverlayProvider>
|
<PolicyUpdateOverlayProvider>
|
||||||
<StatsigProvider>
|
<StatsigProvider>
|
||||||
@@ -195,6 +199,7 @@ function InnerApp() {
|
|||||||
</StatsigProvider>
|
</StatsigProvider>
|
||||||
</PolicyUpdateOverlayProvider>
|
</PolicyUpdateOverlayProvider>
|
||||||
</QueryProvider>
|
</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>
|
||||||
|
|||||||
+11
-6
@@ -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,6 +126,7 @@ 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}>
|
||||||
|
<GrowthBookProvider>
|
||||||
<QueryProvider currentDid={currentAccount?.did}>
|
<QueryProvider currentDid={currentAccount?.did}>
|
||||||
<PolicyUpdateOverlayProvider>
|
<PolicyUpdateOverlayProvider>
|
||||||
<StatsigProvider>
|
<StatsigProvider>
|
||||||
@@ -168,6 +174,7 @@ function InnerApp() {
|
|||||||
</StatsigProvider>
|
</StatsigProvider>
|
||||||
</PolicyUpdateOverlayProvider>
|
</PolicyUpdateOverlayProvider>
|
||||||
</QueryProvider>
|
</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`)
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
Vendored
+4
-3
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user