diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx index 6e31210cbd..fc3421697d 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx @@ -7,7 +7,6 @@ import Edit from '@mui/icons-material/Edit'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; -import { useUiFlag } from 'hooks/useUiFlag'; import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog'; import { useState } from 'react'; import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog'; @@ -98,7 +97,6 @@ const FeatureOverviewMetaData = () => { const featureId = useRequiredPathParam('featureId'); const { feature, refetchFeature } = useFeature(projectId, featureId); const { project, description, type } = feature; - const featureLifecycleEnabled = useUiFlag('featureLifecycle'); const navigate = useNavigate(); const [showDelDialog, setShowDelDialog] = useState(false); const [showMarkCompletedDialogue, setShowMarkCompletedDialogue] = @@ -142,10 +140,7 @@ const FeatureOverviewMetaData = () => { {project} Lifecycle: diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx index d1913e0116..18d40b3aee 100644 --- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx @@ -34,7 +34,6 @@ import { TableEmptyState } from './TableEmptyState/TableEmptyState'; import { useRowActions } from './hooks/useRowActions'; import { useSelectedData } from './hooks/useSelectedData'; import { FeatureOverviewCell } from '../../../common/Table/cells/FeatureOverviewCell/FeatureOverviewCell'; -import { useUiFlag } from 'hooks/useUiFlag'; import { useProjectFeatureSearch, useProjectFeatureSearchActions, @@ -55,7 +54,6 @@ export const ProjectFeatureToggles = ({ environments, }: IPaginatedProjectFeatureTogglesProps) => { const projectId = useRequiredPathParam('projectId'); - const featureLifecycleEnabled = useUiFlag('featureLifecycle'); const { features, @@ -192,36 +190,30 @@ export const ProjectFeatureToggles = ({ width: '1%', }, }), - ...(featureLifecycleEnabled - ? [ - columnHelper.accessor('lifecycle', { - id: 'lifecycle', - header: 'Lifecycle', - cell: ({ row: { original } }) => ( - { - setShowMarkCompletedDialogue({ - featureId: original.name, - open: true, - }); - }} - onUncomplete={refetch} - onArchive={() => - setFeatureArchiveState(original.name) - } - data-loading - /> - ), - enableSorting: false, - size: 50, - meta: { - align: 'center', - width: '1%', - }, - }), - ] - : []), + columnHelper.accessor('lifecycle', { + id: 'lifecycle', + header: 'Lifecycle', + cell: ({ row: { original } }) => ( + { + setShowMarkCompletedDialogue({ + featureId: original.name, + open: true, + }); + }} + onUncomplete={refetch} + onArchive={() => setFeatureArchiveState(original.name)} + data-loading + /> + ), + enableSorting: false, + size: 50, + meta: { + align: 'center', + width: '1%', + }, + }), ...environments.map((name: string) => { const isChangeRequestEnabled = isChangeRequestConfigured(name); @@ -302,7 +294,6 @@ export const ProjectFeatureToggles = ({ tableState.favoritesFirst, refetch, isPlaceholder, - featureLifecycleEnabled, ], ); @@ -430,16 +421,11 @@ export const ProjectFeatureToggles = ({ id: 'lastSeenAt', isVisible: columnVisibility.lastSeenAt, }, - ...(featureLifecycleEnabled - ? [ - { - header: 'Lifecycle', - id: 'lifecycle', - isVisible: - columnVisibility.lifecycle, - }, - ] - : []), + { + header: 'Lifecycle', + id: 'lifecycle', + isVisible: columnVisibility.lifecycle, + }, { id: 'divider', }, diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/hooks/useDefaultColumnVisibility.ts b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/hooks/useDefaultColumnVisibility.ts index 2bdc880025..b5ea57d8eb 100644 --- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/hooks/useDefaultColumnVisibility.ts +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/hooks/useDefaultColumnVisibility.ts @@ -1,7 +1,6 @@ import { useCallback } from 'react'; import { useMediaQuery, useTheme } from '@mui/material'; import type { VisibilityState } from '@tanstack/react-table'; -import { useUiFlag } from 'hooks/useUiFlag'; const staticColumns = ['select', 'actions', 'name', 'favorite']; @@ -22,7 +21,6 @@ export const useDefaultColumnVisibility = (allColumnIds: string[]) => { const isTinyScreen = useMediaQuery(theme.breakpoints.down('sm')); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg')); - const featureLifecycleEnabled = useUiFlag('featureLifecycle'); const showEnvironments = useCallback( (environmentsToShow: number = 0) => @@ -55,7 +53,7 @@ export const useDefaultColumnVisibility = (allColumnIds: string[]) => { return formatAsColumnVisibility(allColumnIds, [ ...staticColumns, 'lastSeenAt', - ...(featureLifecycleEnabled ? ['lifecycle'] : []), + 'lifecycle', 'createdAt', 'createdBy', 'type', diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 653d94d85a..edc33639f4 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -105,7 +105,6 @@ exports[`should create default config 1`] = ` "extendedMetrics": false, "extendedUsageMetrics": false, "featureCollaborators": false, - "featureLifecycle": false, "featureSearchFeedback": { "enabled": false, "name": "withText", diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-controller.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-controller.ts index 1ce31bbd88..3aaddb2dc1 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-controller.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-controller.ts @@ -19,7 +19,6 @@ import { } from '../../openapi'; import Controller from '../../routes/controller'; import type { Request, Response } from 'express'; -import { NotFoundError } from '../../error'; import type { IAuthRequest } from '../../routes/unleash-types'; import type { WithTransactional } from '../../db/transaction'; @@ -120,9 +119,6 @@ export default class FeatureLifecycleController extends Controller { req: Request, res: Response, ): Promise { - if (!this.flagResolver.isEnabled('featureLifecycle')) { - throw new NotFoundError('Feature lifecycle is disabled.'); - } const { featureName } = req.params; const result = @@ -144,9 +140,6 @@ export default class FeatureLifecycleController extends Controller { >, res: Response, ): Promise { - if (!this.flagResolver.isEnabled('featureLifecycle')) { - throw new NotFoundError('Feature lifecycle is disabled.'); - } const { featureName, projectId } = req.params; const status = req.body; @@ -162,9 +155,6 @@ export default class FeatureLifecycleController extends Controller { req: IAuthRequest, res: Response, ): Promise { - if (!this.flagResolver.isEnabled('featureLifecycle')) { - throw new NotFoundError('Feature lifecycle is disabled.'); - } const { featureName, projectId } = req.params; await this.featureLifecycleService.transactional((service) => diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-read-model.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-read-model.ts index caed22d8ed..f7b308095b 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-read-model.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-read-model.ts @@ -36,10 +36,6 @@ export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel { } async getStageCount(): Promise { - if (!this.flagResolver.isEnabled('featureLifecycleMetrics')) { - return []; - } - const { rows } = await this.db.raw(` SELECT stage, @@ -65,10 +61,6 @@ export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel { } async getStageCountByProject(): Promise { - if (!this.flagResolver.isEnabled('featureLifecycleMetrics')) { - return []; - } - const { rows } = await this.db.raw(` SELECT f.project, @@ -133,10 +125,6 @@ export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel { public async getAllWithStageDuration(): Promise< IProjectLifecycleStageDuration[] > { - if (!this.flagResolver.isEnabled('featureLifecycleMetrics')) { - return []; - } - const featureLifeCycles = await this.getAll(); return calculateStageDurations(featureLifeCycles); } diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-service.test.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-service.test.ts index 9b3a2de61d..358d130751 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-service.test.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-service.test.ts @@ -1,7 +1,6 @@ import { CLIENT_METRICS_ADDED, FEATURE_ARCHIVED, - FEATURE_COMPLETED, FEATURE_CREATED, FEATURE_REVIVED, type IEnvironment, @@ -101,33 +100,3 @@ test('can insert and read lifecycle stages', async () => { { stage: 'initial', enteredStageAt: expect.any(Date) }, ]); }); - -test('ignores lifecycle state updates when flag disabled', async () => { - const eventBus = new EventEmitter(); - const { featureLifecycleService, eventStore, environmentStore } = - createFakeFeatureLifecycleService({ - flagResolver: { isEnabled: () => false }, - eventBus, - getLogger: noLoggerProvider, - } as unknown as IUnleashConfig); - const featureName = 'testFeature'; - - await environmentStore.create({ - name: 'my-dev-environment', - type: 'development', - } as IEnvironment); - featureLifecycleService.listen(); - - await eventStore.emit(FEATURE_CREATED, { featureName }); - await eventStore.emit(FEATURE_COMPLETED, { featureName }); - await eventBus.emit(CLIENT_METRICS_ADDED, { - bucket: { toggles: { [featureName]: 'irrelevant' } }, - environment: 'development', - }); - await eventStore.emit(FEATURE_ARCHIVED, { featureName }); - - const lifecycle = - await featureLifecycleService.getFeatureLifecycle(featureName); - - expect(lifecycle).toEqual([]); -}); diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts index 0b2d44aa5a..ae33e19b83 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts @@ -77,19 +77,10 @@ export class FeatureLifecycleService { ); } - private async checkEnabled(fn: () => Promise) { - const enabled = this.flagResolver.isEnabled('featureLifecycle'); - if (enabled) { - return fn(); - } - } - listen() { - void this.checkEnabled(() => this.featureLifecycleStore.backfill()); + this.featureLifecycleStore.backfill(); this.eventStore.on(FEATURE_CREATED, async (event) => { - await this.checkEnabled(() => - this.featureInitialized(event.featureName), - ); + await this.featureInitialized(event.featureName); }); this.eventBus.on( CLIENT_METRICS_ADDED, @@ -103,22 +94,19 @@ export class FeatureLifecycleService { const features = metrics.map( (metric) => metric.featureName, ); - await this.checkEnabled(() => - this.featuresReceivedMetrics(features, environment), + await this.featuresReceivedMetrics( + features, + environment, ); } } }, ); this.eventStore.on(FEATURE_ARCHIVED, async (event) => { - await this.checkEnabled(() => - this.featureArchived(event.featureName), - ); + await this.featureArchived(event.featureName); }); this.eventStore.on(FEATURE_REVIVED, async (event) => { - await this.checkEnabled(() => - this.featureRevived(event.featureName), - ); + await this.featureRevived(event.featureName); }); } diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts b/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts index 87c4aa38a9..7f05c5e274 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts @@ -32,9 +32,7 @@ beforeAll(async () => { db.stores, { experimental: { - flags: { - featureLifecycle: true, - }, + flags: {}, }, }, db.rawDatabase, diff --git a/src/lib/features/feature-search/feature-search-store.ts b/src/lib/features/feature-search/feature-search-store.ts index b49942bca5..7bc944fe15 100644 --- a/src/lib/features/feature-search/feature-search-store.ts +++ b/src/lib/features/feature-search/feature-search-store.ts @@ -11,7 +11,6 @@ import type { } from '../../types'; import FeatureToggleStore from '../feature-toggle/feature-toggle-store'; import type { Db } from '../../db/db'; -import Raw = Knex.Raw; import type { IFeatureSearchParams, IQueryParam, @@ -19,6 +18,7 @@ import type { import { applyGenericQueryParams, applySearchFilters } from './search-utils'; import type { FeatureSearchEnvironmentSchema } from '../../openapi/spec/feature-search-environment-schema'; import { generateImageUrl } from '../../util'; +import Raw = Knex.Raw; const sortEnvironments = (overview: IFeatureSearchOverview[]) => { return overview.map((data: IFeatureSearchOverview) => ({ @@ -112,9 +112,6 @@ class FeatureSearchStore implements IFeatureSearchStore { const validatedSortOrder = sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc'; - const featureLifecycleEnabled = - this.flagResolver.isEnabled('featureLifecycle'); - const finalQuery = this.db .with('ranked_features', (query) => { query.from('features'); @@ -315,27 +312,22 @@ class FeatureSearchStore implements IFeatureSearchStore { .joinRaw('CROSS JOIN total_features') .whereBetween('final_rank', [offset + 1, offset + limit]) .orderBy('final_rank'); - if (featureLifecycleEnabled) { - finalQuery - .select( - 'lifecycle.latest_stage', - 'lifecycle.stage_status', - 'lifecycle.entered_stage_at', - ) - .leftJoin( - 'lifecycle', - 'ranked_features.feature_name', - 'lifecycle.stage_feature', - ); - } + finalQuery + .select( + 'lifecycle.latest_stage', + 'lifecycle.stage_status', + 'lifecycle.entered_stage_at', + ) + .leftJoin( + 'lifecycle', + 'ranked_features.feature_name', + 'lifecycle.stage_feature', + ); this.queryExtraData(finalQuery); const rows = await finalQuery; stopTimer(); if (rows.length > 0) { - const overview = this.getAggregatedSearchData( - rows, - featureLifecycleEnabled, - ); + const overview = this.getAggregatedSearchData(rows); const features = sortEnvironments( overview, ) as IFeatureSearchOverview[]; @@ -461,10 +453,7 @@ class FeatureSearchStore implements IFeatureSearchStore { return rankingSql; } - getAggregatedSearchData( - rows, - featureLifecycleEnabled: boolean, - ): IFeatureSearchOverview[] { + getAggregatedSearchData(rows): IFeatureSearchOverview[] { const entriesMap: Map = new Map(); const orderedEntries: IFeatureSearchOverview[] = []; @@ -501,17 +490,15 @@ class FeatureSearchStore implements IFeatureSearchStore { }), }, }; - if (featureLifecycleEnabled) { - entry.lifecycle = row.latest_stage - ? { - stage: row.latest_stage, - ...(row.stage_status - ? { status: row.stage_status } - : {}), - enteredStageAt: row.entered_stage_at, - } - : undefined; - } + entry.lifecycle = row.latest_stage + ? { + stage: row.latest_stage, + ...(row.stage_status + ? { status: row.stage_status } + : {}), + enteredStageAt: row.entered_stage_at, + } + : undefined; entriesMap.set(row.feature_name, entry); orderedEntries.push(entry); } diff --git a/src/lib/features/feature-search/feature.search.e2e.test.ts b/src/lib/features/feature-search/feature.search.e2e.test.ts index e2c26876d7..86a8ee7d49 100644 --- a/src/lib/features/feature-search/feature.search.e2e.test.ts +++ b/src/lib/features/feature-search/feature.search.e2e.test.ts @@ -21,7 +21,6 @@ beforeAll(async () => { experimental: { flags: { strictSchemaValidation: true, - featureLifecycle: true, anonymiseEventLog: true, }, }, diff --git a/src/lib/metrics.test.ts b/src/lib/metrics.test.ts index 20a46ab343..be70f46dfc 100644 --- a/src/lib/metrics.test.ts +++ b/src/lib/metrics.test.ts @@ -52,11 +52,6 @@ beforeAll(async () => { server: { serverMetrics: true, }, - experimental: { - flags: { - featureLifecycleMetrics: true, - }, - }, }); stores = createStores(); eventStore = stores.eventStore; diff --git a/src/lib/server-impl.test.ts b/src/lib/server-impl.test.ts index 66d522ff61..36effa95f8 100644 --- a/src/lib/server-impl.test.ts +++ b/src/lib/server-impl.test.ts @@ -1,7 +1,6 @@ import express from 'express'; import { createTestConfig } from '../test/config/test-config'; import { create, start } from './server-impl'; -import FakeEventStore from '../test/fixtures/fake-event-store'; jest.mock( './routes', @@ -15,7 +14,6 @@ jest.mock( const noop = () => {}; -const eventStore = new FakeEventStore(); const settingStore = { get: () => { Promise.resolve('secret'); @@ -34,21 +32,20 @@ jest.mock('./metrics', () => ({ }, })); +jest.mock('./services', () => ({ + createServices() { + return { + featureLifecycleService: { listen() {} }, + schedulerService: { stop() {}, start() {} }, + addonService: { destroy() {} }, + }; + }, +})); + jest.mock('./db', () => ({ createStores() { return { - db: { - destroy: () => undefined, - }, - clientInstanceStore: { - destroy: noop, - removeInstancesOlderThanTwoDays: noop, - }, - clientMetricsStore: { destroy: noop, on: noop }, - eventStore, - publicSignupTokenStore: { destroy: noop, on: noop }, settingStore, - projectStore: { getAll: () => Promise.resolve([]) }, }; }, })); diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 16677f115e..df52c4d1eb 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -52,8 +52,6 @@ export type IFlagKey = | 'displayEdgeBanner' | 'disableShowContextFieldSelectionValues' | 'projectOverviewRefactorFeedback' - | 'featureLifecycle' - | 'featureLifecycleMetrics' | 'parseProjectFromSession' | 'manyStrategiesPagination' | 'enableLegacyVariants' @@ -267,10 +265,6 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_PROJECT_OVERVIEW_REFACTOR_FEEDBACK, false, ), - featureLifecycle: parseEnvVarBoolean( - process.env.UNLEASH_EXPERIMENTAL_FEATURE_LIFECYCLE, - false, - ), parseProjectFromSession: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_PARSE_PROJECT_FROM_SESSION, false, diff --git a/src/server-dev.ts b/src/server-dev.ts index 60842e4fae..e6a31f6c31 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -48,7 +48,6 @@ process.nextTick(async () => { outdatedSdksBanner: true, disableShowContextFieldSelectionValues: false, projectOverviewRefactorFeedback: true, - featureLifecycle: true, parseProjectFromSession: true, manyStrategiesPagination: true, enableLegacyVariants: false,