1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-15 01:16:22 +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 { 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 = () => {
<Box sx={{ wordBreak: 'break-all' }}>{project}</Box>
</SpacedBodyItem>
<ConditionallyRender
condition={
featureLifecycleEnabled &&
Boolean(feature.lifecycle)
}
condition={Boolean(feature.lifecycle)}
show={
<SpacedBodyItem data-loading>
<StyledLabel>Lifecycle:</StyledLabel>

View File

@ -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 } }) => (
<FeatureLifecycleCell
feature={original}
onComplete={() => {
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 } }) => (
<FeatureLifecycleCell
feature={original}
onComplete={() => {
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',
},

View File

@ -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',

View File

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

View File

@ -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<FeatureLifecycleParams, any, any, any>,
res: Response<FeatureLifecycleSchema>,
): Promise<void> {
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<void> {
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<FeatureLifecycleParams>,
res: Response,
): Promise<void> {
if (!this.flagResolver.isEnabled('featureLifecycle')) {
throw new NotFoundError('Feature lifecycle is disabled.');
}
const { featureName, projectId } = req.params;
await this.featureLifecycleService.transactional((service) =>

View File

@ -36,10 +36,6 @@ export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel {
}
async getStageCount(): Promise<StageCount[]> {
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<StageCountByProject[]> {
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);
}

View File

@ -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([]);
});

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() {
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);
});
}

View File

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

View File

@ -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<string, IFeatureSearchOverview> = 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);
}

View File

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

View File

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

View File

@ -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([]) },
};
},
}));

View File

@ -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,

View File

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