diff --git a/frontend/common/hooks/__tests__/useFeatureExperimentFreeze.test.ts b/frontend/common/hooks/__tests__/useFeatureExperimentFreeze.test.ts index acd94a1d4e00..584ce40fbc2b 100644 --- a/frontend/common/hooks/__tests__/useFeatureExperimentFreeze.test.ts +++ b/frontend/common/hooks/__tests__/useFeatureExperimentFreeze.test.ts @@ -7,23 +7,20 @@ jest.mock('react', () => ({ useMemo: (fn: () => any) => fn(), })) -jest.mock('common/utils/utils', () => ({ - __esModule: true, - default: { - getFlagsmithHasFeature: jest.fn(), - }, +jest.mock('common/hooks/useHasExperimentationFeature', () => ({ + useHasExperimentationFeature: jest.fn(), })) import { useFeatureExperimentFreeze } from 'common/hooks/useFeatureExperimentFreeze' import { useGetExperimentsQuery } from 'common/services/useExperiment' -import Utils from 'common/utils/utils' +import { useHasExperimentationFeature } from 'common/hooks/useHasExperimentationFeature' import { Experiment } from 'common/types/responses' const mockUseGetExperimentsQuery = useGetExperimentsQuery as jest.MockedFunction -const mockGetFlagsmithHasFeature = - Utils.getFlagsmithHasFeature as jest.MockedFunction< - typeof Utils.getFlagsmithHasFeature +const mockUseHasExperimentationFeature = + useHasExperimentationFeature as jest.MockedFunction< + typeof useHasExperimentationFeature > const makeExperiment = ( @@ -56,7 +53,10 @@ const withResults = (experiments: Experiment[]) => describe('useFeatureExperimentFreeze', () => { beforeEach(() => { jest.clearAllMocks() - mockGetFlagsmithHasFeature.mockReturnValue(true) + mockUseHasExperimentationFeature.mockReturnValue({ + enabled: true, + isLoading: false, + }) }) it('returns isFrozen true when a running experiment exists for the feature', () => { @@ -125,8 +125,11 @@ describe('useFeatureExperimentFreeze', () => { ) }) - it('skips query when experimental_flags feature is disabled', () => { - mockGetFlagsmithHasFeature.mockReturnValue(false) + it('skips query when experimentation is unavailable', () => { + mockUseHasExperimentationFeature.mockReturnValue({ + enabled: false, + isLoading: false, + }) mockUseGetExperimentsQuery.mockReturnValue(empty) const result = useFeatureExperimentFreeze(42, 'env-123') @@ -138,6 +141,18 @@ describe('useFeatureExperimentFreeze', () => { ) }) + it('returns isLoading true while the experimentation check is loading', () => { + mockUseHasExperimentationFeature.mockReturnValue({ + enabled: false, + isLoading: true, + }) + mockUseGetExperimentsQuery.mockReturnValue(empty) + + const result = useFeatureExperimentFreeze(42, 'env-123') + + expect(result.isLoading).toBe(true) + }) + it('queries only running experiments when feature is enabled', () => { mockUseGetExperimentsQuery.mockReturnValue(empty) diff --git a/frontend/common/hooks/__tests__/useHasExperimentationFeature.test.ts b/frontend/common/hooks/__tests__/useHasExperimentationFeature.test.ts new file mode 100644 index 000000000000..9e81cc2fa131 --- /dev/null +++ b/frontend/common/hooks/__tests__/useHasExperimentationFeature.test.ts @@ -0,0 +1,61 @@ +jest.mock('common/services/useBuildVersion', () => ({ + useGetBuildVersionQuery: jest.fn(), +})) + +jest.mock('common/utils/utils', () => ({ + __esModule: true, + default: { + getFlagsmithHasFeature: jest.fn(), + }, +})) + +import { useHasExperimentationFeature } from 'common/hooks/useHasExperimentationFeature' +import { useGetBuildVersionQuery } from 'common/services/useBuildVersion' +import Utils from 'common/utils/utils' + +const mockUseGetBuildVersionQuery = + useGetBuildVersionQuery as jest.MockedFunction +const mockGetFlagsmithHasFeature = + Utils.getFlagsmithHasFeature as jest.MockedFunction< + typeof Utils.getFlagsmithHasFeature + > + +describe('useHasExperimentationFeature', () => { + beforeEach(() => { + jest.clearAllMocks() + mockGetFlagsmithHasFeature.mockReturnValue(true) + }) + + it('reports loading while the build version is pending', () => { + mockUseGetBuildVersionQuery.mockReturnValue({ + data: undefined, + isLoading: true, + } as any) + + const result = useHasExperimentationFeature('experimental_flags') + + expect(result).toEqual({ enabled: false, isLoading: true }) + }) + + it('is disabled when the experimentation infra is not deployed', () => { + mockUseGetBuildVersionQuery.mockReturnValue({ + data: { backend: { experimentation_enabled: false } }, + isLoading: false, + } as any) + + const result = useHasExperimentationFeature('experimental_flags') + + expect(result).toEqual({ enabled: false, isLoading: false }) + }) + + it('is enabled when the flag is on and the experimentation infra is deployed', () => { + mockUseGetBuildVersionQuery.mockReturnValue({ + data: { backend: { experimentation_enabled: true } }, + isLoading: false, + } as any) + + const result = useHasExperimentationFeature('experimental_flags') + + expect(result).toEqual({ enabled: true, isLoading: false }) + }) +}) diff --git a/frontend/common/hooks/useFeatureExperimentFreeze.ts b/frontend/common/hooks/useFeatureExperimentFreeze.ts index 33a0aea79eed..fd2b768c1360 100644 --- a/frontend/common/hooks/useFeatureExperimentFreeze.ts +++ b/frontend/common/hooks/useFeatureExperimentFreeze.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react' import { useGetExperimentsQuery } from 'common/services/useExperiment' import type { Experiment } from 'common/types/responses' -import Utils from 'common/utils/utils' +import { useHasExperimentationFeature } from './useHasExperimentationFeature' export type FeatureExperimentFreeze = { isFrozen: boolean @@ -13,7 +13,8 @@ export function useFeatureExperimentFreeze( featureId: number | undefined, environmentId: string, ): FeatureExperimentFreeze { - const isExperimentEnabled = Utils.getFlagsmithHasFeature('experimental_flags') + const { enabled: isExperimentEnabled, isLoading: isVersionLoading } = + useHasExperimentationFeature('experimental_flags') const skip = !isExperimentEnabled || !featureId || !environmentId const { data, isLoading } = useGetExperimentsQuery( { environmentId, status: 'running' }, @@ -28,6 +29,6 @@ export function useFeatureExperimentFreeze( return { experiment, isFrozen: experiment !== null, - isLoading, + isLoading: isVersionLoading || isLoading, } } diff --git a/frontend/common/hooks/useHasExperimentationFeature.ts b/frontend/common/hooks/useHasExperimentationFeature.ts new file mode 100644 index 000000000000..d0d2a59794d3 --- /dev/null +++ b/frontend/common/hooks/useHasExperimentationFeature.ts @@ -0,0 +1,20 @@ +import { useGetBuildVersionQuery } from 'common/services/useBuildVersion' +import Utils from 'common/utils/utils' + +export type ExperimentationFeature = { + enabled: boolean + isLoading: boolean +} + +// Experimentation features need both their flag and the experimentation infra. +export function useHasExperimentationFeature( + flag: string, +): ExperimentationFeature { + const { data, isLoading } = useGetBuildVersionQuery({}) + return { + enabled: + Utils.getFlagsmithHasFeature(flag) && + !!data?.backend?.experimentation_enabled, + isLoading, + } +} diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index f8646bc07045..003cde03cede 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -1262,6 +1262,7 @@ export type Version = { ci_commit_sha: string image_tag: string has_email_provider: boolean + experimentation_enabled: boolean is_enterprise: boolean is_saas: boolean 'self_hosted_data'?: { diff --git a/frontend/web/components/modals/create-feature/tabs/FeatureValueTab.tsx b/frontend/web/components/modals/create-feature/tabs/FeatureValueTab.tsx index 64b7aa9967df..385d7baf4ecb 100644 --- a/frontend/web/components/modals/create-feature/tabs/FeatureValueTab.tsx +++ b/frontend/web/components/modals/create-feature/tabs/FeatureValueTab.tsx @@ -31,6 +31,7 @@ import { VARIATION_WEIGHT_FIELD, } from 'common/utils/multivariate' import { FeatureExperimentFreeze } from 'common/hooks/useFeatureExperimentFreeze' +import { useHasExperimentationFeature } from 'common/hooks/useHasExperimentationFeature' import ExperimentFreezeNotice from 'components/modals/create-feature/components/ExperimentFreezeNotice' import { useHasPermission } from 'common/providers/Permission' import { ProjectPermission } from 'common/types/permissions.types' @@ -120,6 +121,8 @@ const FeatureValueTab: FC = ({ level: 'project', permission: ProjectPermission.CREATE_FEATURE, }) + const { enabled: hasExperiments } = + useHasExperimentationFeature('experimental_flags') const default_enabled = featureState.enabled ?? false const initial_value = featureState.feature_state_value @@ -507,7 +510,7 @@ const FeatureValueTab: FC = ({ = ({ changeRequestsData?.count) || 0 const scheduled = scheduledData?.count || 0 + const { enabled: hasExperiments } = + useHasExperimentationFeature('experimental_flags') const inner = (
= ({ > Features - {Utils.getFlagsmithHasFeature('experimental_flags') ? ( + {hasExperiments ? ( { useParams() const history = useHistory() const numericId = Number(experimentId) - const hasFeature = Utils.getFlagsmithHasFeature('experimental_flags') + const { enabled: hasFeature, isLoading: isLoadingFeature } = + useHasExperimentationFeature('experimental_flags') const { data: experiment, @@ -53,14 +54,14 @@ const ExperimentDetailPage: FC = () => { const results = bayesianResults?.payload ?? undefined - if (!hasFeature) { + if (!isLoadingFeature && !hasFeature) { history.replace( `/project/${projectId}/environment/${environmentId}/features`, ) return null } - if (isLoading) { + if (isLoadingFeature || isLoading) { return (
diff --git a/frontend/web/components/pages/ExperimentsPage.tsx b/frontend/web/components/pages/ExperimentsPage.tsx index 22013a752f30..ec35f341a29a 100644 --- a/frontend/web/components/pages/ExperimentsPage.tsx +++ b/frontend/web/components/pages/ExperimentsPage.tsx @@ -1,6 +1,7 @@ import { FC, useCallback, useEffect, useState } from 'react' import { useHistory } from 'react-router-dom' import Utils from 'common/utils/utils' +import { useHasExperimentationFeature } from 'common/hooks/useHasExperimentationFeature' import { useRouteContext } from 'components/providers/RouteContext' import { useGetExperimentsQuery } from 'common/services/useExperiment' import { useGetWarehouseConnectionsQuery } from 'common/services/useWarehouseConnection' @@ -38,6 +39,9 @@ const ExperimentsPage: FC = () => { setIsCreating(false) }, [environmentId]) + const { enabled: hasExperiments, isLoading: isLoadingFeature } = + useHasExperimentationFeature('experimental_flags') + const { data: experimentsData, isLoading } = useGetExperimentsQuery( { environmentId: environmentId ?? '', @@ -46,13 +50,13 @@ const ExperimentsPage: FC = () => { q: search || undefined, status: activeTab !== 'all' ? activeTab : undefined, }, - { skip: !environmentId }, + { skip: !environmentId || !hasExperiments }, ) const { data: warehouseConnections, isLoading: isLoadingWarehouse } = useGetWarehouseConnectionsQuery( { environmentId: environmentId ?? '', exclude_event_stats: true }, - { skip: !environmentId }, + { skip: !environmentId || !hasExperiments }, ) const hasWarehouse = (warehouseConnections?.length ?? 0) > 0 @@ -79,9 +83,8 @@ const ExperimentsPage: FC = () => { [statusCounts], ) - if (!environmentId || !projectId) return null + if (!environmentId || !projectId || isLoadingFeature) return null - const hasExperiments = Utils.getFlagsmithHasFeature('experimental_flags') const hasFakeDoor = Utils.getFlagsmithHasFeature('experiments_fake_door') if (!Utils.isOrgOnFreePlan() && !hasExperiments && hasFakeDoor) { diff --git a/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx b/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx index 471e64266887..6de8b24812a0 100644 --- a/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx +++ b/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react' import FieldLabel from 'components/base/forms/FieldLabel' import ConfirmRemoveEnvironment from 'components/modals/ConfirmRemoveEnvironment' import ProjectStore from 'common/stores/project-store' +import { useHasExperimentationFeature } from 'common/hooks/useHasExperimentationFeature' import ConfigProvider from 'common/providers/ConfigProvider' import CreateWebhookModal from 'components/modals/CreateWebhook' import ConfirmRemoveWebhook from 'components/modals/ConfirmRemoveWebhook' @@ -119,7 +120,7 @@ const EnvironmentSettingsPage: React.FC = () => { const has4EyesPermission = Utils.getPlansPermission('4_EYES') const metadataEnable = Utils.getPlansPermission('METADATA') - const warehouseEnabled = Utils.getFlagsmithHasFeature( + const { enabled: warehouseEnabled } = useHasExperimentationFeature( 'experimentation_warehouse_connection', ) const cohortSyncEnabled = getSegmentSources().some(