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:
parent
ef8edf9c44
commit
e5760b5690
@ -102,7 +102,6 @@ exports[`should create default config 1`] = `
|
|||||||
"responseTimeWithAppNameKillSwitch": false,
|
"responseTimeWithAppNameKillSwitch": false,
|
||||||
"scheduledConfigurationChanges": false,
|
"scheduledConfigurationChanges": false,
|
||||||
"strictSchemaValidation": false,
|
"strictSchemaValidation": false,
|
||||||
"useLastSeenRefactor": false,
|
|
||||||
},
|
},
|
||||||
"externalResolver": {
|
"externalResolver": {
|
||||||
"getVariant": [Function],
|
"getVariant": [Function],
|
||||||
|
@ -189,9 +189,7 @@ 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);
|
||||||
this.addLastSeenByEnvironment(feature, r);
|
|
||||||
}
|
|
||||||
|
|
||||||
acc[r.name] = feature;
|
acc[r.name] = feature;
|
||||||
return acc;
|
return acc;
|
||||||
@ -246,9 +244,7 @@ export class FeatureToggleRowConverter {
|
|||||||
feature.lastSeenAt = row.last_seen_at;
|
feature.lastSeenAt = row.last_seen_at;
|
||||||
feature.archivedAt = row.archived_at;
|
feature.archivedAt = row.archived_at;
|
||||||
|
|
||||||
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
|
this.addLastSeenByEnvironment(feature, row);
|
||||||
this.addLastSeenByEnvironment(feature, row);
|
|
||||||
}
|
|
||||||
|
|
||||||
acc[row.name] = feature;
|
acc[row.name] = feature;
|
||||||
return acc;
|
return acc;
|
||||||
|
@ -1970,13 +1970,7 @@ class FeatureToggleService {
|
|||||||
archived: boolean,
|
archived: boolean,
|
||||||
userId: number,
|
userId: number,
|
||||||
): Promise<FeatureToggle[]> {
|
): Promise<FeatureToggle[]> {
|
||||||
let features;
|
const features = await this.featureToggleStore.getArchivedFeatures();
|
||||||
|
|
||||||
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
|
|
||||||
features = await this.featureToggleStore.getArchivedFeatures();
|
|
||||||
} else {
|
|
||||||
features = await this.featureToggleStore.getAll({ archived });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.flagResolver.isEnabled('privateProjects')) {
|
if (this.flagResolver.isEnabled('privateProjects')) {
|
||||||
const projectAccess =
|
const projectAccess =
|
||||||
@ -1998,11 +1992,7 @@ class FeatureToggleService {
|
|||||||
archived: boolean,
|
archived: boolean,
|
||||||
project: string,
|
project: string,
|
||||||
): Promise<FeatureToggle[]> {
|
): Promise<FeatureToggle[]> {
|
||||||
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
|
return this.featureToggleStore.getArchivedFeatures(project);
|
||||||
return this.featureToggleStore.getArchivedFeatures(project);
|
|
||||||
} else {
|
|
||||||
return this.featureToggleStore.getAll({ archived, project });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectId(name: string): Promise<string | undefined> {
|
async getProjectId(name: string): Promise<string | undefined> {
|
||||||
|
@ -171,15 +171,13 @@ 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(archived);
|
||||||
builder.withLastSeenByEnvironment(archived);
|
builder.addSelectColumn(
|
||||||
builder.addSelectColumn(
|
'last_seen_at_metrics.last_seen_at as env_last_seen_at',
|
||||||
'last_seen_at_metrics.last_seen_at as env_last_seen_at',
|
);
|
||||||
);
|
builder.addSelectColumn(
|
||||||
builder.addSelectColumn(
|
'last_seen_at_metrics.environment as last_seen_at_env',
|
||||||
'last_seen_at_metrics.environment as last_seen_at_env',
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userId) {
|
if (userId) {
|
||||||
builder.withFavorites(userId);
|
builder.withFavorites(userId);
|
||||||
|
@ -341,23 +341,21 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
|
|
||||||
let selectColumns = ['features_view.*'] as (string | Raw<any>)[];
|
let selectColumns = ['features_view.*'] as (string | Raw<any>)[];
|
||||||
|
|
||||||
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
|
query.leftJoin('last_seen_at_metrics', function () {
|
||||||
query.leftJoin('last_seen_at_metrics', function () {
|
this.on(
|
||||||
this.on(
|
'last_seen_at_metrics.environment',
|
||||||
'last_seen_at_metrics.environment',
|
'=',
|
||||||
'=',
|
'features_view.environment_name',
|
||||||
'features_view.environment_name',
|
).andOn(
|
||||||
).andOn(
|
'last_seen_at_metrics.feature_name',
|
||||||
'last_seen_at_metrics.feature_name',
|
'=',
|
||||||
'=',
|
'features_view.name',
|
||||||
'features_view.name',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
// Override feature view for now
|
|
||||||
selectColumns.push(
|
|
||||||
'last_seen_at_metrics.last_seen_at as env_last_seen_at',
|
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
// Override feature view for now
|
||||||
|
selectColumns.push(
|
||||||
|
'last_seen_at_metrics.last_seen_at as env_last_seen_at',
|
||||||
|
);
|
||||||
|
|
||||||
if (userId) {
|
if (userId) {
|
||||||
query = query.leftJoin(`favorite_features`, function () {
|
query = query.leftJoin(`favorite_features`, function () {
|
||||||
@ -631,19 +629,17 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
'segments.id',
|
'segments.id',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
|
query.leftJoin('last_seen_at_metrics', function () {
|
||||||
query.leftJoin('last_seen_at_metrics', function () {
|
this.on(
|
||||||
this.on(
|
'last_seen_at_metrics.environment',
|
||||||
'last_seen_at_metrics.environment',
|
'=',
|
||||||
'=',
|
'environments.name',
|
||||||
'environments.name',
|
).andOn(
|
||||||
).andOn(
|
'last_seen_at_metrics.feature_name',
|
||||||
'last_seen_at_metrics.feature_name',
|
'=',
|
||||||
'=',
|
'features.name',
|
||||||
'features.name',
|
);
|
||||||
);
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let selectColumns = [
|
let selectColumns = [
|
||||||
'features.name as feature_name',
|
'features.name as feature_name',
|
||||||
@ -664,11 +660,8 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
'segments.name as segment_name',
|
'segments.name as segment_name',
|
||||||
] as (string | Raw<any> | Knex.QueryBuilder)[];
|
] as (string | Raw<any> | Knex.QueryBuilder)[];
|
||||||
|
|
||||||
let lastSeenQuery = 'feature_environments.last_seen_at';
|
const lastSeenQuery = 'last_seen_at_metrics.last_seen_at';
|
||||||
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
|
selectColumns.push(`${lastSeenQuery} as env_last_seen_at`);
|
||||||
lastSeenQuery = 'last_seen_at_metrics.last_seen_at';
|
|
||||||
selectColumns.push(`${lastSeenQuery} as env_last_seen_at`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userId) {
|
if (userId) {
|
||||||
query.leftJoin(`favorite_features`, function () {
|
query.leftJoin(`favorite_features`, function () {
|
||||||
@ -802,19 +795,13 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
)
|
)
|
||||||
.leftJoin('feature_tag as ft', 'ft.feature_name', 'features.name');
|
.leftJoin('feature_tag as ft', 'ft.feature_name', 'features.name');
|
||||||
|
|
||||||
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
|
query.leftJoin('last_seen_at_metrics', function () {
|
||||||
query.leftJoin('last_seen_at_metrics', function () {
|
this.on(
|
||||||
this.on(
|
'last_seen_at_metrics.environment',
|
||||||
'last_seen_at_metrics.environment',
|
'=',
|
||||||
'=',
|
'environments.name',
|
||||||
'environments.name',
|
).andOn('last_seen_at_metrics.feature_name', '=', 'features.name');
|
||||||
).andOn(
|
});
|
||||||
'last_seen_at_metrics.feature_name',
|
|
||||||
'=',
|
|
||||||
'features.name',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let selectColumns = [
|
let selectColumns = [
|
||||||
'features.name as feature_name',
|
'features.name as feature_name',
|
||||||
@ -833,15 +820,9 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
'ft.tag_type as tag_type',
|
'ft.tag_type as tag_type',
|
||||||
] as (string | Raw<any> | Knex.QueryBuilder)[];
|
] as (string | Raw<any> | Knex.QueryBuilder)[];
|
||||||
|
|
||||||
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
|
selectColumns.push(
|
||||||
selectColumns.push(
|
'last_seen_at_metrics.last_seen_at as env_last_seen_at',
|
||||||
'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',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userId) {
|
if (userId) {
|
||||||
query = query.leftJoin(`favorite_features`, function () {
|
query = query.leftJoin(`favorite_features`, function () {
|
||||||
|
@ -22,7 +22,6 @@ beforeAll(async () => {
|
|||||||
experimental: {
|
experimental: {
|
||||||
flags: {
|
flags: {
|
||||||
strictSchemaValidation: true,
|
strictSchemaValidation: true,
|
||||||
useLastSeenRefactor: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -689,9 +689,7 @@ test('Should return last seen at per environment', async () => {
|
|||||||
expect(environments[0].lastSeenAt).toEqual(new Date(date));
|
expect(environments[0].lastSeenAt).toEqual(new Date(date));
|
||||||
|
|
||||||
// Test with feature flag on
|
// Test with feature flag on
|
||||||
const config = createTestConfig({
|
const config = createTestConfig();
|
||||||
experimental: { flags: { useLastSeenRefactor: true } },
|
|
||||||
});
|
|
||||||
|
|
||||||
const featureService = createFeatureToggleService(db.rawDatabase, config);
|
const featureService = createFeatureToggleService(db.rawDatabase, config);
|
||||||
|
|
||||||
|
@ -20,7 +20,6 @@ beforeAll(async () => {
|
|||||||
strictSchemaValidation: true,
|
strictSchemaValidation: true,
|
||||||
strategyVariant: true,
|
strategyVariant: true,
|
||||||
privateProjects: true,
|
privateProjects: true,
|
||||||
useLastSeenRefactor: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -185,34 +185,6 @@ test('schema allow yes=<string nbr>', () => {
|
|||||||
expect(value.bucket.toggles.Demo2.no).toBe(256);
|
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 () => {
|
test('should return a 400 when required fields are missing', async () => {
|
||||||
stores.featureToggleStore.create('default', {
|
stores.featureToggleStore.create('default', {
|
||||||
name: 'toggleLastSeen',
|
name: 'toggleLastSeen',
|
||||||
|
@ -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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -51,11 +51,7 @@ export class LastSeenService {
|
|||||||
`Updating last seen for ${lastSeenToggles.length} toggles`,
|
`Updating last seen for ${lastSeenToggles.length} toggles`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.config.flagResolver.isEnabled('useLastSeenRefactor')) {
|
await this.lastSeenStore.setLastSeen(lastSeenToggles);
|
||||||
await this.lastSeenStore.setLastSeen(lastSeenToggles);
|
|
||||||
} else {
|
|
||||||
await this.featureToggleStore.setLastSeen(lastSeenToggles);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
@ -81,8 +77,6 @@ export class LastSeenService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async cleanLastSeen() {
|
async cleanLastSeen() {
|
||||||
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
|
await this.lastSeenStore.cleanLastSeen();
|
||||||
await this.lastSeenStore.cleanLastSeen();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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),
|
|
||||||
);
|
|
||||||
});
|
|
@ -16,7 +16,6 @@ beforeAll(async () => {
|
|||||||
experimental: {
|
experimental: {
|
||||||
flags: {
|
flags: {
|
||||||
strictSchemaValidation: true,
|
strictSchemaValidation: true,
|
||||||
useLastSeenRefactor: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -36,7 +36,9 @@ function initLastSeenService(flagEnabled = true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test('should not add duplicates per feature/environment', async () => {
|
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([
|
lastSeenService.updateLastSeen([
|
||||||
{
|
{
|
||||||
@ -59,10 +61,8 @@ test('should not add duplicates per feature/environment', async () => {
|
|||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
featureToggleStore.setLastSeen = jest.fn();
|
|
||||||
await lastSeenService.store();
|
await lastSeenService.store();
|
||||||
|
expect(lastSeenSpy).toHaveBeenCalledWith([
|
||||||
expect(featureToggleStore.setLastSeen).toHaveBeenCalledWith([
|
|
||||||
{
|
{
|
||||||
environment: 'development',
|
environment: 'development',
|
||||||
featureName: 'myFeature',
|
featureName: 'myFeature',
|
||||||
@ -96,7 +96,6 @@ test('should call last seen at store with correct data', async () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
lastSeenStore.setLastSeen = jest.fn();
|
lastSeenStore.setLastSeen = jest.fn();
|
||||||
featureToggleStore.setLastSeen = jest.fn();
|
|
||||||
await lastSeenService.store();
|
await lastSeenService.store();
|
||||||
|
|
||||||
expect(lastSeenStore.setLastSeen).toHaveBeenCalledWith([
|
expect(lastSeenStore.setLastSeen).toHaveBeenCalledWith([
|
||||||
@ -105,5 +104,4 @@ test('should call last seen at store with correct data', async () => {
|
|||||||
featureName: 'myFeature',
|
featureName: 'myFeature',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
expect(featureToggleStore.setLastSeen).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
});
|
||||||
|
122
src/lib/services/scheduler-service.test.ts
Normal file
122
src/lib/services/scheduler-service.test.ts
Normal 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'],
|
||||||
|
]);
|
||||||
|
});
|
@ -25,7 +25,6 @@ export type IFlagKey =
|
|||||||
| 'customRootRolesKillSwitch'
|
| 'customRootRolesKillSwitch'
|
||||||
| 'privateProjects'
|
| 'privateProjects'
|
||||||
| 'disableMetrics'
|
| 'disableMetrics'
|
||||||
| 'useLastSeenRefactor'
|
|
||||||
| 'banners'
|
| 'banners'
|
||||||
| 'featureSearchAPI'
|
| 'featureSearchAPI'
|
||||||
| 'featureSearchFrontend'
|
| 'featureSearchFrontend'
|
||||||
@ -114,10 +113,6 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_DISABLE_METRICS,
|
process.env.UNLEASH_EXPERIMENTAL_DISABLE_METRICS,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
useLastSeenRefactor: parseEnvVarBoolean(
|
|
||||||
process.env.UNLEASH_EXPERIMENTAL_USE_LAST_SEEN_REFACTOR,
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
featureSearchAPI: parseEnvVarBoolean(
|
featureSearchAPI: parseEnvVarBoolean(
|
||||||
process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_API,
|
process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_API,
|
||||||
false,
|
false,
|
||||||
|
@ -38,7 +38,6 @@ process.nextTick(async () => {
|
|||||||
anonymiseEventLog: false,
|
anonymiseEventLog: false,
|
||||||
responseTimeWithAppNameKillSwitch: false,
|
responseTimeWithAppNameKillSwitch: false,
|
||||||
privateProjects: true,
|
privateProjects: true,
|
||||||
useLastSeenRefactor: true,
|
|
||||||
featureSearchAPI: true,
|
featureSearchAPI: true,
|
||||||
featureSearchFrontend: false,
|
featureSearchFrontend: false,
|
||||||
},
|
},
|
||||||
|
@ -77,14 +77,13 @@ test('returns three archived toggles', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('returns three archived toggles with archivedAt', async () => {
|
test('returns three archived toggles with archivedAt', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(2);
|
||||||
return app.request
|
return app.request
|
||||||
.get('/api/admin/archive/features')
|
.get('/api/admin/archive/features')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect((res) => {
|
.expect((res) => {
|
||||||
expect(res.body.features.length).toEqual(3);
|
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);
|
expect(res.body.features.every((f) => f.archivedAt)).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -14,6 +14,7 @@ let app: IUnleashTest;
|
|||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
|
|
||||||
let projectStore: IProjectStore;
|
let projectStore: IProjectStore;
|
||||||
|
const testDate = '2023-10-01T12:34:56.000Z';
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('projects_api_serial', getLogger);
|
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 () => {
|
test('response should include last seen at per environment', async () => {
|
||||||
await app.createFeature('my-new-feature-toggle');
|
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(
|
await insertFeatureEnvironmentsLastSeen(
|
||||||
'my-new-feature-toggle',
|
'my-new-feature-toggle',
|
||||||
db.rawDatabase,
|
db.rawDatabase,
|
||||||
@ -158,19 +164,11 @@ test('response should include last seen at per environment', async () => {
|
|||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(body.features[0].environments[0].lastSeenAt).toEqual(
|
expect(body.features[0].environments[0].lastSeenAt).toEqual(testDate);
|
||||||
'2022-05-01T12:34:56.000Z',
|
|
||||||
);
|
|
||||||
|
|
||||||
const appWithLastSeenRefactor = await setupAppWithCustomConfig(
|
const appWithLastSeenRefactor = await setupAppWithCustomConfig(
|
||||||
db.stores,
|
db.stores,
|
||||||
{
|
{},
|
||||||
experimental: {
|
|
||||||
flags: {
|
|
||||||
useLastSeenRefactor: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
db.rawDatabase,
|
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 () => {
|
test('response should include last seen at per environment for multiple environments', async () => {
|
||||||
const appWithLastSeenRefactor = await setupAppWithCustomConfig(
|
const appWithLastSeenRefactor = await setupAppWithCustomConfig(
|
||||||
db.stores,
|
db.stores,
|
||||||
{
|
{},
|
||||||
experimental: {
|
|
||||||
flags: {
|
|
||||||
useLastSeenRefactor: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
db.rawDatabase,
|
db.rawDatabase,
|
||||||
);
|
);
|
||||||
await app.createFeature('my-new-feature-toggle');
|
await app.createFeature('my-new-feature-toggle');
|
||||||
|
@ -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);
|
|
||||||
});
|
|
Loading…
Reference in New Issue
Block a user