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:
parent
fffed5d8dc
commit
b65e593c23
@ -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>
|
||||
|
@ -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',
|
||||
},
|
||||
|
@ -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',
|
||||
|
@ -105,7 +105,6 @@ exports[`should create default config 1`] = `
|
||||
"extendedMetrics": false,
|
||||
"extendedUsageMetrics": false,
|
||||
"featureCollaborators": false,
|
||||
"featureLifecycle": false,
|
||||
"featureSearchFeedback": {
|
||||
"enabled": false,
|
||||
"name": "withText",
|
||||
|
@ -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) =>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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([]);
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -32,9 +32,7 @@ beforeAll(async () => {
|
||||
db.stores,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
featureLifecycle: true,
|
||||
},
|
||||
flags: {},
|
||||
},
|
||||
},
|
||||
db.rawDatabase,
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ beforeAll(async () => {
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
featureLifecycle: true,
|
||||
anonymiseEventLog: true,
|
||||
},
|
||||
},
|
||||
|
@ -52,11 +52,6 @@ beforeAll(async () => {
|
||||
server: {
|
||||
serverMetrics: true,
|
||||
},
|
||||
experimental: {
|
||||
flags: {
|
||||
featureLifecycleMetrics: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
stores = createStores();
|
||||
eventStore = stores.eventStore;
|
||||
|
@ -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([]) },
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
@ -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,
|
||||
|
@ -48,7 +48,6 @@ process.nextTick(async () => {
|
||||
outdatedSdksBanner: true,
|
||||
disableShowContextFieldSelectionValues: false,
|
||||
projectOverviewRefactorFeedback: true,
|
||||
featureLifecycle: true,
|
||||
parseProjectFromSession: true,
|
||||
manyStrategiesPagination: true,
|
||||
enableLegacyVariants: false,
|
||||
|
Loading…
Reference in New Issue
Block a user