diff --git a/apps/studio/components/interfaces/App/StudioFeatureFlagProvider.test.tsx b/apps/studio/components/interfaces/App/StudioFeatureFlagProvider.test.tsx index 6425e8c4452..dd5620764e5 100644 --- a/apps/studio/components/interfaces/App/StudioFeatureFlagProvider.test.tsx +++ b/apps/studio/components/interfaces/App/StudioFeatureFlagProvider.test.tsx @@ -2,7 +2,7 @@ import type { Session } from '@supabase/supabase-js' import { QueryClient } from '@tanstack/react-query' import { act, screen, waitFor } from '@testing-library/react' import { platformComponents as components } from 'api-types' -import { AuthContext, useFlag } from 'common' +import { AuthContext, useFeatureFlags, useFlag } from 'common' import { HttpResponse } from 'msw' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -44,7 +44,13 @@ const PROFILE_CONTEXT = createMockProfileContext() function FlagValue() { const isEnabled = useFlag('projectTargetedFeature') - return
{isEnabled ? 'Enabled' : 'Disabled'}
+ const { hasLoaded } = useFeatureFlags() + return ( + <> +
{isEnabled ? 'Enabled' : 'Disabled'}
+
{hasLoaded ? 'Loaded' : 'Loading'}
+ + ) } function TestProvider() { @@ -145,4 +151,59 @@ describe('StudioFeatureFlagProvider', () => { }) expect(screen.getByText('Disabled')).toBeInTheDocument() }) + + it.each([ + { destination: '/projects/project-b', ref: 'project-b', scenario: 'switching projects' }, + { destination: '/organizations?slug=test-org', ref: undefined, scenario: 'leaving a project' }, + ])('hides the previous project flags while $scenario', async ({ destination, ref }) => { + const pendingFlags = + Promise.withResolvers>() + getFlags.mockImplementation((_email, attributes) => + attributes.project_ref === ref + ? pendingFlags.promise + : Promise.resolve([{ settingKey: 'projectTargetedFeature', settingValue: true }]) + ) + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + customRender(, { profileContext: PROFILE_CONTEXT, queryClient }) + expect(await screen.findByText('Enabled')).toBeInTheDocument() + + await act(async () => routerMock.push(destination)) + + expect(screen.getByText('Disabled')).toBeInTheDocument() + expect(screen.getByText('Loading')).toBeInTheDocument() + + await act(async () => { + pendingFlags.resolve([{ settingKey: 'projectTargetedFeature', settingValue: false }]) + }) + expect(await screen.findByText('Loaded')).toBeInTheDocument() + expect(screen.getByText('Disabled')).toBeInTheDocument() + }) + + it('ignores a late evaluation after navigating to a different project', async () => { + const pendingFlags = + Promise.withResolvers>() + getFlags.mockImplementation((_email, attributes) => + attributes.project_ref === 'project-b' + ? pendingFlags.promise + : Promise.resolve([ + { + settingKey: 'projectTargetedFeature', + settingValue: attributes.project_ref === 'project-a', + }, + ]) + ) + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + customRender(, { profileContext: PROFILE_CONTEXT, queryClient }) + expect(await screen.findByText('Enabled')).toBeInTheDocument() + + await act(async () => routerMock.push('/projects/project-b')) + await act(async () => routerMock.push('/projects/project-c')) + expect(await screen.findByText('Loaded')).toBeInTheDocument() + expect(screen.getByText('Disabled')).toBeInTheDocument() + + await act(async () => { + pendingFlags.resolve([{ settingKey: 'projectTargetedFeature', settingValue: true }]) + }) + expect(screen.getByText('Disabled')).toBeInTheDocument() + }) }) diff --git a/packages/common/feature-flags.tsx b/packages/common/feature-flags.tsx index a84e6b9fc83..b7b1c2d9b69 100644 --- a/packages/common/feature-flags.tsx +++ b/packages/common/feature-flags.tsx @@ -99,13 +99,24 @@ export const FeatureFlagProvider = ({ const resolvedProjectRef = projectRef ?? params.ref const lastSentGroupContextRef = useRef(null) - const [store, setStore] = useState({ - API_URL, - configcat: {}, - posthog: {}, - hasLoaded: false, + const [evaluation, setEvaluation] = useState<{ + projectRef?: string + flags: FeatureFlagContextType + }>({ + projectRef: resolvedProjectRef, + flags: { + API_URL, + configcat: {}, + posthog: {}, + hasLoaded: false, + }, }) + const store: FeatureFlagContextType = + evaluation.projectRef === resolvedProjectRef + ? evaluation.flags + : { API_URL, configcat: {}, posthog: {}, hasLoaded: false } + useEffect(() => { let mounted = true @@ -229,7 +240,7 @@ export const FeatureFlagProvider = ({ flagStore.hasLoaded = true if (mounted) { - setStore(flagStore) + setEvaluation({ projectRef: resolvedProjectRef, flags: flagStore }) } }