mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-26 01:17:00 +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,
|
||||
IFeatureToggleClient,
|
||||
IStrategyConfig,
|
||||
FeatureToggle,
|
||||
IFeatureToggleQuery,
|
||||
ITag,
|
||||
IFlagResolver,
|
||||
IFeatureToggleListItem,
|
||||
} from '../../../types';
|
||||
|
||||
import { mapValues, ensureStringValue } from '../../../util';
|
||||
@ -69,6 +69,31 @@ export class FeatureToggleRowConverter {
|
||||
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 => {
|
||||
let strategy: IStrategyConfig;
|
||||
if (this.flagResolver.isEnabled('playgroundImprovements')) {
|
||||
@ -166,9 +191,9 @@ export class FeatureToggleRowConverter {
|
||||
rows: any[],
|
||||
featureQuery?: IFeatureToggleQuery,
|
||||
includeDisabledStrategies?: boolean,
|
||||
): FeatureToggle[] => {
|
||||
): IFeatureToggleListItem[] => {
|
||||
const result = rows.reduce((acc, r) => {
|
||||
let feature: PartialDeep<IFeatureToggleClient> = acc[r.name] ?? {
|
||||
let feature: PartialDeep<IFeatureToggleListItem> = acc[r.name] ?? {
|
||||
strategies: [],
|
||||
};
|
||||
|
||||
@ -182,6 +207,10 @@ export class FeatureToggleRowConverter {
|
||||
feature.createdAt = r.created_at;
|
||||
feature.favorite = r.favorite;
|
||||
|
||||
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
|
||||
this.addLastSeenByEnvironment(feature, r);
|
||||
}
|
||||
|
||||
acc[r.name] = feature;
|
||||
return acc;
|
||||
}, {});
|
||||
|
@ -164,8 +164,6 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
||||
}
|
||||
|
||||
async getPlaygroundFeatures(
|
||||
dependentFeaturesEnabled: boolean,
|
||||
includeDisabledStrategies: boolean,
|
||||
query?: IFeatureToggleQuery,
|
||||
): Promise<FeatureConfigurationClient[]> {
|
||||
return this.features.filter(
|
||||
|
@ -1055,11 +1055,7 @@ class FeatureToggleService {
|
||||
const [featuresFromClientStore, featuresFromFeatureToggleStore] =
|
||||
await Promise.all([
|
||||
await this.clientFeatureToggleStore.getPlayground(query || {}),
|
||||
await this.featureToggleStore.getPlaygroundFeatures(
|
||||
this.flagResolver.isEnabled('dependentFeatures'),
|
||||
this.flagResolver.isEnabled('playgroundImprovements'),
|
||||
query,
|
||||
),
|
||||
await this.featureToggleStore.getPlaygroundFeatures(query),
|
||||
]);
|
||||
|
||||
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 { IFlagResolver } from '../../../lib/types';
|
||||
import { FeatureToggleRowConverter } from './converters/feature-toggle-row-converter';
|
||||
import FlagResolver from 'lib/util/flag-resolver';
|
||||
|
||||
export type EnvironmentFeatureNames = { [key: string]: string[] };
|
||||
|
||||
@ -77,6 +78,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
||||
this.featureToggleRowConverter = new FeatureToggleRowConverter(
|
||||
flagResolver,
|
||||
);
|
||||
this.flagResolver = flagResolver;
|
||||
this.timer = (action) =>
|
||||
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
||||
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_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) {
|
||||
builder.withFavorites(userId);
|
||||
builder.addSelectColumn(
|
||||
@ -165,8 +177,6 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
||||
}
|
||||
|
||||
async getPlaygroundFeatures(
|
||||
dependentFeaturesEnabled: boolean,
|
||||
includeDisabledStrategies: boolean,
|
||||
featureQuery: IFeatureToggleQuery,
|
||||
): Promise<FeatureConfigurationClient[]> {
|
||||
const environment = featureQuery?.environment || DEFAULT_ENV;
|
||||
@ -174,6 +184,12 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
||||
const archived = false;
|
||||
const builder = this.getBaseFeatureQuery(archived, environment);
|
||||
|
||||
const dependentFeaturesEnabled =
|
||||
this.flagResolver.isEnabled('dependentFeatures');
|
||||
const includeDisabledStrategies = this.flagResolver.isEnabled(
|
||||
'playgroundImprovements',
|
||||
);
|
||||
|
||||
if (dependentFeaturesEnabled) {
|
||||
builder.withDependentFeatureToggles();
|
||||
|
||||
|
@ -115,6 +115,12 @@ export class FeatureToggleListBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
withLastSeenByEnvironment = () => {
|
||||
this.internalQuery.leftJoin('last_seen_at_metrics', 'last_seen_at_metrics.feature_name', 'features.name');
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
withFavorites = (userId: number) => {
|
||||
this.internalQuery.leftJoin(`favorite_features`, function () {
|
||||
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 { createFeatureToggleService, createSegmentService } from '../..';
|
||||
import {
|
||||
insertFeatureEnvironmentsLastSeen,
|
||||
insertLastSeenAt,
|
||||
} from '../../../../test/e2e/api/admin/project/projects.e2e.test';
|
||||
insertFeatureEnvironmentsLastSeen,
|
||||
} from '../../../../test/e2e/helpers/test-helper';
|
||||
|
||||
let stores: IUnleashStores;
|
||||
let db;
|
||||
|
@ -1,6 +1,7 @@
|
||||
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';
|
||||
|
@ -38,8 +38,6 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
|
||||
archived?: boolean,
|
||||
): Promise<FeatureToggle[]>;
|
||||
getPlaygroundFeatures(
|
||||
dependentFeaturesEnabled: boolean,
|
||||
includeDisabledStrategies: boolean,
|
||||
featureQuery?: IFeatureToggleQuery,
|
||||
): Promise<FeatureConfigurationClient[]>;
|
||||
countByDate(queryModifiers: {
|
||||
|
@ -67,6 +67,11 @@ export interface FeatureToggle extends FeatureToggleDTO {
|
||||
createdAt?: Date;
|
||||
}
|
||||
|
||||
export interface IFeatureToggleListItem extends FeatureToggle {
|
||||
environments?: Partial<IEnvironmentBase>[];
|
||||
favorite: boolean;
|
||||
}
|
||||
|
||||
export interface IFeatureToggleClient {
|
||||
name: string;
|
||||
description: string;
|
||||
|
@ -1,45 +1,19 @@
|
||||
import dbInit, { ITestDb } from '../../../helpers/database-init';
|
||||
import {
|
||||
IUnleashTest,
|
||||
insertFeatureEnvironmentsLastSeen,
|
||||
insertLastSeenAt,
|
||||
setupAppWithCustomConfig,
|
||||
} from '../../../helpers/test-helper';
|
||||
import getLogger from '../../../../fixtures/no-logger';
|
||||
|
||||
import { IProjectStore } from 'lib/types';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
let app: IUnleashTest;
|
||||
let db: ITestDb;
|
||||
|
||||
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 () => {
|
||||
db = await dbInit('projects_api_serial', getLogger);
|
||||
app = await setupAppWithCustomConfig(
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
CreateFeatureStrategySchema,
|
||||
ImportTogglesSchema,
|
||||
} from '../../../lib/openapi';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
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