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

Feat: remove last seen refactor flag (#5423)

What it says on the box

---------

Signed-off-by: andreas-unleash <andreas@getunleash.ai>
Co-authored-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
Fredrik Strand Oseberg 2023-11-30 09:17:50 +01:00 committed by GitHub
parent ef8edf9c44
commit e5760b5690
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 188 additions and 435 deletions

View File

@ -102,7 +102,6 @@ exports[`should create default config 1`] = `
"responseTimeWithAppNameKillSwitch": false,
"scheduledConfigurationChanges": false,
"strictSchemaValidation": false,
"useLastSeenRefactor": false,
},
"externalResolver": {
"getVariant": [Function],

View File

@ -189,9 +189,7 @@ export class FeatureToggleRowConverter {
feature.createdAt = r.created_at;
feature.favorite = r.favorite;
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
this.addLastSeenByEnvironment(feature, r);
}
this.addLastSeenByEnvironment(feature, r);
acc[r.name] = feature;
return acc;
@ -246,9 +244,7 @@ export class FeatureToggleRowConverter {
feature.lastSeenAt = row.last_seen_at;
feature.archivedAt = row.archived_at;
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
this.addLastSeenByEnvironment(feature, row);
}
this.addLastSeenByEnvironment(feature, row);
acc[row.name] = feature;
return acc;

View File

@ -1970,13 +1970,7 @@ class FeatureToggleService {
archived: boolean,
userId: number,
): Promise<FeatureToggle[]> {
let features;
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
features = await this.featureToggleStore.getArchivedFeatures();
} else {
features = await this.featureToggleStore.getAll({ archived });
}
const features = await this.featureToggleStore.getArchivedFeatures();
if (this.flagResolver.isEnabled('privateProjects')) {
const projectAccess =
@ -1998,11 +1992,7 @@ class FeatureToggleService {
archived: boolean,
project: string,
): Promise<FeatureToggle[]> {
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
return this.featureToggleStore.getArchivedFeatures(project);
} else {
return this.featureToggleStore.getAll({ archived, project });
}
return this.featureToggleStore.getArchivedFeatures(project);
}
async getProjectId(name: string): Promise<string | undefined> {

View File

@ -171,15 +171,13 @@ 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(archived);
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',
);
}
builder.withLastSeenByEnvironment(archived);
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);

View File

@ -341,23 +341,21 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
let selectColumns = ['features_view.*'] as (string | Raw<any>)[];
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
query.leftJoin('last_seen_at_metrics', function () {
this.on(
'last_seen_at_metrics.environment',
'=',
'features_view.environment_name',
).andOn(
'last_seen_at_metrics.feature_name',
'=',
'features_view.name',
);
});
// Override feature view for now
selectColumns.push(
'last_seen_at_metrics.last_seen_at as env_last_seen_at',
query.leftJoin('last_seen_at_metrics', function () {
this.on(
'last_seen_at_metrics.environment',
'=',
'features_view.environment_name',
).andOn(
'last_seen_at_metrics.feature_name',
'=',
'features_view.name',
);
}
});
// Override feature view for now
selectColumns.push(
'last_seen_at_metrics.last_seen_at as env_last_seen_at',
);
if (userId) {
query = query.leftJoin(`favorite_features`, function () {
@ -631,19 +629,17 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
'segments.id',
);
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
query.leftJoin('last_seen_at_metrics', function () {
this.on(
'last_seen_at_metrics.environment',
'=',
'environments.name',
).andOn(
'last_seen_at_metrics.feature_name',
'=',
'features.name',
);
});
}
query.leftJoin('last_seen_at_metrics', function () {
this.on(
'last_seen_at_metrics.environment',
'=',
'environments.name',
).andOn(
'last_seen_at_metrics.feature_name',
'=',
'features.name',
);
});
let selectColumns = [
'features.name as feature_name',
@ -664,11 +660,8 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
'segments.name as segment_name',
] as (string | Raw<any> | Knex.QueryBuilder)[];
let lastSeenQuery = 'feature_environments.last_seen_at';
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
lastSeenQuery = 'last_seen_at_metrics.last_seen_at';
selectColumns.push(`${lastSeenQuery} as env_last_seen_at`);
}
const lastSeenQuery = 'last_seen_at_metrics.last_seen_at';
selectColumns.push(`${lastSeenQuery} as env_last_seen_at`);
if (userId) {
query.leftJoin(`favorite_features`, function () {
@ -802,19 +795,13 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
)
.leftJoin('feature_tag as ft', 'ft.feature_name', 'features.name');
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
query.leftJoin('last_seen_at_metrics', function () {
this.on(
'last_seen_at_metrics.environment',
'=',
'environments.name',
).andOn(
'last_seen_at_metrics.feature_name',
'=',
'features.name',
);
});
}
query.leftJoin('last_seen_at_metrics', function () {
this.on(
'last_seen_at_metrics.environment',
'=',
'environments.name',
).andOn('last_seen_at_metrics.feature_name', '=', 'features.name');
});
let selectColumns = [
'features.name as feature_name',
@ -833,15 +820,9 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
'ft.tag_type as tag_type',
] as (string | Raw<any> | Knex.QueryBuilder)[];
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
selectColumns.push(
'last_seen_at_metrics.last_seen_at as env_last_seen_at',
);
} else {
selectColumns.push(
'feature_environments.last_seen_at as env_last_seen_at',
);
}
selectColumns.push(
'last_seen_at_metrics.last_seen_at as env_last_seen_at',
);
if (userId) {
query = query.leftJoin(`favorite_features`, function () {

View File

@ -22,7 +22,6 @@ beforeAll(async () => {
experimental: {
flags: {
strictSchemaValidation: true,
useLastSeenRefactor: true,
},
},
};

View File

@ -689,9 +689,7 @@ test('Should return last seen at per environment', async () => {
expect(environments[0].lastSeenAt).toEqual(new Date(date));
// Test with feature flag on
const config = createTestConfig({
experimental: { flags: { useLastSeenRefactor: true } },
});
const config = createTestConfig();
const featureService = createFeatureToggleService(db.rawDatabase, config);

View File

@ -20,7 +20,6 @@ beforeAll(async () => {
strictSchemaValidation: true,
strategyVariant: true,
privateProjects: true,
useLastSeenRefactor: true,
},
},
},

View File

@ -185,34 +185,6 @@ test('schema allow yes=<string nbr>', () => {
expect(value.bucket.toggles.Demo2.no).toBe(256);
});
test('should set lastSeen on toggle', async () => {
expect.assertions(1);
stores.featureToggleStore.create('default', {
name: 'toggleLastSeen',
});
await request
.post('/api/client/metrics')
.send({
appName: 'demo',
bucket: {
start: Date.now(),
stop: Date.now(),
toggles: {
toggleLastSeen: {
yes: 200,
no: 0,
},
},
},
})
.expect(202);
await services.lastSeenService.store();
const toggle = await stores.featureToggleStore.get('toggleLastSeen');
expect(toggle.lastSeenAt).toBeTruthy();
});
test('should return a 400 when required fields are missing', async () => {
stores.featureToggleStore.create('default', {
name: 'toggleLastSeen',

View File

@ -1,39 +0,0 @@
import { Logger } from '../../../logger';
import { IFeatureOverview } from '../../../types';
import { IFeatureLastSeenResults } from './last-seen-read-model';
export class LastSeenMapper {
mapToFeatures(
features: IFeatureOverview[],
lastSeenAtPerEnvironment: IFeatureLastSeenResults,
logger: Logger,
): IFeatureOverview[] {
return features.map((feature) => {
if (!feature.environments) {
logger.warn('Feature without environments:', feature);
return feature;
}
feature.environments = feature.environments.map((environment) => {
const noData =
!lastSeenAtPerEnvironment[feature.name] ||
!lastSeenAtPerEnvironment[feature.name][environment.name];
if (noData) {
logger.warn(
'No last seen data for environment:',
environment,
);
return environment;
}
environment.lastSeenAt = new Date(
lastSeenAtPerEnvironment[feature.name][environment.name]
.lastSeen,
);
return environment;
});
return feature;
});
}
}

View File

@ -51,11 +51,7 @@ export class LastSeenService {
`Updating last seen for ${lastSeenToggles.length} toggles`,
);
if (this.config.flagResolver.isEnabled('useLastSeenRefactor')) {
await this.lastSeenStore.setLastSeen(lastSeenToggles);
} else {
await this.featureToggleStore.setLastSeen(lastSeenToggles);
}
await this.lastSeenStore.setLastSeen(lastSeenToggles);
}
return count;
}
@ -81,8 +77,6 @@ export class LastSeenService {
}
async cleanLastSeen() {
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
await this.lastSeenStore.cleanLastSeen();
}
await this.lastSeenStore.cleanLastSeen();
}
}

View File

@ -1,95 +0,0 @@
import { IFeatureOverview } from '../../../../types';
import { LastSeenMapper } from '../last-seen-mapper';
import getLogger from '../../../../../test/fixtures/no-logger';
test('should produce correct output when mapped', () => {
const mapper = new LastSeenMapper();
const inputLastSeen = {
exp: {
production: { lastSeen: '2023-10-05T07:27:04.286Z' },
development: { lastSeen: '2023-10-04T19:03:29.682Z' },
},
'payment-system': {
production: { lastSeen: '2023-10-05T07:27:04.286Z' },
development: { lastSeen: '2023-10-04T19:03:29.682Z' },
},
};
const inputFeatures: IFeatureOverview[] = [
{
type: 'release',
description: null,
favorite: false,
name: 'payment-system',
// @ts-ignore
createdAt: '2023-06-30T12:57:20.476Z',
// @ts-ignore
lastSeenAt: '2023-10-03T13:08:16.263Z',
stale: false,
impressionData: false,
environments: [
{
name: 'development',
enabled: false,
type: 'development',
sortOrder: 2,
variantCount: 0,
// @ts-ignore
lastSeenAt: '2023-10-04T19:03:29.682Z',
},
{
name: 'production',
enabled: true,
type: 'production',
sortOrder: 3,
variantCount: 0,
// @ts-ignore
lastSeenAt: '2023-10-05T07:27:04.286Z',
},
],
},
{
type: 'experiment',
description: null,
favorite: false,
name: 'exp',
// @ts-ignore
createdAt: '2023-09-13T08:08:28.211Z',
// @ts-ignore
lastSeenAt: '2023-10-03T13:08:16.263Z',
stale: false,
impressionData: false,
environments: [
{
name: 'development',
enabled: false,
type: 'development',
sortOrder: 2,
variantCount: 0,
// @ts-ignore
lastSeenAt: '2023-10-04T19:03:29.682Z',
},
{
name: 'production',
enabled: true,
type: 'production',
sortOrder: 3,
variantCount: 0,
// @ts-ignore
lastSeenAt: '2023-10-05T07:27:04.286Z',
},
],
},
];
const logger = getLogger();
const result = mapper.mapToFeatures(inputFeatures, inputLastSeen, logger);
expect(result[0].environments[0].name).toBe('development');
expect(result[0].name).toBe('payment-system');
expect(result[0].environments[0].lastSeenAt).toEqual(
new Date(inputLastSeen['payment-system'].development.lastSeen),
);
});

View File

@ -16,7 +16,6 @@ beforeAll(async () => {
experimental: {
flags: {
strictSchemaValidation: true,
useLastSeenRefactor: true,
},
},
},

View File

@ -36,7 +36,9 @@ function initLastSeenService(flagEnabled = true) {
}
test('should not add duplicates per feature/environment', async () => {
const { lastSeenService, featureToggleStore } = initLastSeenService(false);
const { lastSeenService, featureToggleStore, lastSeenStore } =
initLastSeenService(false);
const lastSeenSpy = jest.spyOn(lastSeenStore, 'setLastSeen');
lastSeenService.updateLastSeen([
{
@ -59,10 +61,8 @@ test('should not add duplicates per feature/environment', async () => {
timestamp: new Date(),
},
]);
featureToggleStore.setLastSeen = jest.fn();
await lastSeenService.store();
expect(featureToggleStore.setLastSeen).toHaveBeenCalledWith([
expect(lastSeenSpy).toHaveBeenCalledWith([
{
environment: 'development',
featureName: 'myFeature',
@ -96,7 +96,6 @@ test('should call last seen at store with correct data', async () => {
},
]);
lastSeenStore.setLastSeen = jest.fn();
featureToggleStore.setLastSeen = jest.fn();
await lastSeenService.store();
expect(lastSeenStore.setLastSeen).toHaveBeenCalledWith([
@ -105,5 +104,4 @@ test('should call last seen at store with correct data', async () => {
featureName: 'myFeature',
},
]);
expect(featureToggleStore.setLastSeen).toHaveBeenCalledTimes(0);
});

View File

@ -0,0 +1,122 @@
import { LogProvider } from '../logger';
import { SchedulerService } from '../features/scheduler/scheduler-service';
import { createTestConfig } from '../../test/config/test-config';
import FakeSettingStore from '../../test/fixtures/fake-setting-store';
import SettingService from './setting-service';
import EventService from './event-service';
import MaintenanceService from '../features/maintenance/maintenance-service';
function ms(timeMs) {
return new Promise((resolve) => setTimeout(resolve, timeMs));
}
const getLogger = () => {
const records: any[] = [];
const logger: LogProvider = () => ({
error(...args: any[]) {
records.push(args);
},
debug() {},
info() {},
warn() {},
fatal() {},
});
const getRecords = () => {
return records;
};
return { logger, getRecords };
};
let schedulerService: SchedulerService;
let getRecords;
beforeEach(() => {
const config = createTestConfig();
const settingStore = new FakeSettingStore();
const settingService = new SettingService({ settingStore }, config, {
storeEvent() {},
} as unknown as EventService);
const maintenanceService = new MaintenanceService(config, settingService);
const { logger, getRecords: getRecordsFn } = getLogger();
getRecords = getRecordsFn;
schedulerService = new SchedulerService(
logger,
maintenanceService,
config.eventBus,
);
});
test('Schedules job immediately', async () => {
const job = jest.fn();
await schedulerService.schedule(job, 10, 'test-id');
expect(job).toBeCalledTimes(1);
schedulerService.stop();
});
test('Can schedule a single regular job', async () => {
const job = jest.fn();
await schedulerService.schedule(job, 50, 'test-id-3');
await ms(75);
expect(job).toBeCalledTimes(2);
schedulerService.stop();
});
test('Can schedule multiple jobs at the same interval', async () => {
const job = jest.fn();
const anotherJob = jest.fn();
await schedulerService.schedule(job, 50, 'test-id-6');
await schedulerService.schedule(anotherJob, 50, 'test-id-7');
await ms(75);
expect(job).toBeCalledTimes(2);
expect(anotherJob).toBeCalledTimes(2);
schedulerService.stop();
});
test('Can schedule multiple jobs at the different intervals', async () => {
const job = jest.fn();
const anotherJob = jest.fn();
await schedulerService.schedule(job, 100, 'test-id-8');
await schedulerService.schedule(anotherJob, 200, 'test-id-9');
await ms(250);
expect(job).toBeCalledTimes(3);
expect(anotherJob).toBeCalledTimes(2);
schedulerService.stop();
});
test('Can handle crash of a async job', async () => {
const job = async () => {
await Promise.reject('async reason');
};
await schedulerService.schedule(job, 50, 'test-id-10');
await ms(75);
schedulerService.stop();
expect(getRecords()).toEqual([
['scheduled job failed | id: test-id-10 | async reason'],
['scheduled job failed | id: test-id-10 | async reason'],
]);
});
test('Can handle crash of a sync job', async () => {
const job = () => {
throw new Error('sync reason');
};
await schedulerService.schedule(job, 50, 'test-id-11');
await ms(75);
schedulerService.stop();
expect(getRecords()).toEqual([
['scheduled job failed | id: test-id-11 | Error: sync reason'],
['scheduled job failed | id: test-id-11 | Error: sync reason'],
]);
});

View File

@ -25,7 +25,6 @@ export type IFlagKey =
| 'customRootRolesKillSwitch'
| 'privateProjects'
| 'disableMetrics'
| 'useLastSeenRefactor'
| 'banners'
| 'featureSearchAPI'
| 'featureSearchFrontend'
@ -114,10 +113,6 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_DISABLE_METRICS,
false,
),
useLastSeenRefactor: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_USE_LAST_SEEN_REFACTOR,
false,
),
featureSearchAPI: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_API,
false,

View File

@ -38,7 +38,6 @@ process.nextTick(async () => {
anonymiseEventLog: false,
responseTimeWithAppNameKillSwitch: false,
privateProjects: true,
useLastSeenRefactor: true,
featureSearchAPI: true,
featureSearchFrontend: false,
},

View File

@ -77,14 +77,13 @@ test('returns three archived toggles', async () => {
});
test('returns three archived toggles with archivedAt', async () => {
expect.assertions(3);
expect.assertions(2);
return app.request
.get('/api/admin/archive/features')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
expect(res.body.features.length).toEqual(3);
expect(res.body.features.every((f) => f.archived)).toEqual(true);
expect(res.body.features.every((f) => f.archivedAt)).toEqual(true);
});
});

View File

@ -14,6 +14,7 @@ let app: IUnleashTest;
let db: ITestDb;
let projectStore: IProjectStore;
const testDate = '2023-10-01T12:34:56.000Z';
beforeAll(async () => {
db = await dbInit('projects_api_serial', getLogger);
@ -146,7 +147,12 @@ test('response for default project should include created_at', async () => {
test('response should include last seen at per environment', async () => {
await app.createFeature('my-new-feature-toggle');
await insertLastSeenAt('my-new-feature-toggle', db.rawDatabase, 'default');
await insertLastSeenAt(
'my-new-feature-toggle',
db.rawDatabase,
'default',
testDate,
);
await insertFeatureEnvironmentsLastSeen(
'my-new-feature-toggle',
db.rawDatabase,
@ -158,19 +164,11 @@ test('response should include last seen at per environment', async () => {
.expect('Content-Type', /json/)
.expect(200);
expect(body.features[0].environments[0].lastSeenAt).toEqual(
'2022-05-01T12:34:56.000Z',
);
expect(body.features[0].environments[0].lastSeenAt).toEqual(testDate);
const appWithLastSeenRefactor = await setupAppWithCustomConfig(
db.stores,
{
experimental: {
flags: {
useLastSeenRefactor: true,
},
},
},
{},
db.rawDatabase,
);
@ -187,13 +185,7 @@ test('response should include last seen at per environment', async () => {
test('response should include last seen at per environment for multiple environments', async () => {
const appWithLastSeenRefactor = await setupAppWithCustomConfig(
db.stores,
{
experimental: {
flags: {
useLastSeenRefactor: true,
},
},
},
{},
db.rawDatabase,
);
await app.createFeature('my-new-feature-toggle');

View File

@ -1,143 +0,0 @@
import { createTestConfig } from '../../config/test-config';
import dbInit from '../helpers/database-init';
import { IUnleashStores } from '../../../lib/types/stores';
import { LastSeenService } from '../../../lib/services/client-metrics/last-seen/last-seen-service';
import { IClientMetricsEnv } from '../../../lib/types/stores/client-metrics-store-v2';
let stores: IUnleashStores;
let db;
let config;
beforeAll(async () => {
config = createTestConfig();
db = await dbInit('last_seen_service_serial', config.getLogger);
stores = db.stores;
});
beforeEach(async () => {
await stores.featureToggleStore.deleteAll();
});
afterAll(async () => {
await db.destroy();
});
test('Should update last seen for known toggles', async () => {
const service = new LastSeenService(
{
lastSeenStore: stores.lastSeenStore,
featureToggleStore: stores.featureToggleStore,
},
config,
);
const time = Date.now() - 100;
await stores.featureToggleStore.create('default', { name: 'ta1' });
const metrics: IClientMetricsEnv[] = [
{
featureName: 'ta1',
appName: 'some-App',
environment: 'default',
timestamp: new Date(time),
yes: 1,
no: 0,
},
{
featureName: 'ta2',
appName: 'some-App',
environment: 'default',
timestamp: new Date(time),
yes: 1,
no: 0,
},
];
service.updateLastSeen(metrics);
await service.store();
const t1 = await stores.featureToggleStore.get('ta1');
expect(t1.lastSeenAt.getTime()).toBeGreaterThan(time);
});
test('Should not update last seen toggles with 0 metrics', async () => {
// jest.useFakeTimers();
const service = new LastSeenService(
{
lastSeenStore: stores.lastSeenStore,
featureToggleStore: stores.featureToggleStore,
},
config,
);
const time = Date.now();
await stores.featureToggleStore.create('default', { name: 'tb1' });
await stores.featureToggleStore.create('default', { name: 'tb2' });
const metrics: IClientMetricsEnv[] = [
{
featureName: 'tb1',
appName: 'some-App',
environment: 'default',
timestamp: new Date(time),
yes: 1,
no: 0,
},
{
featureName: 'tb2',
appName: 'some-App',
environment: 'default',
timestamp: new Date(time),
yes: 0,
no: 0,
},
];
service.updateLastSeen(metrics);
// bypass interval waiting
await service.store();
const t1 = await stores.featureToggleStore.get('tb1');
const t2 = await stores.featureToggleStore.get('tb2');
expect(t2.lastSeenAt).toBeNull();
expect(t1.lastSeenAt.getTime()).toBeGreaterThanOrEqual(time);
});
test('Should not update anything for 0 toggles', async () => {
// jest.useFakeTimers();
const service = new LastSeenService(
{
lastSeenStore: stores.lastSeenStore,
featureToggleStore: stores.featureToggleStore,
},
config,
);
const time = Date.now();
await stores.featureToggleStore.create('default', { name: 'tb1' });
await stores.featureToggleStore.create('default', { name: 'tb2' });
const metrics: IClientMetricsEnv[] = [
{
featureName: 'tb1',
appName: 'some-App',
environment: 'default',
timestamp: new Date(time),
yes: 0,
no: 0,
},
{
featureName: 'tb2',
appName: 'some-App',
environment: 'default',
timestamp: new Date(time),
yes: 0,
no: 0,
},
];
service.updateLastSeen(metrics);
// bypass interval waiting
const count = await service.store();
expect(count).toBe(0);
});