Gracefully handle feature flag set up (#38394)

* Gracefully handle feature flag set up

* Update FeatureFlagProvider to support granular provider toggling, and make getConfigCatFlags an optional prop

* Nit

* Fix TS
This commit is contained in:
Joshen Lim authored and GitHub committed 2025-09-10 12:00:16 +08:00
1 parent bc3a473170
commit 3963cbdf8f
6 files changed
+47 -25

No files matched your search

+1 -1
View File
@@ -101,10 +101,10 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
const isTestEnv = process.env.NEXT_PUBLIC_NODE_ENV === 'test'
const cloudProvider = useDefaultProvider()
const getConfigCatFlags = useCallback(
(userEmail?: string) => {
const customAttributes = cloudProvider ? { cloud_provider: cloudProvider } : undefined
return getFlags(userEmail, customAttributes)
},
[cloudProvider]
+1 -1
View File
@@ -9,12 +9,12 @@ import { useEffect } from 'react'
import * as supabaseLogoWordmarkDark from 'common/assets/images/supabase-logo-wordmark--dark.png'
import * as supabaseLogoWordmarkLight from 'common/assets/images/supabase-logo-wordmark--light.png'
import footerData from 'data/Footer'
import { usePathname } from 'next/navigation'
import { Badge, IconDiscord, IconGitHubSolid, IconTwitterX, IconYoutubeSolid, cn } from 'ui'
import { ThemeToggle } from 'ui-patterns/ThemeToggle'
import supabase from '~/lib/supabase'
import useDarkLaunchWeeks from '../../hooks/useDarkLaunchWeeks'
import SectionContainer from '../Layouts/SectionContainer'
import { usePathname } from 'next/navigation'
interface Props {
className?: string
+2 -2
View File
@@ -2,8 +2,8 @@ import { createClient } from '@supabase/supabase-js'
import { Database } from './database.types'
const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
process.env.NEXT_PUBLIC_SUPABASE_URL ?? '',
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? '',
{
realtime: {
params: {
+2 -2
View File
@@ -5,7 +5,6 @@ import '../styles/index.css'
import {
AuthProvider,
FeatureFlagProvider,
getFlags as getConfigCatFlags,
IS_PLATFORM,
PageTelemetry,
TelemetryTagManager,
@@ -87,7 +86,8 @@ export default function App({ Component, pageProps }: AppProps) {
/>
<AuthProvider>
<FeatureFlagProvider API_URL={API_URL} getConfigCatFlags={getConfigCatFlags}>
{/* [TODO] I think we need to deconflict with the providers in layout.tsx? */}
<FeatureFlagProvider API_URL={API_URL} enabled={{ cc: true, ph: false }}>
<ThemeProvider
themes={themes.map((theme) => theme.value)}
enableSystem
+14 -7
View File
@@ -27,17 +27,22 @@ export const fetchHandler: typeof fetch = async (input, init) => {
}
async function getClient() {
if (!process.env.NEXT_PUBLIC_CONFIGCAT_PROXY_URL) {
console.error(
'Failed to get ConfigCat client: missing env var "NEXT_PUBLIC_CONFIGCAT_PROXY_URL"'
)
}
if (client) return client
if (!process.env.NEXT_PUBLIC_CONFIGCAT_SDK_KEY && !process.env.NEXT_PUBLIC_CONFIGCAT_PROXY_URL) {
console.log('Skipping ConfigCat set up as env vars are not present')
return undefined
}
const response = await fetchHandler(process.env.NEXT_PUBLIC_CONFIGCAT_PROXY_URL + endpoint)
const options = { pollIntervalSeconds: 7 * 60 } // 7 minutes
if (response.status !== 200) {
if (!process.env.NEXT_PUBLIC_CONFIGCAT_SDK_KEY) {
console.error('Failed to set up ConfigCat: SDK Key is missing')
return undefined
}
// proxy is down, use default client
client = configcat.getClient(
process.env.NEXT_PUBLIC_CONFIGCAT_SDK_KEY ?? '',
@@ -57,7 +62,9 @@ async function getClient() {
export async function getFlags(userEmail: string = '', customAttributes?: Record<string, string>) {
const client = await getClient()
if (userEmail) {
if (!client) {
return []
} else if (userEmail) {
return client.getAllValuesAsync(
new configcat.User(userEmail, undefined, undefined, customAttributes)
)
+27 -12
View File
@@ -5,6 +5,7 @@ import { createContext, PropsWithChildren, useContext, useEffect, useState } fro
import { components } from 'api-types'
import { useUser } from './auth'
import { getFlags as getDefaultConfigCatFlags } from './configcat'
import { hasConsented } from './consent-state'
import { get, post } from './fetchWrappers'
import { ensurePlatformSuffix } from './helpers'
@@ -13,9 +14,15 @@ type TrackFeatureFlagVariables = components['schemas']['TelemetryFeatureFlagBody
export type CallFeatureFlagsResponse = components['schemas']['TelemetryCallFeatureFlagsResponse']
export async function getFeatureFlags(API_URL: string) {
const data = await get(`${ensurePlatformSuffix(API_URL)}/telemetry/feature-flags`)
return data as CallFeatureFlagsResponse
try {
const data = await get(`${ensurePlatformSuffix(API_URL)}/telemetry/feature-flags`)
return data as CallFeatureFlagsResponse
} catch (error: any) {
if (error.message.includes('Failed to fetch')) {
console.error('Failed to fetch PH flags: API is not available')
}
throw error
}
}
export async function trackFeatureFlag(API_URL: string, body: TrackFeatureFlagVariables) {
@@ -57,8 +64,10 @@ export const FeatureFlagProvider = ({
getConfigCatFlags,
children,
}: PropsWithChildren<{
API_URL: string
enabled?: boolean
API_URL?: string
/** Accepts either `boolean` which controls all feature flags or `{ cc: boolean, ph: boolean }` for individual providers */
enabled?: boolean | { cc: boolean; ph: boolean }
/** Custom fetcher for ConfigCat flags if passing in custom attributes */
getConfigCatFlags?: (
userEmail?: string
) => Promise<{ settingKey: string; settingValue: boolean | number | string | null | undefined }[]>
@@ -78,23 +87,29 @@ export const FeatureFlagProvider = ({
async function processFlags() {
if (!enabled) return
const loadPHFlags =
(enabled === true || (typeof enabled === 'object' && enabled.ph)) && !!API_URL
const loadCCFlags = enabled === true || (typeof enabled === 'object' && enabled.cc)
let flagStore: FeatureFlagContextType = { configcat: {}, posthog: {} }
// Run both async operations in parallel
const [flags, flagValues] = await Promise.all([
getFeatureFlags(API_URL),
typeof getConfigCatFlags === 'function'
? getConfigCatFlags(user?.email)
loadPHFlags ? getFeatureFlags(API_URL) : Promise.resolve({}),
loadCCFlags
? typeof getConfigCatFlags === 'function'
? getConfigCatFlags(user?.email)
: getDefaultConfigCatFlags(user?.email)
: Promise.resolve([]),
])
// Process PostHog flags
if (flags) {
// Process PostHog flags if loaded
if (Object.keys(flags).length > 0) {
flagStore.posthog = flags
}
// Process ConfigCat flags
if (typeof getConfigCatFlags === 'function') {
// Process ConfigCat flags if loaded
if (flagValues.length > 0) {
let overridesCookieValue: Record<string, boolean> = {}
try {
const cookies = getCookies()