mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-04 01:18:20 +02:00
Refactor/last seen at ft list (#5089)
Refactor last seen at for the feature toggle list
This commit is contained in:
parent
1ca3e8d893
commit
7195a63e56
@ -2,10 +2,10 @@ import {
|
|||||||
PartialDeep,
|
PartialDeep,
|
||||||
IFeatureToggleClient,
|
IFeatureToggleClient,
|
||||||
IStrategyConfig,
|
IStrategyConfig,
|
||||||
FeatureToggle,
|
|
||||||
IFeatureToggleQuery,
|
IFeatureToggleQuery,
|
||||||
ITag,
|
ITag,
|
||||||
IFlagResolver,
|
IFlagResolver,
|
||||||
|
IFeatureToggleListItem,
|
||||||
} from '../../../types';
|
} from '../../../types';
|
||||||
|
|
||||||
import { mapValues, ensureStringValue } from '../../../util';
|
import { mapValues, ensureStringValue } from '../../../util';
|
||||||
@ -69,6 +69,31 @@ export class FeatureToggleRowConverter {
|
|||||||
strategy.segments.push(row.segment_id);
|
strategy.segments.push(row.segment_id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
addLastSeenByEnvironment = (
|
||||||
|
feature: PartialDeep<IFeatureToggleListItem>,
|
||||||
|
row: Record<string, any>,
|
||||||
|
) => {
|
||||||
|
if (!feature.environments) {
|
||||||
|
feature.environments = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const found = feature.environments.find(
|
||||||
|
(environment) => environment?.name === row.last_seen_at_env,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEnvironment = {
|
||||||
|
name: row.last_seen_at_env,
|
||||||
|
lastSeenAt: row.env_last_seen_at,
|
||||||
|
enabled: row.enabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
feature.environments.push(newEnvironment);
|
||||||
|
};
|
||||||
|
|
||||||
rowToStrategy = (row: Record<string, any>): IStrategyConfig => {
|
rowToStrategy = (row: Record<string, any>): IStrategyConfig => {
|
||||||
let strategy: IStrategyConfig;
|
let strategy: IStrategyConfig;
|
||||||
if (this.flagResolver.isEnabled('playgroundImprovements')) {
|
if (this.flagResolver.isEnabled('playgroundImprovements')) {
|
||||||
@ -166,9 +191,9 @@ export class FeatureToggleRowConverter {
|
|||||||
rows: any[],
|
rows: any[],
|
||||||
featureQuery?: IFeatureToggleQuery,
|
featureQuery?: IFeatureToggleQuery,
|
||||||
includeDisabledStrategies?: boolean,
|
includeDisabledStrategies?: boolean,
|
||||||
): FeatureToggle[] => {
|
): IFeatureToggleListItem[] => {
|
||||||
const result = rows.reduce((acc, r) => {
|
const result = rows.reduce((acc, r) => {
|
||||||
let feature: PartialDeep<IFeatureToggleClient> = acc[r.name] ?? {
|
let feature: PartialDeep<IFeatureToggleListItem> = acc[r.name] ?? {
|
||||||
strategies: [],
|
strategies: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -182,6 +207,10 @@ export class FeatureToggleRowConverter {
|
|||||||
feature.createdAt = r.created_at;
|
feature.createdAt = r.created_at;
|
||||||
feature.favorite = r.favorite;
|
feature.favorite = r.favorite;
|
||||||
|
|
||||||
|
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
|
||||||
|
this.addLastSeenByEnvironment(feature, r);
|
||||||
|
}
|
||||||
|
|
||||||
acc[r.name] = feature;
|
acc[r.name] = feature;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
@ -164,8 +164,6 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getPlaygroundFeatures(
|
async getPlaygroundFeatures(
|
||||||
dependentFeaturesEnabled: boolean,
|
|
||||||
includeDisabledStrategies: boolean,
|
|
||||||
query?: IFeatureToggleQuery,
|
query?: IFeatureToggleQuery,
|
||||||
): Promise<FeatureConfigurationClient[]> {
|
): Promise<FeatureConfigurationClient[]> {
|
||||||
return this.features.filter(
|
return this.features.filter(
|
||||||
|
@ -1055,11 +1055,7 @@ class FeatureToggleService {
|
|||||||
const [featuresFromClientStore, featuresFromFeatureToggleStore] =
|
const [featuresFromClientStore, featuresFromFeatureToggleStore] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
await this.clientFeatureToggleStore.getPlayground(query || {}),
|
await this.clientFeatureToggleStore.getPlayground(query || {}),
|
||||||
await this.featureToggleStore.getPlaygroundFeatures(
|
await this.featureToggleStore.getPlaygroundFeatures(query),
|
||||||
this.flagResolver.isEnabled('dependentFeatures'),
|
|
||||||
this.flagResolver.isEnabled('playgroundImprovements'),
|
|
||||||
query,
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const equal = isEqual(
|
const equal = isEqual(
|
||||||
|
@ -20,6 +20,7 @@ import { FeatureToggleListBuilder } from './query-builders/feature-toggle-list-b
|
|||||||
import { FeatureConfigurationClient } from './types/feature-toggle-strategies-store-type';
|
import { FeatureConfigurationClient } from './types/feature-toggle-strategies-store-type';
|
||||||
import { IFlagResolver } from '../../../lib/types';
|
import { IFlagResolver } from '../../../lib/types';
|
||||||
import { FeatureToggleRowConverter } from './converters/feature-toggle-row-converter';
|
import { FeatureToggleRowConverter } from './converters/feature-toggle-row-converter';
|
||||||
|
import FlagResolver from 'lib/util/flag-resolver';
|
||||||
|
|
||||||
export type EnvironmentFeatureNames = { [key: string]: string[] };
|
export type EnvironmentFeatureNames = { [key: string]: string[] };
|
||||||
|
|
||||||
@ -77,6 +78,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
|||||||
this.featureToggleRowConverter = new FeatureToggleRowConverter(
|
this.featureToggleRowConverter = new FeatureToggleRowConverter(
|
||||||
flagResolver,
|
flagResolver,
|
||||||
);
|
);
|
||||||
|
this.flagResolver = flagResolver;
|
||||||
this.timer = (action) =>
|
this.timer = (action) =>
|
||||||
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
||||||
store: 'feature-toggle',
|
store: 'feature-toggle',
|
||||||
@ -144,6 +146,16 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
|||||||
builder.addSelectColumn('ft.tag_value as tag_value');
|
builder.addSelectColumn('ft.tag_value as tag_value');
|
||||||
builder.addSelectColumn('ft.tag_type as tag_type');
|
builder.addSelectColumn('ft.tag_type as tag_type');
|
||||||
|
|
||||||
|
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
|
||||||
|
builder.withLastSeenByEnvironment();
|
||||||
|
builder.addSelectColumn(
|
||||||
|
'last_seen_at_metrics.last_seen_at as env_last_seen_at',
|
||||||
|
);
|
||||||
|
builder.addSelectColumn(
|
||||||
|
'last_seen_at_metrics.environment as last_seen_at_env',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (userId) {
|
if (userId) {
|
||||||
builder.withFavorites(userId);
|
builder.withFavorites(userId);
|
||||||
builder.addSelectColumn(
|
builder.addSelectColumn(
|
||||||
@ -165,8 +177,6 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getPlaygroundFeatures(
|
async getPlaygroundFeatures(
|
||||||
dependentFeaturesEnabled: boolean,
|
|
||||||
includeDisabledStrategies: boolean,
|
|
||||||
featureQuery: IFeatureToggleQuery,
|
featureQuery: IFeatureToggleQuery,
|
||||||
): Promise<FeatureConfigurationClient[]> {
|
): Promise<FeatureConfigurationClient[]> {
|
||||||
const environment = featureQuery?.environment || DEFAULT_ENV;
|
const environment = featureQuery?.environment || DEFAULT_ENV;
|
||||||
@ -174,6 +184,12 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
|||||||
const archived = false;
|
const archived = false;
|
||||||
const builder = this.getBaseFeatureQuery(archived, environment);
|
const builder = this.getBaseFeatureQuery(archived, environment);
|
||||||
|
|
||||||
|
const dependentFeaturesEnabled =
|
||||||
|
this.flagResolver.isEnabled('dependentFeatures');
|
||||||
|
const includeDisabledStrategies = this.flagResolver.isEnabled(
|
||||||
|
'playgroundImprovements',
|
||||||
|
);
|
||||||
|
|
||||||
if (dependentFeaturesEnabled) {
|
if (dependentFeaturesEnabled) {
|
||||||
builder.withDependentFeatureToggles();
|
builder.withDependentFeatureToggles();
|
||||||
|
|
||||||
|
@ -115,6 +115,12 @@ export class FeatureToggleListBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withLastSeenByEnvironment = () => {
|
||||||
|
this.internalQuery.leftJoin('last_seen_at_metrics', 'last_seen_at_metrics.feature_name', 'features.name');
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
withFavorites = (userId: number) => {
|
withFavorites = (userId: number) => {
|
||||||
this.internalQuery.leftJoin(`favorite_features`, function () {
|
this.internalQuery.leftJoin(`favorite_features`, function () {
|
||||||
this.on('favorite_features.feature', 'features.name').andOnVal(
|
this.on('favorite_features.feature', 'features.name').andOnVal(
|
||||||
|
@ -0,0 +1,115 @@
|
|||||||
|
import dbInit, { ITestDb } from '../../../../test/e2e/helpers/database-init';
|
||||||
|
import {
|
||||||
|
IUnleashTest,
|
||||||
|
insertLastSeenAt,
|
||||||
|
setupAppWithCustomConfig,
|
||||||
|
} from '../../../../test/e2e/helpers/test-helper';
|
||||||
|
import getLogger from '../../../../test/fixtures/no-logger';
|
||||||
|
|
||||||
|
let app: IUnleashTest;
|
||||||
|
let db: ITestDb;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const config = {
|
||||||
|
experimental: {
|
||||||
|
flags: {
|
||||||
|
strictSchemaValidation: true,
|
||||||
|
dependentFeatures: true,
|
||||||
|
separateAdminClientApi: true,
|
||||||
|
useLastSeenRefactor: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
db = await dbInit(
|
||||||
|
'feature_toggles_last_seen_at_refactor',
|
||||||
|
getLogger,
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
app = await setupAppWithCustomConfig(db.stores, config, db.rawDatabase);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
await db.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return last seen at per env for /api/admin/features', async () => {
|
||||||
|
await app.createFeature('lastSeenAtPerEnv');
|
||||||
|
|
||||||
|
await insertLastSeenAt('lastSeenAtPerEnv', db.rawDatabase, 'default');
|
||||||
|
|
||||||
|
const response = await app.request
|
||||||
|
.get('/api/admin/features')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const found = await response.body.features.find(
|
||||||
|
(featureToggle) => featureToggle.name === 'lastSeenAtPerEnv',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(found.environments[0].lastSeenAt).toEqual(
|
||||||
|
'2023-10-01T12:34:56.000Z',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('response should include last seen at per environment for multiple environments', async () => {
|
||||||
|
await db.stores.environmentStore.create({
|
||||||
|
name: 'development',
|
||||||
|
type: 'development',
|
||||||
|
sortOrder: 1,
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.stores.environmentStore.create({
|
||||||
|
name: 'production',
|
||||||
|
type: 'production',
|
||||||
|
sortOrder: 2,
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.services.projectService.addEnvironmentToProject(
|
||||||
|
'default',
|
||||||
|
'development',
|
||||||
|
);
|
||||||
|
await app.services.projectService.addEnvironmentToProject(
|
||||||
|
'default',
|
||||||
|
'production',
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.createFeature('multiple-environment-last-seen-at');
|
||||||
|
|
||||||
|
await insertLastSeenAt(
|
||||||
|
'multiple-environment-last-seen-at',
|
||||||
|
db.rawDatabase,
|
||||||
|
'default',
|
||||||
|
);
|
||||||
|
await insertLastSeenAt(
|
||||||
|
'multiple-environment-last-seen-at',
|
||||||
|
db.rawDatabase,
|
||||||
|
'development',
|
||||||
|
);
|
||||||
|
await insertLastSeenAt(
|
||||||
|
'multiple-environment-last-seen-at',
|
||||||
|
db.rawDatabase,
|
||||||
|
'production',
|
||||||
|
);
|
||||||
|
|
||||||
|
const { body } = await app.request
|
||||||
|
.get('/api/admin/features')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const featureEnvironments = body.features[1].environments;
|
||||||
|
|
||||||
|
const [def, development, production] = featureEnvironments;
|
||||||
|
|
||||||
|
expect(def.name).toBe('default');
|
||||||
|
expect(def.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
|
||||||
|
|
||||||
|
expect(development.name).toBe('development');
|
||||||
|
expect(development.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
|
||||||
|
|
||||||
|
expect(production.name).toBe('production');
|
||||||
|
expect(production.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
|
||||||
|
});
|
@ -15,9 +15,9 @@ import { ForbiddenError, PatternError, PermissionError } from '../../../error';
|
|||||||
import { ISegmentService } from '../../../segments/segment-service-interface';
|
import { ISegmentService } from '../../../segments/segment-service-interface';
|
||||||
import { createFeatureToggleService, createSegmentService } from '../..';
|
import { createFeatureToggleService, createSegmentService } from '../..';
|
||||||
import {
|
import {
|
||||||
insertFeatureEnvironmentsLastSeen,
|
|
||||||
insertLastSeenAt,
|
insertLastSeenAt,
|
||||||
} from '../../../../test/e2e/api/admin/project/projects.e2e.test';
|
insertFeatureEnvironmentsLastSeen,
|
||||||
|
} from '../../../../test/e2e/helpers/test-helper';
|
||||||
|
|
||||||
let stores: IUnleashStores;
|
let stores: IUnleashStores;
|
||||||
let db;
|
let db;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import dbInit, { ITestDb } from '../../../../test/e2e/helpers/database-init';
|
import dbInit, { ITestDb } from '../../../../test/e2e/helpers/database-init';
|
||||||
import {
|
import {
|
||||||
IUnleashTest,
|
IUnleashTest,
|
||||||
|
insertLastSeenAt,
|
||||||
setupAppWithCustomConfig,
|
setupAppWithCustomConfig,
|
||||||
} from '../../../../test/e2e/helpers/test-helper';
|
} from '../../../../test/e2e/helpers/test-helper';
|
||||||
import getLogger from '../../../../test/fixtures/no-logger';
|
import getLogger from '../../../../test/fixtures/no-logger';
|
||||||
|
@ -38,8 +38,6 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
|
|||||||
archived?: boolean,
|
archived?: boolean,
|
||||||
): Promise<FeatureToggle[]>;
|
): Promise<FeatureToggle[]>;
|
||||||
getPlaygroundFeatures(
|
getPlaygroundFeatures(
|
||||||
dependentFeaturesEnabled: boolean,
|
|
||||||
includeDisabledStrategies: boolean,
|
|
||||||
featureQuery?: IFeatureToggleQuery,
|
featureQuery?: IFeatureToggleQuery,
|
||||||
): Promise<FeatureConfigurationClient[]>;
|
): Promise<FeatureConfigurationClient[]>;
|
||||||
countByDate(queryModifiers: {
|
countByDate(queryModifiers: {
|
||||||
|
@ -67,6 +67,11 @@ export interface FeatureToggle extends FeatureToggleDTO {
|
|||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IFeatureToggleListItem extends FeatureToggle {
|
||||||
|
environments?: Partial<IEnvironmentBase>[];
|
||||||
|
favorite: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IFeatureToggleClient {
|
export interface IFeatureToggleClient {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
@ -1,45 +1,19 @@
|
|||||||
import dbInit, { ITestDb } from '../../../helpers/database-init';
|
import dbInit, { ITestDb } from '../../../helpers/database-init';
|
||||||
import {
|
import {
|
||||||
IUnleashTest,
|
IUnleashTest,
|
||||||
|
insertFeatureEnvironmentsLastSeen,
|
||||||
|
insertLastSeenAt,
|
||||||
setupAppWithCustomConfig,
|
setupAppWithCustomConfig,
|
||||||
} from '../../../helpers/test-helper';
|
} from '../../../helpers/test-helper';
|
||||||
import getLogger from '../../../../fixtures/no-logger';
|
import getLogger from '../../../../fixtures/no-logger';
|
||||||
|
|
||||||
import { IProjectStore } from 'lib/types';
|
import { IProjectStore } from 'lib/types';
|
||||||
import { Knex } from 'knex';
|
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
|
|
||||||
let projectStore: IProjectStore;
|
let projectStore: IProjectStore;
|
||||||
|
|
||||||
export const insertLastSeenAt = async (
|
|
||||||
featureName: string,
|
|
||||||
db: Knex,
|
|
||||||
environment: string = 'default',
|
|
||||||
date: string = '2023-10-01 12:34:56',
|
|
||||||
): Promise<string> => {
|
|
||||||
await db.raw(`INSERT INTO last_seen_at_metrics (feature_name, environment, last_seen_at)
|
|
||||||
VALUES ('${featureName}', '${environment}', '${date}');`);
|
|
||||||
|
|
||||||
return date;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const insertFeatureEnvironmentsLastSeen = async (
|
|
||||||
featureName: string,
|
|
||||||
db: Knex,
|
|
||||||
environment: string = 'default',
|
|
||||||
date: string = '2022-05-01 12:34:56',
|
|
||||||
): Promise<string> => {
|
|
||||||
await db.raw(`
|
|
||||||
INSERT INTO feature_environments (feature_name, environment, last_seen_at, enabled)
|
|
||||||
VALUES ('${featureName}', '${environment}', '${date}', true)
|
|
||||||
ON CONFLICT (feature_name, environment) DO UPDATE SET last_seen_at = '${date}', enabled = true;
|
|
||||||
`);
|
|
||||||
|
|
||||||
return date;
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('projects_api_serial', getLogger);
|
db = await dbInit('projects_api_serial', getLogger);
|
||||||
app = await setupAppWithCustomConfig(
|
app = await setupAppWithCustomConfig(
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
CreateFeatureStrategySchema,
|
CreateFeatureStrategySchema,
|
||||||
ImportTogglesSchema,
|
ImportTogglesSchema,
|
||||||
} from '../../../lib/openapi';
|
} from '../../../lib/openapi';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
@ -300,3 +301,30 @@ export async function setupAppWithBaseUrl(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const insertLastSeenAt = async (
|
||||||
|
featureName: string,
|
||||||
|
db: Knex,
|
||||||
|
environment: string = 'default',
|
||||||
|
date: string = '2023-10-01 12:34:56',
|
||||||
|
): Promise<string> => {
|
||||||
|
await db.raw(`INSERT INTO last_seen_at_metrics (feature_name, environment, last_seen_at)
|
||||||
|
VALUES ('${featureName}', '${environment}', '${date}');`);
|
||||||
|
|
||||||
|
return date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const insertFeatureEnvironmentsLastSeen = async (
|
||||||
|
featureName: string,
|
||||||
|
db: Knex,
|
||||||
|
environment: string = 'default',
|
||||||
|
date: string = '2022-05-01 12:34:56',
|
||||||
|
): Promise<string> => {
|
||||||
|
await db.raw(`
|
||||||
|
INSERT INTO feature_environments (feature_name, environment, last_seen_at, enabled)
|
||||||
|
VALUES ('${featureName}', '${environment}', '${date}', true)
|
||||||
|
ON CONFLICT (feature_name, environment) DO UPDATE SET last_seen_at = '${date}', enabled = true;
|
||||||
|
`);
|
||||||
|
|
||||||
|
return date;
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user