1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-31 13:47:02 +02:00

chore: remove featureLifecycle and featureLifecycleMetrics flags (#7808)

This commit is contained in:
Mateusz Kwasniewski 2024-08-08 13:45:23 +02:00 committed by GitHub
parent fffed5d8dc
commit b65e593c23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 72 additions and 190 deletions

View File

@ -7,7 +7,6 @@ import Edit from '@mui/icons-material/Edit';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions'; import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useUiFlag } from 'hooks/useUiFlag';
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog'; import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
import { useState } from 'react'; import { useState } from 'react';
import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog'; import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog';
@ -98,7 +97,6 @@ const FeatureOverviewMetaData = () => {
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
const { feature, refetchFeature } = useFeature(projectId, featureId); const { feature, refetchFeature } = useFeature(projectId, featureId);
const { project, description, type } = feature; const { project, description, type } = feature;
const featureLifecycleEnabled = useUiFlag('featureLifecycle');
const navigate = useNavigate(); const navigate = useNavigate();
const [showDelDialog, setShowDelDialog] = useState(false); const [showDelDialog, setShowDelDialog] = useState(false);
const [showMarkCompletedDialogue, setShowMarkCompletedDialogue] = const [showMarkCompletedDialogue, setShowMarkCompletedDialogue] =
@ -142,10 +140,7 @@ const FeatureOverviewMetaData = () => {
<Box sx={{ wordBreak: 'break-all' }}>{project}</Box> <Box sx={{ wordBreak: 'break-all' }}>{project}</Box>
</SpacedBodyItem> </SpacedBodyItem>
<ConditionallyRender <ConditionallyRender
condition={ condition={Boolean(feature.lifecycle)}
featureLifecycleEnabled &&
Boolean(feature.lifecycle)
}
show={ show={
<SpacedBodyItem data-loading> <SpacedBodyItem data-loading>
<StyledLabel>Lifecycle:</StyledLabel> <StyledLabel>Lifecycle:</StyledLabel>

View File

@ -34,7 +34,6 @@ import { TableEmptyState } from './TableEmptyState/TableEmptyState';
import { useRowActions } from './hooks/useRowActions'; import { useRowActions } from './hooks/useRowActions';
import { useSelectedData } from './hooks/useSelectedData'; import { useSelectedData } from './hooks/useSelectedData';
import { FeatureOverviewCell } from '../../../common/Table/cells/FeatureOverviewCell/FeatureOverviewCell'; import { FeatureOverviewCell } from '../../../common/Table/cells/FeatureOverviewCell/FeatureOverviewCell';
import { useUiFlag } from 'hooks/useUiFlag';
import { import {
useProjectFeatureSearch, useProjectFeatureSearch,
useProjectFeatureSearchActions, useProjectFeatureSearchActions,
@ -55,7 +54,6 @@ export const ProjectFeatureToggles = ({
environments, environments,
}: IPaginatedProjectFeatureTogglesProps) => { }: IPaginatedProjectFeatureTogglesProps) => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const featureLifecycleEnabled = useUiFlag('featureLifecycle');
const { const {
features, features,
@ -192,36 +190,30 @@ export const ProjectFeatureToggles = ({
width: '1%', width: '1%',
}, },
}), }),
...(featureLifecycleEnabled columnHelper.accessor('lifecycle', {
? [ id: 'lifecycle',
columnHelper.accessor('lifecycle', { header: 'Lifecycle',
id: 'lifecycle', cell: ({ row: { original } }) => (
header: 'Lifecycle', <FeatureLifecycleCell
cell: ({ row: { original } }) => ( feature={original}
<FeatureLifecycleCell onComplete={() => {
feature={original} setShowMarkCompletedDialogue({
onComplete={() => { featureId: original.name,
setShowMarkCompletedDialogue({ open: true,
featureId: original.name, });
open: true, }}
}); onUncomplete={refetch}
}} onArchive={() => setFeatureArchiveState(original.name)}
onUncomplete={refetch} data-loading
onArchive={() => />
setFeatureArchiveState(original.name) ),
} enableSorting: false,
data-loading size: 50,
/> meta: {
), align: 'center',
enableSorting: false, width: '1%',
size: 50, },
meta: { }),
align: 'center',
width: '1%',
},
}),
]
: []),
...environments.map((name: string) => { ...environments.map((name: string) => {
const isChangeRequestEnabled = isChangeRequestConfigured(name); const isChangeRequestEnabled = isChangeRequestConfigured(name);
@ -302,7 +294,6 @@ export const ProjectFeatureToggles = ({
tableState.favoritesFirst, tableState.favoritesFirst,
refetch, refetch,
isPlaceholder, isPlaceholder,
featureLifecycleEnabled,
], ],
); );
@ -430,16 +421,11 @@ export const ProjectFeatureToggles = ({
id: 'lastSeenAt', id: 'lastSeenAt',
isVisible: columnVisibility.lastSeenAt, isVisible: columnVisibility.lastSeenAt,
}, },
...(featureLifecycleEnabled {
? [ header: 'Lifecycle',
{ id: 'lifecycle',
header: 'Lifecycle', isVisible: columnVisibility.lifecycle,
id: 'lifecycle', },
isVisible:
columnVisibility.lifecycle,
},
]
: []),
{ {
id: 'divider', id: 'divider',
}, },

View File

@ -1,7 +1,6 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useMediaQuery, useTheme } from '@mui/material'; import { useMediaQuery, useTheme } from '@mui/material';
import type { VisibilityState } from '@tanstack/react-table'; import type { VisibilityState } from '@tanstack/react-table';
import { useUiFlag } from 'hooks/useUiFlag';
const staticColumns = ['select', 'actions', 'name', 'favorite']; const staticColumns = ['select', 'actions', 'name', 'favorite'];
@ -22,7 +21,6 @@ export const useDefaultColumnVisibility = (allColumnIds: string[]) => {
const isTinyScreen = useMediaQuery(theme.breakpoints.down('sm')); const isTinyScreen = useMediaQuery(theme.breakpoints.down('sm'));
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg')); const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
const featureLifecycleEnabled = useUiFlag('featureLifecycle');
const showEnvironments = useCallback( const showEnvironments = useCallback(
(environmentsToShow: number = 0) => (environmentsToShow: number = 0) =>
@ -55,7 +53,7 @@ export const useDefaultColumnVisibility = (allColumnIds: string[]) => {
return formatAsColumnVisibility(allColumnIds, [ return formatAsColumnVisibility(allColumnIds, [
...staticColumns, ...staticColumns,
'lastSeenAt', 'lastSeenAt',
...(featureLifecycleEnabled ? ['lifecycle'] : []), 'lifecycle',
'createdAt', 'createdAt',
'createdBy', 'createdBy',
'type', 'type',

View File

@ -105,7 +105,6 @@ exports[`should create default config 1`] = `
"extendedMetrics": false, "extendedMetrics": false,
"extendedUsageMetrics": false, "extendedUsageMetrics": false,
"featureCollaborators": false, "featureCollaborators": false,
"featureLifecycle": false,
"featureSearchFeedback": { "featureSearchFeedback": {
"enabled": false, "enabled": false,
"name": "withText", "name": "withText",

View File

@ -19,7 +19,6 @@ import {
} from '../../openapi'; } from '../../openapi';
import Controller from '../../routes/controller'; import Controller from '../../routes/controller';
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { NotFoundError } from '../../error';
import type { IAuthRequest } from '../../routes/unleash-types'; import type { IAuthRequest } from '../../routes/unleash-types';
import type { WithTransactional } from '../../db/transaction'; import type { WithTransactional } from '../../db/transaction';
@ -120,9 +119,6 @@ export default class FeatureLifecycleController extends Controller {
req: Request<FeatureLifecycleParams, any, any, any>, req: Request<FeatureLifecycleParams, any, any, any>,
res: Response<FeatureLifecycleSchema>, res: Response<FeatureLifecycleSchema>,
): Promise<void> { ): Promise<void> {
if (!this.flagResolver.isEnabled('featureLifecycle')) {
throw new NotFoundError('Feature lifecycle is disabled.');
}
const { featureName } = req.params; const { featureName } = req.params;
const result = const result =
@ -144,9 +140,6 @@ export default class FeatureLifecycleController extends Controller {
>, >,
res: Response, res: Response,
): Promise<void> { ): Promise<void> {
if (!this.flagResolver.isEnabled('featureLifecycle')) {
throw new NotFoundError('Feature lifecycle is disabled.');
}
const { featureName, projectId } = req.params; const { featureName, projectId } = req.params;
const status = req.body; const status = req.body;
@ -162,9 +155,6 @@ export default class FeatureLifecycleController extends Controller {
req: IAuthRequest<FeatureLifecycleParams>, req: IAuthRequest<FeatureLifecycleParams>,
res: Response, res: Response,
): Promise<void> { ): Promise<void> {
if (!this.flagResolver.isEnabled('featureLifecycle')) {
throw new NotFoundError('Feature lifecycle is disabled.');
}
const { featureName, projectId } = req.params; const { featureName, projectId } = req.params;
await this.featureLifecycleService.transactional((service) => await this.featureLifecycleService.transactional((service) =>

View File

@ -36,10 +36,6 @@ export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel {
} }
async getStageCount(): Promise<StageCount[]> { async getStageCount(): Promise<StageCount[]> {
if (!this.flagResolver.isEnabled('featureLifecycleMetrics')) {
return [];
}
const { rows } = await this.db.raw(` const { rows } = await this.db.raw(`
SELECT SELECT
stage, stage,
@ -65,10 +61,6 @@ export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel {
} }
async getStageCountByProject(): Promise<StageCountByProject[]> { async getStageCountByProject(): Promise<StageCountByProject[]> {
if (!this.flagResolver.isEnabled('featureLifecycleMetrics')) {
return [];
}
const { rows } = await this.db.raw(` const { rows } = await this.db.raw(`
SELECT SELECT
f.project, f.project,
@ -133,10 +125,6 @@ export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel {
public async getAllWithStageDuration(): Promise< public async getAllWithStageDuration(): Promise<
IProjectLifecycleStageDuration[] IProjectLifecycleStageDuration[]
> { > {
if (!this.flagResolver.isEnabled('featureLifecycleMetrics')) {
return [];
}
const featureLifeCycles = await this.getAll(); const featureLifeCycles = await this.getAll();
return calculateStageDurations(featureLifeCycles); return calculateStageDurations(featureLifeCycles);
} }

View File

@ -1,7 +1,6 @@
import { import {
CLIENT_METRICS_ADDED, CLIENT_METRICS_ADDED,
FEATURE_ARCHIVED, FEATURE_ARCHIVED,
FEATURE_COMPLETED,
FEATURE_CREATED, FEATURE_CREATED,
FEATURE_REVIVED, FEATURE_REVIVED,
type IEnvironment, type IEnvironment,
@ -101,33 +100,3 @@ test('can insert and read lifecycle stages', async () => {
{ stage: 'initial', enteredStageAt: expect.any(Date) }, { 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([]);
});

View File

@ -77,19 +77,10 @@ export class FeatureLifecycleService {
); );
} }
private async checkEnabled(fn: () => Promise<void>) {
const enabled = this.flagResolver.isEnabled('featureLifecycle');
if (enabled) {
return fn();
}
}
listen() { listen() {
void this.checkEnabled(() => this.featureLifecycleStore.backfill()); this.featureLifecycleStore.backfill();
this.eventStore.on(FEATURE_CREATED, async (event) => { this.eventStore.on(FEATURE_CREATED, async (event) => {
await this.checkEnabled(() => await this.featureInitialized(event.featureName);
this.featureInitialized(event.featureName),
);
}); });
this.eventBus.on( this.eventBus.on(
CLIENT_METRICS_ADDED, CLIENT_METRICS_ADDED,
@ -103,22 +94,19 @@ export class FeatureLifecycleService {
const features = metrics.map( const features = metrics.map(
(metric) => metric.featureName, (metric) => metric.featureName,
); );
await this.checkEnabled(() => await this.featuresReceivedMetrics(
this.featuresReceivedMetrics(features, environment), features,
environment,
); );
} }
} }
}, },
); );
this.eventStore.on(FEATURE_ARCHIVED, async (event) => { this.eventStore.on(FEATURE_ARCHIVED, async (event) => {
await this.checkEnabled(() => await this.featureArchived(event.featureName);
this.featureArchived(event.featureName),
);
}); });
this.eventStore.on(FEATURE_REVIVED, async (event) => { this.eventStore.on(FEATURE_REVIVED, async (event) => {
await this.checkEnabled(() => await this.featureRevived(event.featureName);
this.featureRevived(event.featureName),
);
}); });
} }

View File

@ -32,9 +32,7 @@ beforeAll(async () => {
db.stores, db.stores,
{ {
experimental: { experimental: {
flags: { flags: {},
featureLifecycle: true,
},
}, },
}, },
db.rawDatabase, db.rawDatabase,

View File

@ -11,7 +11,6 @@ import type {
} from '../../types'; } from '../../types';
import FeatureToggleStore from '../feature-toggle/feature-toggle-store'; import FeatureToggleStore from '../feature-toggle/feature-toggle-store';
import type { Db } from '../../db/db'; import type { Db } from '../../db/db';
import Raw = Knex.Raw;
import type { import type {
IFeatureSearchParams, IFeatureSearchParams,
IQueryParam, IQueryParam,
@ -19,6 +18,7 @@ import type {
import { applyGenericQueryParams, applySearchFilters } from './search-utils'; import { applyGenericQueryParams, applySearchFilters } from './search-utils';
import type { FeatureSearchEnvironmentSchema } from '../../openapi/spec/feature-search-environment-schema'; import type { FeatureSearchEnvironmentSchema } from '../../openapi/spec/feature-search-environment-schema';
import { generateImageUrl } from '../../util'; import { generateImageUrl } from '../../util';
import Raw = Knex.Raw;
const sortEnvironments = (overview: IFeatureSearchOverview[]) => { const sortEnvironments = (overview: IFeatureSearchOverview[]) => {
return overview.map((data: IFeatureSearchOverview) => ({ return overview.map((data: IFeatureSearchOverview) => ({
@ -112,9 +112,6 @@ class FeatureSearchStore implements IFeatureSearchStore {
const validatedSortOrder = const validatedSortOrder =
sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc'; sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
const featureLifecycleEnabled =
this.flagResolver.isEnabled('featureLifecycle');
const finalQuery = this.db const finalQuery = this.db
.with('ranked_features', (query) => { .with('ranked_features', (query) => {
query.from('features'); query.from('features');
@ -315,27 +312,22 @@ class FeatureSearchStore implements IFeatureSearchStore {
.joinRaw('CROSS JOIN total_features') .joinRaw('CROSS JOIN total_features')
.whereBetween('final_rank', [offset + 1, offset + limit]) .whereBetween('final_rank', [offset + 1, offset + limit])
.orderBy('final_rank'); .orderBy('final_rank');
if (featureLifecycleEnabled) { finalQuery
finalQuery .select(
.select( 'lifecycle.latest_stage',
'lifecycle.latest_stage', 'lifecycle.stage_status',
'lifecycle.stage_status', 'lifecycle.entered_stage_at',
'lifecycle.entered_stage_at', )
) .leftJoin(
.leftJoin( 'lifecycle',
'lifecycle', 'ranked_features.feature_name',
'ranked_features.feature_name', 'lifecycle.stage_feature',
'lifecycle.stage_feature', );
);
}
this.queryExtraData(finalQuery); this.queryExtraData(finalQuery);
const rows = await finalQuery; const rows = await finalQuery;
stopTimer(); stopTimer();
if (rows.length > 0) { if (rows.length > 0) {
const overview = this.getAggregatedSearchData( const overview = this.getAggregatedSearchData(rows);
rows,
featureLifecycleEnabled,
);
const features = sortEnvironments( const features = sortEnvironments(
overview, overview,
) as IFeatureSearchOverview[]; ) as IFeatureSearchOverview[];
@ -461,10 +453,7 @@ class FeatureSearchStore implements IFeatureSearchStore {
return rankingSql; return rankingSql;
} }
getAggregatedSearchData( getAggregatedSearchData(rows): IFeatureSearchOverview[] {
rows,
featureLifecycleEnabled: boolean,
): IFeatureSearchOverview[] {
const entriesMap: Map<string, IFeatureSearchOverview> = new Map(); const entriesMap: Map<string, IFeatureSearchOverview> = new Map();
const orderedEntries: IFeatureSearchOverview[] = []; const orderedEntries: IFeatureSearchOverview[] = [];
@ -501,17 +490,15 @@ class FeatureSearchStore implements IFeatureSearchStore {
}), }),
}, },
}; };
if (featureLifecycleEnabled) { entry.lifecycle = row.latest_stage
entry.lifecycle = row.latest_stage ? {
? { stage: row.latest_stage,
stage: row.latest_stage, ...(row.stage_status
...(row.stage_status ? { status: row.stage_status }
? { status: row.stage_status } : {}),
: {}), enteredStageAt: row.entered_stage_at,
enteredStageAt: row.entered_stage_at, }
} : undefined;
: undefined;
}
entriesMap.set(row.feature_name, entry); entriesMap.set(row.feature_name, entry);
orderedEntries.push(entry); orderedEntries.push(entry);
} }

View File

@ -21,7 +21,6 @@ beforeAll(async () => {
experimental: { experimental: {
flags: { flags: {
strictSchemaValidation: true, strictSchemaValidation: true,
featureLifecycle: true,
anonymiseEventLog: true, anonymiseEventLog: true,
}, },
}, },

View File

@ -52,11 +52,6 @@ beforeAll(async () => {
server: { server: {
serverMetrics: true, serverMetrics: true,
}, },
experimental: {
flags: {
featureLifecycleMetrics: true,
},
},
}); });
stores = createStores(); stores = createStores();
eventStore = stores.eventStore; eventStore = stores.eventStore;

View File

@ -1,7 +1,6 @@
import express from 'express'; import express from 'express';
import { createTestConfig } from '../test/config/test-config'; import { createTestConfig } from '../test/config/test-config';
import { create, start } from './server-impl'; import { create, start } from './server-impl';
import FakeEventStore from '../test/fixtures/fake-event-store';
jest.mock( jest.mock(
'./routes', './routes',
@ -15,7 +14,6 @@ jest.mock(
const noop = () => {}; const noop = () => {};
const eventStore = new FakeEventStore();
const settingStore = { const settingStore = {
get: () => { get: () => {
Promise.resolve('secret'); 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', () => ({ jest.mock('./db', () => ({
createStores() { createStores() {
return { return {
db: {
destroy: () => undefined,
},
clientInstanceStore: {
destroy: noop,
removeInstancesOlderThanTwoDays: noop,
},
clientMetricsStore: { destroy: noop, on: noop },
eventStore,
publicSignupTokenStore: { destroy: noop, on: noop },
settingStore, settingStore,
projectStore: { getAll: () => Promise.resolve([]) },
}; };
}, },
})); }));

View File

@ -52,8 +52,6 @@ export type IFlagKey =
| 'displayEdgeBanner' | 'displayEdgeBanner'
| 'disableShowContextFieldSelectionValues' | 'disableShowContextFieldSelectionValues'
| 'projectOverviewRefactorFeedback' | 'projectOverviewRefactorFeedback'
| 'featureLifecycle'
| 'featureLifecycleMetrics'
| 'parseProjectFromSession' | 'parseProjectFromSession'
| 'manyStrategiesPagination' | 'manyStrategiesPagination'
| 'enableLegacyVariants' | 'enableLegacyVariants'
@ -267,10 +265,6 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_PROJECT_OVERVIEW_REFACTOR_FEEDBACK, process.env.UNLEASH_EXPERIMENTAL_PROJECT_OVERVIEW_REFACTOR_FEEDBACK,
false, false,
), ),
featureLifecycle: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_FEATURE_LIFECYCLE,
false,
),
parseProjectFromSession: parseEnvVarBoolean( parseProjectFromSession: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_PARSE_PROJECT_FROM_SESSION, process.env.UNLEASH_EXPERIMENTAL_PARSE_PROJECT_FROM_SESSION,
false, false,

View File

@ -48,7 +48,6 @@ process.nextTick(async () => {
outdatedSdksBanner: true, outdatedSdksBanner: true,
disableShowContextFieldSelectionValues: false, disableShowContextFieldSelectionValues: false,
projectOverviewRefactorFeedback: true, projectOverviewRefactorFeedback: true,
featureLifecycle: true,
parseProjectFromSession: true, parseProjectFromSession: true,
manyStrategiesPagination: true, manyStrategiesPagination: true,
enableLegacyVariants: false, enableLegacyVariants: false,