mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
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:
1 parent
bc3a473170
commit
3963cbdf8f
6 files changed
+47
-25
No files matched your search
@@ -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]
|
||||
|
||||
@@ -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,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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user