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 })
}
}