From 3963cbdf8f0dc05fc29b7c0da85f272c13cec7b8 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 10 Sep 2025 12:00:16 +0800 Subject: [PATCH] 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 --- apps/studio/pages/_app.tsx | 2 +- apps/www/components/Footer/index.tsx | 2 +- apps/www/lib/supabase.ts | 4 +-- apps/www/pages/_app.tsx | 4 +-- packages/common/configcat.ts | 21 ++++++++++----- packages/common/feature-flags.tsx | 39 +++++++++++++++++++--------- 6 files changed, 47 insertions(+), 25 deletions(-) diff --git a/apps/studio/pages/_app.tsx b/apps/studio/pages/_app.tsx index 34b2be5f2d1..c1113bbac65 100644 --- a/apps/studio/pages/_app.tsx +++ b/apps/studio/pages/_app.tsx @@ -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] diff --git a/apps/www/components/Footer/index.tsx b/apps/www/components/Footer/index.tsx index 26fba97af5c..381e9a03105 100644 --- a/apps/www/components/Footer/index.tsx +++ b/apps/www/components/Footer/index.tsx @@ -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 diff --git a/apps/www/lib/supabase.ts b/apps/www/lib/supabase.ts index 9d8b881ebe4..22237c346a5 100644 --- a/apps/www/lib/supabase.ts +++ b/apps/www/lib/supabase.ts @@ -2,8 +2,8 @@ import { createClient } from '@supabase/supabase-js' import { Database } from './database.types' const supabase = createClient( - 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: { diff --git a/apps/www/pages/_app.tsx b/apps/www/pages/_app.tsx index be7a9e4042e..f839f5cbd40 100644 --- a/apps/www/pages/_app.tsx +++ b/apps/www/pages/_app.tsx @@ -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) { /> - + {/* [TODO] I think we need to deconflict with the providers in layout.tsx? */} + theme.value)} enableSystem diff --git a/packages/common/configcat.ts b/packages/common/configcat.ts index 410f40addcc..419055cbb90 100644 --- a/packages/common/configcat.ts +++ b/packages/common/configcat.ts @@ -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) { const client = await getClient() - if (userEmail) { + if (!client) { + return [] + } else if (userEmail) { return client.getAllValuesAsync( new configcat.User(userEmail, undefined, undefined, customAttributes) ) diff --git a/packages/common/feature-flags.tsx b/packages/common/feature-flags.tsx index 85413bff5ca..ee8b1c3b77d 100644 --- a/packages/common/feature-flags.tsx +++ b/packages/common/feature-flags.tsx @@ -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 = {} try { const cookies = getCookies()