Add setting for opting in to beta features (#11123)

This commit is contained in:
DS Boyce
2026-07-15 06:06:54 -07:00
committed by GitHub
parent d7f40b7e7f
commit 3c5c11c002
20 changed files with 685 additions and 90 deletions
+92 -5
View File
@@ -1,8 +1,11 @@
import {MMKV} from '@bsky.app/react-native-mmkv'
import {setPolyfills} from '@growthbook/growthbook'
import {GrowthBook} from '@growthbook/growthbook-react'
import {type I18n} from '@lingui/core'
import {msg} from '@lingui/core/macro'
import {Logger} from '#/logger'
import {Features} from '#/analytics/features/types'
import {getNavigationMetadata, type Metadata} from '#/analytics/metadata'
import * as env from '#/env'
@@ -11,12 +14,14 @@ export {Features} from '#/analytics/features/types'
const logger = Logger.create(Logger.Context.Growthbook)
const CACHE = new MMKV({id: 'bsky_features_cache'})
const BETA_USER_ATTRIBUTE = 'isBetaUser'
setPolyfills({
localStorage: {
getItem: key => {
return CACHE.getString(key) ?? null
},
setItem: async (key, value) => {
setItem: (key, value) => {
CACHE.set(key, value)
},
},
@@ -44,15 +49,13 @@ export const features = new GrowthBook({
* that case, we may see a flash of uncustomized content until the
* initialization completes.
*/
export const init = new Promise<void>(async y => {
const res = await features.init({timeout: TIMEOUT_INIT})
export const init = features.init({timeout: TIMEOUT_INIT}).then(res => {
if (!res.success) {
logger.warn('GrowthBook initialization failed or timed out', {
source: res.source,
safeMessage: res.error?.toString(),
})
}
y()
})
/**
@@ -68,6 +71,89 @@ export async function refresh({strategy}: {strategy: FeatureFetchStrategy}) {
})
}
export function getFeatures() {
return features.getFeatures()
}
export function getFeatureDescription(feature: Features, i18n: I18n) {
switch (feature) {
case Features.PostThreadKnownLikersEnable:
return {
key: feature,
name: i18n._(
msg({
message: 'Social proofing on posts',
comment: 'Name for a feature flag',
}),
),
description: i18n._(
msg({
message: 'Spot posts your friends and follows have liked.',
comment: 'Description of a feature flag (Social proofing on posts)',
}),
),
}
default:
return null
}
}
/**
* Walks a GrowthBook condition tree to determine whether it targets the given
* attribute. Conditions can nest via the logical operators `$and`, `$or`,
* `$nor` (arrays of sub-conditions) and `$not` (a single sub-condition), so a
* flat scan of the top-level keys would miss e.g.
* `{$and: [{isBetaUser: true}, ...]}`. Dot-notation access (e.g.
* `isBetaUser.foo`) counts as targeting the attribute as well.
*/
function conditionTargetsAttribute(
condition: unknown,
attribute: string,
): boolean {
if (!condition || typeof condition !== 'object') return false
for (const [key, value] of Object.entries(condition)) {
if (key === attribute || key.startsWith(`${attribute}.`)) return true
if (key === '$and' || key === '$or' || key === '$nor') {
if (
Array.isArray(value) &&
value.some(sub => conditionTargetsAttribute(sub, attribute))
) {
return true
}
} else if (key === '$not') {
if (conditionTargetsAttribute(value, attribute)) return true
}
}
return false
}
export function getTargetedFeatures(i18n: I18n) {
const allFeatures = features.getFeatures()
const targetedFeatures: {key: Features; name: string; description: string}[] =
[]
for (const [featureKey, feature] of Object.entries(allFeatures)) {
// Check if the feature contains any rules
if (!feature.rules) continue
// Determine if any rule targets the beta user attribute
const hasTargeting = feature.rules.some(rule =>
conditionTargetsAttribute(rule.condition, BETA_USER_ATTRIBUTE),
)
if (hasTargeting) {
const featureName = getFeatureDescription(featureKey as Features, i18n)
if (featureName) {
targetedFeatures.push(featureName)
}
}
}
return targetedFeatures
}
/**
* Converts our metadata into GrowthBook attributes and sets them. GrowthBook
* attributes are manually configured in the GrowthBook dashboard. So these
@@ -80,7 +166,7 @@ export function setAttributes({
session,
preferences,
}: Metadata) {
features.setAttributes({
void features.setAttributes({
deviceId: base.deviceId,
sessionId: base.sessionId,
platform: base.platform,
@@ -92,5 +178,6 @@ export function setAttributes({
appLanguage: preferences?.appLanguage,
contentLanguages: preferences?.contentLanguages,
currentScreen: getNavigationMetadata()?.currentScreen,
isBetaUser: base.isBetaUser,
})
}
+4
View File
@@ -1,3 +1,7 @@
/**
* If a feature is in the beta program, be sure to add a localized description
* for it via getFeatureDescription().
*/
export enum Features {
// core flags
IsBskyTeam = 'is_bsky_team',