import { isEqual } from 'lodash-es' import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' import { logForDebugging } from '../../utils/debug.js' import { logError } from '../../utils/log.js' import { createSignal } from '../../utils/signal.js' type GrowthBookRefreshListener = () => void | Promise // Subscribers use this to react to local cache / override changes. const refreshed = createSignal() function callSafe(listener: GrowthBookRefreshListener): void { try { void Promise.resolve(listener()).catch(e => { logError(e) }) } catch (e) { logError(e) } } function hasAnyCachedGrowthBookFeatures(): boolean { try { return Object.keys(getGlobalConfig().cachedGrowthBookFeatures ?? {}) .length > 0 } catch { return false } } /** * Register a callback to fire when GrowthBook values change. * This is now backed by local cache / override changes only. */ export function onGrowthBookRefresh( listener: GrowthBookRefreshListener, ): () => void { let subscribed = true const unsubscribe = refreshed.subscribe(() => callSafe(listener)) if (hasAnyCachedGrowthBookFeatures()) { queueMicrotask(() => { if (subscribed) { callSafe(listener) } }) } return () => { subscribed = false unsubscribe() } } /** * Parse env var overrides for GrowthBook features. * These bypass disk cache and are used by eval harnesses / tests. */ let envOverrides: Record | null = null let envOverridesParsed = false function getEnvOverrides(): Record | null { if (!envOverridesParsed) { envOverridesParsed = true if (process.env.USER_TYPE === 'ant') { const raw = process.env.CLAUDE_INTERNAL_FC_OVERRIDES if (raw) { try { envOverrides = JSON.parse(raw) as Record logForDebugging( `GrowthBook: Using env var overrides for ${Object.keys(envOverrides).length} features: ${Object.keys(envOverrides).join(', ')}`, ) } catch { logError( new Error( `GrowthBook: Failed to parse CLAUDE_INTERNAL_FC_OVERRIDES: ${raw}`, ), ) } } } } return envOverrides } /** * Check if a feature has an env-var override (CLAUDE_INTERNAL_FC_OVERRIDES). */ export function hasGrowthBookEnvOverride(feature: string): boolean { const overrides = getEnvOverrides() return overrides !== null && feature in overrides } /** * Local config overrides set via /config Gates tab (ant-only). */ function getConfigOverrides(): Record | undefined { if (process.env.USER_TYPE !== 'ant') return undefined try { return getGlobalConfig().growthBookOverrides } catch { return undefined } } function getCachedGrowthBookFeature(feature: string): T | undefined { try { const cached = getGlobalConfig().cachedGrowthBookFeatures?.[feature] return cached !== undefined ? (cached as T) : undefined } catch { return undefined } } function getCachedStatsigGate(gate: string): boolean | undefined { try { const cached = getGlobalConfig().cachedStatsigGates?.[gate] return cached !== undefined ? Boolean(cached) : undefined } catch { return undefined } } /** * Enumerate all known GrowthBook features from the local cache. */ export function getAllGrowthBookFeatures(): Record { try { return getGlobalConfig().cachedGrowthBookFeatures ?? {} } catch { return {} } } export function getGrowthBookConfigOverrides(): Record { return getConfigOverrides() ?? {} } /** * Set or clear a single config override. Pass undefined to clear. */ export function setGrowthBookConfigOverride( feature: string, value: unknown, ): void { if (process.env.USER_TYPE !== 'ant') return try { saveGlobalConfig(c => { const current = c.growthBookOverrides ?? {} if (value === undefined) { if (!(feature in current)) return c const { [feature]: _, ...rest } = current if (Object.keys(rest).length === 0) { const { growthBookOverrides: __, ...configWithout } = c return configWithout } return { ...c, growthBookOverrides: rest } } if (isEqual(current[feature], value)) return c return { ...c, growthBookOverrides: { ...current, [feature]: value } } }) refreshed.emit() } catch (e) { logError(e) } } export function clearGrowthBookConfigOverrides(): void { if (process.env.USER_TYPE !== 'ant') return try { saveGlobalConfig(c => { if ( !c.growthBookOverrides || Object.keys(c.growthBookOverrides).length === 0 ) { return c } const { growthBookOverrides: _, ...rest } = c return rest }) refreshed.emit() } catch (e) { logError(e) } } async function getFeatureValueInternal( feature: string, defaultValue: T, ): Promise { const overrides = getEnvOverrides() if (overrides && feature in overrides) { return overrides[feature] as T } const configOverrides = getConfigOverrides() if (configOverrides && feature in configOverrides) { return configOverrides[feature] as T } const cached = getCachedGrowthBookFeature(feature) if (cached !== undefined) { return cached } return defaultValue } /** * GrowthBook is local-cache-only in this build. * These no-op lifecycle helpers keep call sites stable. */ export async function initializeGrowthBook(): Promise { return null } export function refreshGrowthBookAfterAuthChange(): void { refreshed.emit() } export function resetGrowthBook(): void { envOverrides = null envOverridesParsed = false refreshed.emit() } /** * Get a feature value with a default fallback. */ export async function getFeatureValue_DEPRECATED( feature: string, defaultValue: T, ): Promise { return getFeatureValueInternal(feature, defaultValue) } /** * Get a feature value from local cache immediately. */ export function getFeatureValue_CACHED_MAY_BE_STALE( feature: string, defaultValue: T, ): T { const envOverride = getEnvOverrides() if (envOverride && feature in envOverride) { return envOverride[feature] as T } const configOverride = getConfigOverrides() if (configOverride && feature in configOverride) { return configOverride[feature] as T } const cached = getCachedGrowthBookFeature(feature) return cached !== undefined ? cached : defaultValue } /** * Keep the old API shape; this now resolves from local cache only. */ export function getFeatureValue_CACHED_WITH_REFRESH( feature: string, defaultValue: T, _refreshIntervalMs: number, ): T { return getFeatureValue_CACHED_MAY_BE_STALE(feature, defaultValue) } /** * Check a Statsig feature gate value via local GrowthBook cache, with fallback * to Statsig's cached gates. */ export function checkStatsigFeatureGate_CACHED_MAY_BE_STALE( gate: string, ): boolean { const overrides = getEnvOverrides() if (overrides && gate in overrides) { return Boolean(overrides[gate]) } const configOverrides = getConfigOverrides() if (configOverrides && gate in configOverrides) { return Boolean(configOverrides[gate]) } const cached = getCachedGrowthBookFeature(gate) if (cached !== undefined) { return Boolean(cached) } const statsigCached = getCachedStatsigGate(gate) if (statsigCached !== undefined) { return Boolean(statsigCached) } return false } /** * Check a security restriction gate using only local caches. */ export async function checkSecurityRestrictionGate( gate: string, ): Promise { const overrides = getEnvOverrides() if (overrides && gate in overrides) { return Boolean(overrides[gate]) } const configOverrides = getConfigOverrides() if (configOverrides && gate in configOverrides) { return Boolean(configOverrides[gate]) } const cached = getCachedGrowthBookFeature(gate) if (cached !== undefined) { return Boolean(cached) } const statsigCached = getCachedStatsigGate(gate) if (statsigCached !== undefined) { return Boolean(statsigCached) } return false } /** * Check a boolean entitlement gate with fallback-to-blocking semantics. * In this build the slow path no longer blocks on network. */ export async function checkGate_CACHED_OR_BLOCKING( gate: string, ): Promise { const overrides = getEnvOverrides() if (overrides && gate in overrides) { return Boolean(overrides[gate]) } const configOverrides = getConfigOverrides() if (configOverrides && gate in configOverrides) { return Boolean(configOverrides[gate]) } const cached = getCachedGrowthBookFeature(gate) if (cached !== undefined) { return Boolean(cached) } const statsigCached = getCachedStatsigGate(gate) if (statsigCached !== undefined) { return Boolean(statsigCached) } return false } // ============================================================================ // Dynamic Config Functions // These are semantic wrappers around feature functions for Statsig API parity. // In GrowthBook, dynamic configs are just features with object values. // ============================================================================ /** * Get a dynamic config value - cached/local-override only. */ export async function getDynamicConfig_BLOCKS_ON_INIT( configName: string, defaultValue: T, ): Promise { return getFeatureValue_DEPRECATED(configName, defaultValue) } /** * Get a dynamic config value from local cache immediately. */ export function getDynamicConfig_CACHED_MAY_BE_STALE( configName: string, defaultValue: T, ): T { return getFeatureValue_CACHED_MAY_BE_STALE(configName, defaultValue) }