mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-23 00:22:19 +01:00
Fix/last seen at by environment (#4939)
Initial architecture for last seen at by environment.
This commit is contained in:
parent
34fc17146e
commit
d896dbd0c7
@ -110,6 +110,7 @@ exports[`should create default config 1`] = `
|
||||
"responseTimeWithAppNameKillSwitch": false,
|
||||
"strictSchemaValidation": false,
|
||||
"transactionalDecorator": false,
|
||||
"useLastSeenRefactor": false,
|
||||
"variantTypeNumber": false,
|
||||
},
|
||||
},
|
||||
@ -150,6 +151,7 @@ exports[`should create default config 1`] = `
|
||||
"responseTimeWithAppNameKillSwitch": false,
|
||||
"strictSchemaValidation": false,
|
||||
"transactionalDecorator": false,
|
||||
"useLastSeenRefactor": false,
|
||||
"variantTypeNumber": false,
|
||||
},
|
||||
"externalResolver": {
|
||||
|
@ -7,7 +7,7 @@ import { Logger, LogProvider } from '../logger';
|
||||
import { FeatureToggle, FeatureToggleDTO, IVariant } from '../types/model';
|
||||
import { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
|
||||
import { Db } from './db';
|
||||
import { LastSeenInput } from '../services/client-metrics/last-seen-service';
|
||||
import { LastSeenInput } from '../services/client-metrics/last-seen/last-seen-service';
|
||||
import { NameExistsError } from '../error';
|
||||
|
||||
export type EnvironmentFeatureNames = { [key: string]: string[] };
|
||||
|
@ -38,6 +38,7 @@ import { Db } from './db';
|
||||
import { ImportTogglesStore } from '../features/export-import-toggles/import-toggles-store';
|
||||
import PrivateProjectStore from '../features/private-project/privateProjectStore';
|
||||
import { DependentFeaturesStore } from '../features/dependent-features/dependent-features-store';
|
||||
import LastSeenStore from '../services/client-metrics/last-seen/last-seen-store';
|
||||
|
||||
export const createStores = (
|
||||
config: IUnleashConfig,
|
||||
@ -132,6 +133,7 @@ export const createStores = (
|
||||
importTogglesStore: new ImportTogglesStore(db),
|
||||
privateProjectStore: new PrivateProjectStore(db, getLogger),
|
||||
dependentFeaturesStore: new DependentFeaturesStore(db),
|
||||
lastSeenStore: new LastSeenStore(db, eventBus, getLogger),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -41,6 +41,8 @@ import {
|
||||
createPrivateProjectChecker,
|
||||
} from '../private-project/createPrivateProjectChecker';
|
||||
import FakeFeatureTagStore from '../../../test/fixtures/fake-feature-tag-store';
|
||||
import { LastSeenAtReadModel } from '../../services/client-metrics/last-seen/last-seen-read-model';
|
||||
import { FakeLastSeenReadModel } from '../../services/client-metrics/last-seen/fake-last-seen-read-model';
|
||||
|
||||
export const createProjectService = (
|
||||
db: Db,
|
||||
@ -99,6 +101,7 @@ export const createProjectService = (
|
||||
);
|
||||
|
||||
const privateProjectChecker = createPrivateProjectChecker(db, config);
|
||||
const lastSeenReadModel = new LastSeenAtReadModel(db);
|
||||
|
||||
return new ProjectService(
|
||||
{
|
||||
@ -118,6 +121,7 @@ export const createProjectService = (
|
||||
favoriteService,
|
||||
eventService,
|
||||
privateProjectChecker,
|
||||
lastSeenReadModel,
|
||||
);
|
||||
};
|
||||
|
||||
@ -160,6 +164,7 @@ export const createFakeProjectService = (
|
||||
);
|
||||
|
||||
const privateProjectChecker = createFakePrivateProjectChecker();
|
||||
const fakeLastSeenReadModel = new FakeLastSeenReadModel();
|
||||
|
||||
return new ProjectService(
|
||||
{
|
||||
@ -179,5 +184,6 @@ export const createFakeProjectService = (
|
||||
favoriteService,
|
||||
eventService,
|
||||
privateProjectChecker,
|
||||
fakeLastSeenReadModel,
|
||||
);
|
||||
};
|
||||
|
@ -149,8 +149,9 @@ export class ProjectApiTokenController extends Controller {
|
||||
): Promise<void> {
|
||||
const { user } = req;
|
||||
const { projectId } = req.params;
|
||||
await this.projectService.getProject(projectId); // Validates that the project exists
|
||||
|
||||
const project = await this.projectService.getProject(projectId); // Validates that the project exists
|
||||
console.log('project', project);
|
||||
const projectTokens = await this.accessibleTokens(user, projectId);
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
|
@ -5,21 +5,25 @@ import { createTestConfig } from '../../../test/config/test-config';
|
||||
import { clientMetricsSchema } from '../../services/client-metrics/schema';
|
||||
import { createServices } from '../../services';
|
||||
import { IUnleashOptions, IUnleashServices, IUnleashStores } from '../../types';
|
||||
import dbInit from '../../../test/e2e/helpers/database-init';
|
||||
|
||||
let db;
|
||||
|
||||
async function getSetup(opts?: IUnleashOptions) {
|
||||
const stores = createStores();
|
||||
|
||||
const config = createTestConfig(opts);
|
||||
const services = createServices(stores, config);
|
||||
const app = await getApp(config, stores, services);
|
||||
db = await dbInit('metrics', config.getLogger);
|
||||
|
||||
const services = createServices(db.stores, config, db.rawDatabase);
|
||||
const app = await getApp(config, db.stores, services);
|
||||
|
||||
return {
|
||||
request: supertest(app),
|
||||
stores,
|
||||
stores: db.stores,
|
||||
services,
|
||||
destroy: () => {
|
||||
destroy: async () => {
|
||||
services.versionService.destroy();
|
||||
services.clientInstanceService.destroy();
|
||||
await db.destroy();
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -207,6 +211,8 @@ test('should set lastSeen on toggle', async () => {
|
||||
await services.lastSeenService.store();
|
||||
const toggle = await stores.featureToggleStore.get('toggleLastSeen');
|
||||
|
||||
console.log(toggle);
|
||||
|
||||
expect(toggle.lastSeenAt).toBeTruthy();
|
||||
});
|
||||
|
||||
|
@ -1,61 +0,0 @@
|
||||
import createStores from '../../../test/fixtures/store';
|
||||
import EventEmitter from 'events';
|
||||
import getLogger from '../../../test/fixtures/no-logger';
|
||||
import { IUnleashConfig } from '../../types';
|
||||
import { LastSeenService } from './last-seen-service';
|
||||
|
||||
function initLastSeenService(flagEnabled = true) {
|
||||
const stores = createStores();
|
||||
|
||||
const eventBus = new EventEmitter();
|
||||
eventBus.emit = jest.fn();
|
||||
|
||||
const config = {
|
||||
eventBus,
|
||||
getLogger,
|
||||
flagResolver: {
|
||||
isEnabled: () => {
|
||||
return flagEnabled;
|
||||
},
|
||||
},
|
||||
} as unknown as IUnleashConfig;
|
||||
|
||||
const lastSeenService = new LastSeenService(stores, config);
|
||||
|
||||
return { lastSeenService, featureToggleStore: stores.featureToggleStore };
|
||||
}
|
||||
|
||||
test('should not add duplicates per feature/environment', async () => {
|
||||
const { lastSeenService, featureToggleStore } = initLastSeenService();
|
||||
|
||||
lastSeenService.updateLastSeen([
|
||||
{
|
||||
featureName: 'myFeature',
|
||||
environment: 'development',
|
||||
yes: 1,
|
||||
no: 0,
|
||||
appName: 'test',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
|
||||
lastSeenService.updateLastSeen([
|
||||
{
|
||||
featureName: 'myFeature',
|
||||
environment: 'development',
|
||||
yes: 1,
|
||||
no: 0,
|
||||
appName: 'test',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
featureToggleStore.setLastSeen = jest.fn();
|
||||
await lastSeenService.store();
|
||||
|
||||
expect(featureToggleStore.setLastSeen).toHaveBeenCalledWith([
|
||||
{
|
||||
environment: 'development',
|
||||
featureName: 'myFeature',
|
||||
},
|
||||
]);
|
||||
});
|
@ -0,0 +1,34 @@
|
||||
import FakeFeatureToggleStore from '../../../../test/fixtures/fake-feature-toggle-store';
|
||||
import FeatureToggleStore from '../../../db/feature-toggle-store';
|
||||
import { Db, IUnleashConfig } from '../../../server-impl';
|
||||
import { FakeLastSeenStore } from './fake-last-seen-store';
|
||||
import { LastSeenService } from './last-seen-service';
|
||||
import LastSeenStore from './last-seen-store';
|
||||
|
||||
export const createLastSeenService = (
|
||||
db: Db,
|
||||
config: IUnleashConfig,
|
||||
): LastSeenService => {
|
||||
const lastSeenStore = new LastSeenStore(
|
||||
db,
|
||||
config.eventBus,
|
||||
config.getLogger,
|
||||
);
|
||||
|
||||
const featureToggleStore = new FeatureToggleStore(
|
||||
db,
|
||||
config.eventBus,
|
||||
config.getLogger,
|
||||
);
|
||||
|
||||
return new LastSeenService({ lastSeenStore, featureToggleStore }, config);
|
||||
};
|
||||
|
||||
export const createFakeLastSeenService = (
|
||||
config: IUnleashConfig,
|
||||
): LastSeenService => {
|
||||
const lastSeenStore = new FakeLastSeenStore();
|
||||
const featureToggleStore = new FakeFeatureToggleStore();
|
||||
|
||||
return new LastSeenService({ lastSeenStore, featureToggleStore }, config);
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import { IFeatureLastSeenResults } from './last-seen-read-model';
|
||||
import { ILastSeenReadModel } from './types/last-seen-read-model-type';
|
||||
|
||||
export class FakeLastSeenReadModel implements ILastSeenReadModel {
|
||||
// eslint-disable-next-line
|
||||
getForFeature(features: string[]): Promise<IFeatureLastSeenResults> {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { LastSeenInput } from './last-seen-service';
|
||||
import { ILastSeenStore } from './types/last-seen-store-type';
|
||||
|
||||
export class FakeLastSeenStore implements ILastSeenStore {
|
||||
setLastSeen(data: LastSeenInput[]): Promise<void> {
|
||||
data.map((lastSeen) => lastSeen);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import { Db } from '../../../db/db';
|
||||
import { ILastSeenReadModel } from './types/last-seen-read-model-type';
|
||||
|
||||
const TABLE = 'last_seen_at_metrics';
|
||||
|
||||
export interface IFeatureLastSeenResults {
|
||||
[featureName: string]: {
|
||||
[environment: string]: {
|
||||
lastSeen: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
export class LastSeenAtReadModel implements ILastSeenReadModel {
|
||||
private db: Db;
|
||||
|
||||
constructor(db: Db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
async getForFeature(features: string[]): Promise<IFeatureLastSeenResults> {
|
||||
const rows = await this.db(TABLE).whereIn('feature_name', features);
|
||||
|
||||
const result = rows.reduce((acc, curr) => {
|
||||
if (!acc[curr.feature_name]) {
|
||||
acc[curr.feature_name] = {};
|
||||
|
||||
acc[curr.feature_name][curr.environment] = {
|
||||
lastSeen: curr.last_seen_at,
|
||||
};
|
||||
} else {
|
||||
acc[curr.feature_name][curr.environment] = {
|
||||
lastSeen: curr.last_seen_at,
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import { secondsToMilliseconds } from 'date-fns';
|
||||
import { Logger } from '../../logger';
|
||||
import { IUnleashConfig } from '../../server-impl';
|
||||
import { IUnleashStores } from '../../types';
|
||||
import { IClientMetricsEnv } from '../../types/stores/client-metrics-store-v2';
|
||||
import { IFeatureToggleStore } from '../../types/stores/feature-toggle-store';
|
||||
import { Logger } from '../../../logger';
|
||||
import { IUnleashConfig } from '../../../server-impl';
|
||||
import { IClientMetricsEnv } from '../../../types/stores/client-metrics-store-v2';
|
||||
import { ILastSeenStore } from './types/last-seen-store-type';
|
||||
import { IFeatureToggleStore, IUnleashStores } from '../../../../lib/types';
|
||||
|
||||
export type LastSeenInput = {
|
||||
featureName: string;
|
||||
@ -17,17 +17,26 @@ export class LastSeenService {
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
private lastSeenStore: ILastSeenStore;
|
||||
|
||||
private featureToggleStore: IFeatureToggleStore;
|
||||
|
||||
private config: IUnleashConfig;
|
||||
|
||||
constructor(
|
||||
{ featureToggleStore }: Pick<IUnleashStores, 'featureToggleStore'>,
|
||||
{
|
||||
featureToggleStore,
|
||||
lastSeenStore,
|
||||
}: Pick<IUnleashStores, 'featureToggleStore' | 'lastSeenStore'>,
|
||||
config: IUnleashConfig,
|
||||
lastSeenInterval = secondsToMilliseconds(30),
|
||||
) {
|
||||
this.lastSeenStore = lastSeenStore;
|
||||
this.featureToggleStore = featureToggleStore;
|
||||
this.logger = config.getLogger(
|
||||
'/services/client-metrics/last-seen-service.ts',
|
||||
);
|
||||
this.config = config;
|
||||
|
||||
this.timers.push(
|
||||
setInterval(() => this.store(), lastSeenInterval).unref(),
|
||||
@ -42,7 +51,12 @@ export class LastSeenService {
|
||||
this.logger.debug(
|
||||
`Updating last seen for ${lastSeenToggles.length} toggles`,
|
||||
);
|
||||
await this.featureToggleStore.setLastSeen(lastSeenToggles);
|
||||
|
||||
if (this.config.flagResolver.isEnabled('useLastSeenRefactor')) {
|
||||
await this.lastSeenStore.setLastSeen(lastSeenToggles);
|
||||
} else {
|
||||
await this.featureToggleStore.setLastSeen(lastSeenToggles);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
63
src/lib/services/client-metrics/last-seen/last-seen-store.ts
Normal file
63
src/lib/services/client-metrics/last-seen/last-seen-store.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import EventEmitter from 'events';
|
||||
import { LogProvider, Logger } from '../../../logger';
|
||||
import { DB_TIME } from '../../../metric-events';
|
||||
import { Db } from '../../../server-impl';
|
||||
import metricsHelper from '../../..//util/metrics-helper';
|
||||
import { LastSeenInput } from './last-seen-service';
|
||||
import { ILastSeenStore } from './types/last-seen-store-type';
|
||||
|
||||
const TABLE = 'last_seen_at_metrics';
|
||||
|
||||
export interface FeaturesTable {
|
||||
feature_name: string;
|
||||
last_seen_at: Date;
|
||||
environment: string;
|
||||
}
|
||||
|
||||
export default class LastSeenStore implements ILastSeenStore {
|
||||
private db: Db;
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
private timer: Function;
|
||||
|
||||
constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) {
|
||||
this.db = db;
|
||||
this.logger = getLogger('last-seen-store.ts');
|
||||
this.timer = (action) =>
|
||||
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
||||
store: 'last-seen-environment-store',
|
||||
action,
|
||||
});
|
||||
}
|
||||
|
||||
async setLastSeen(data: LastSeenInput[]): Promise<void> {
|
||||
const now = new Date();
|
||||
|
||||
try {
|
||||
const inserts = data.map((item) => {
|
||||
return {
|
||||
feature_name: item.featureName,
|
||||
environment: item.environment,
|
||||
last_seen_at: now,
|
||||
};
|
||||
});
|
||||
|
||||
const batchSize = 1000;
|
||||
|
||||
for (let i = 0; i < inserts.length; i += batchSize) {
|
||||
const batch = inserts.slice(i, i + batchSize);
|
||||
// Knex optimizes multi row insert when given an array:
|
||||
// https://knexjs.org/guide/query-builder.html#insert
|
||||
await this.db(TABLE)
|
||||
.insert(batch)
|
||||
.onConflict(['feature_name', 'environment'])
|
||||
.merge();
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error('Could not update lastSeen, error: ', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LastSeenStore;
|
@ -0,0 +1,95 @@
|
||||
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),
|
||||
);
|
||||
});
|
@ -0,0 +1,109 @@
|
||||
import createStores from '../../../../../test/fixtures/store';
|
||||
import EventEmitter from 'events';
|
||||
import getLogger from '../../../../../test/fixtures/no-logger';
|
||||
import { IUnleashConfig } from '../../../../types';
|
||||
import { LastSeenService } from '../last-seen-service';
|
||||
|
||||
function initLastSeenService(flagEnabled = true) {
|
||||
const stores = createStores();
|
||||
|
||||
const eventBus = new EventEmitter();
|
||||
eventBus.emit = jest.fn();
|
||||
|
||||
const config = {
|
||||
eventBus,
|
||||
getLogger,
|
||||
flagResolver: {
|
||||
isEnabled: () => {
|
||||
return flagEnabled;
|
||||
},
|
||||
},
|
||||
} as unknown as IUnleashConfig;
|
||||
|
||||
const lastSeenService = new LastSeenService(
|
||||
{
|
||||
lastSeenStore: stores.lastSeenStore,
|
||||
featureToggleStore: stores.featureToggleStore,
|
||||
},
|
||||
config,
|
||||
);
|
||||
|
||||
return {
|
||||
lastSeenService,
|
||||
featureToggleStore: stores.featureToggleStore,
|
||||
lastSeenStore: stores.lastSeenStore,
|
||||
};
|
||||
}
|
||||
|
||||
test('should not add duplicates per feature/environment', async () => {
|
||||
const { lastSeenService, featureToggleStore } = initLastSeenService(false);
|
||||
|
||||
lastSeenService.updateLastSeen([
|
||||
{
|
||||
featureName: 'myFeature',
|
||||
environment: 'development',
|
||||
yes: 1,
|
||||
no: 0,
|
||||
appName: 'test',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
|
||||
lastSeenService.updateLastSeen([
|
||||
{
|
||||
featureName: 'myFeature',
|
||||
environment: 'development',
|
||||
yes: 1,
|
||||
no: 0,
|
||||
appName: 'test',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
featureToggleStore.setLastSeen = jest.fn();
|
||||
await lastSeenService.store();
|
||||
|
||||
expect(featureToggleStore.setLastSeen).toHaveBeenCalledWith([
|
||||
{
|
||||
environment: 'development',
|
||||
featureName: 'myFeature',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should call last seen at store with correct data', async () => {
|
||||
const { lastSeenService, lastSeenStore, featureToggleStore } =
|
||||
initLastSeenService(true);
|
||||
|
||||
lastSeenService.updateLastSeen([
|
||||
{
|
||||
featureName: 'myFeature',
|
||||
environment: 'development',
|
||||
yes: 1,
|
||||
no: 0,
|
||||
appName: 'test',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
|
||||
lastSeenService.updateLastSeen([
|
||||
{
|
||||
featureName: 'myFeature',
|
||||
environment: 'development',
|
||||
yes: 1,
|
||||
no: 0,
|
||||
appName: 'test',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
lastSeenStore.setLastSeen = jest.fn();
|
||||
featureToggleStore.setLastSeen = jest.fn();
|
||||
await lastSeenService.store();
|
||||
|
||||
expect(lastSeenStore.setLastSeen).toHaveBeenCalledWith([
|
||||
{
|
||||
environment: 'development',
|
||||
featureName: 'myFeature',
|
||||
},
|
||||
]);
|
||||
expect(featureToggleStore.setLastSeen).toHaveBeenCalledTimes(0);
|
||||
});
|
@ -0,0 +1,5 @@
|
||||
import { IFeatureLastSeenResults } from '../last-seen-read-model';
|
||||
|
||||
export interface ILastSeenReadModel {
|
||||
getForFeature(features: string[]): Promise<IFeatureLastSeenResults>;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import { LastSeenInput } from '../last-seen-service';
|
||||
|
||||
export interface ILastSeenStore {
|
||||
setLastSeen(data: LastSeenInput[]): Promise<void>;
|
||||
}
|
@ -4,7 +4,7 @@ import getLogger from '../../../test/fixtures/no-logger';
|
||||
|
||||
import createStores from '../../../test/fixtures/store';
|
||||
import EventEmitter from 'events';
|
||||
import { LastSeenService } from './last-seen-service';
|
||||
import { LastSeenService } from './last-seen/last-seen-service';
|
||||
import { IUnleashConfig } from 'lib/types';
|
||||
|
||||
function initClientMetrics(flagEnabled = true) {
|
||||
@ -23,7 +23,13 @@ function initClientMetrics(flagEnabled = true) {
|
||||
},
|
||||
} as unknown as IUnleashConfig;
|
||||
|
||||
const lastSeenService = new LastSeenService(stores, config);
|
||||
const lastSeenService = new LastSeenService(
|
||||
{
|
||||
lastSeenStore: stores.lastSeenStore,
|
||||
featureToggleStore: stores.featureToggleStore,
|
||||
},
|
||||
config,
|
||||
);
|
||||
lastSeenService.updateLastSeen = jest.fn();
|
||||
|
||||
const service = new ClientMetricsServiceV2(stores, config, lastSeenService);
|
||||
|
@ -17,7 +17,7 @@ import ApiUser from '../../types/api-user';
|
||||
import { ALL } from '../../types/models/api-token';
|
||||
import User from '../../types/user';
|
||||
import { collapseHourlyMetrics } from '../../util/collapseHourlyMetrics';
|
||||
import { LastSeenService } from './last-seen-service';
|
||||
import { LastSeenService } from './last-seen/last-seen-service';
|
||||
import { generateHourBuckets } from '../../util/time-utils';
|
||||
import { ClientMetricsSchema } from 'lib/openapi';
|
||||
import { nameSchema } from '../../schema/feature-schema';
|
||||
|
@ -35,7 +35,7 @@ import { ProxyService } from './proxy-service';
|
||||
import EdgeService from './edge-service';
|
||||
import PatService from './pat-service';
|
||||
import { PublicSignupTokenService } from './public-signup-token-service';
|
||||
import { LastSeenService } from './client-metrics/last-seen-service';
|
||||
import { LastSeenService } from './client-metrics/last-seen/last-seen-service';
|
||||
import { InstanceStatsService } from '../features/instance-stats/instance-stats-service';
|
||||
import { FavoritesService } from './favorites-service';
|
||||
import MaintenanceService from './maintenance-service';
|
||||
@ -59,7 +59,11 @@ import {
|
||||
createFakeChangeRequestAccessService,
|
||||
} from '../features/change-request-access-service/createChangeRequestAccessReadModel';
|
||||
import ConfigurationRevisionService from '../features/feature-toggle/configuration-revision-service';
|
||||
import { createFeatureToggleService } from '../features';
|
||||
import {
|
||||
createFakeProjectService,
|
||||
createFeatureToggleService,
|
||||
createProjectService,
|
||||
} from '../features';
|
||||
import EventAnnouncerService from './event-announcer-service';
|
||||
import { createGroupService } from '../features/group/createGroupService';
|
||||
import {
|
||||
@ -77,6 +81,10 @@ import {
|
||||
} from '../features/dependent-features/createDependentFeaturesService';
|
||||
import { DependentFeaturesReadModel } from '../features/dependent-features/dependent-features-read-model';
|
||||
import { FakeDependentFeaturesReadModel } from '../features/dependent-features/fake-dependent-features-read-model';
|
||||
import {
|
||||
createFakeLastSeenService,
|
||||
createLastSeenService,
|
||||
} from './client-metrics/last-seen/createLastSeenService';
|
||||
|
||||
// TODO: will be moved to scheduler feature directory
|
||||
export const scheduleServices = async (
|
||||
@ -171,7 +179,9 @@ export const createServices = (
|
||||
const groupService = new GroupService(stores, config, eventService);
|
||||
const accessService = new AccessService(stores, config, groupService);
|
||||
const apiTokenService = new ApiTokenService(stores, config, eventService);
|
||||
const lastSeenService = new LastSeenService(stores, config);
|
||||
const lastSeenService = db
|
||||
? createLastSeenService(db, config)
|
||||
: createFakeLastSeenService(config);
|
||||
const clientMetricsServiceV2 = new ClientMetricsServiceV2(
|
||||
stores,
|
||||
config,
|
||||
@ -260,16 +270,10 @@ export const createServices = (
|
||||
eventService,
|
||||
);
|
||||
const favoritesService = new FavoritesService(stores, config, eventService);
|
||||
const projectService = new ProjectService(
|
||||
stores,
|
||||
config,
|
||||
accessService,
|
||||
featureToggleServiceV2,
|
||||
groupService,
|
||||
favoritesService,
|
||||
eventService,
|
||||
privateProjectChecker,
|
||||
);
|
||||
const projectService = db
|
||||
? createProjectService(db, config)
|
||||
: createFakeProjectService(config);
|
||||
|
||||
const projectHealthService = new ProjectHealthService(
|
||||
stores,
|
||||
config,
|
||||
|
@ -65,6 +65,8 @@ import { ProjectDoraMetricsSchema } from 'lib/openapi';
|
||||
import { checkFeatureNamingData } from '../features/feature-naming-pattern/feature-naming-validation';
|
||||
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
|
||||
import EventService from './event-service';
|
||||
import { ILastSeenReadModel } from './client-metrics/last-seen/types/last-seen-read-model-type';
|
||||
import { LastSeenMapper } from './client-metrics/last-seen/last-seen-mapper';
|
||||
|
||||
const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown';
|
||||
|
||||
@ -118,6 +120,8 @@ export default class ProjectService {
|
||||
|
||||
private projectStatsStore: IProjectStatsStore;
|
||||
|
||||
private lastSeenReadModel: ILastSeenReadModel;
|
||||
|
||||
private flagResolver: IFlagResolver;
|
||||
|
||||
private isEnterprise: boolean;
|
||||
@ -150,6 +154,7 @@ export default class ProjectService {
|
||||
favoriteService: FavoritesService,
|
||||
eventService: EventService,
|
||||
privateProjectChecker: IPrivateProjectChecker,
|
||||
lastSeenReadModel: ILastSeenReadModel,
|
||||
) {
|
||||
this.projectStore = projectStore;
|
||||
this.environmentStore = environmentStore;
|
||||
@ -165,6 +170,7 @@ export default class ProjectService {
|
||||
this.groupService = groupService;
|
||||
this.eventService = eventService;
|
||||
this.projectStatsStore = projectStatsStore;
|
||||
this.lastSeenReadModel = lastSeenReadModel;
|
||||
this.logger = config.getLogger('services/project-service.js');
|
||||
this.flagResolver = config.flagResolver;
|
||||
this.isEnterprise = config.isEnterprise;
|
||||
@ -1071,6 +1077,22 @@ export default class ProjectService {
|
||||
this.projectStatsStore.getProjectStats(projectId),
|
||||
]);
|
||||
|
||||
let decoratedFeatures = features;
|
||||
|
||||
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
|
||||
const mapper = new LastSeenMapper();
|
||||
|
||||
const featureNames = features.map((feature) => feature.name);
|
||||
const lastSeenAtPerEnvironment =
|
||||
await this.lastSeenReadModel.getForFeature(featureNames);
|
||||
|
||||
decoratedFeatures = mapper.mapToFeatures(
|
||||
decoratedFeatures,
|
||||
lastSeenAtPerEnvironment,
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
stats: projectStats,
|
||||
name: project.name,
|
||||
@ -1084,7 +1106,7 @@ export default class ProjectService {
|
||||
updatedAt: project.updatedAt,
|
||||
createdAt: project.createdAt,
|
||||
environments,
|
||||
features,
|
||||
features: decoratedFeatures,
|
||||
members,
|
||||
version: 1,
|
||||
};
|
||||
|
@ -32,7 +32,8 @@ export type IFlagKey =
|
||||
| 'dependentFeatures'
|
||||
| 'datadogJsonTemplate'
|
||||
| 'disableMetrics'
|
||||
| 'transactionalDecorator';
|
||||
| 'transactionalDecorator'
|
||||
| 'useLastSeenRefactor';
|
||||
|
||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||
|
||||
@ -152,6 +153,10 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_TRANSACTIONAL_DECORATOR,
|
||||
false,
|
||||
),
|
||||
useLastSeenRefactor: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_USE_LAST_SEEN_REFACTOR,
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||
|
@ -32,7 +32,7 @@ import { ProxyService } from '../services/proxy-service';
|
||||
import EdgeService from '../services/edge-service';
|
||||
import PatService from '../services/pat-service';
|
||||
import { PublicSignupTokenService } from '../services/public-signup-token-service';
|
||||
import { LastSeenService } from '../services/client-metrics/last-seen-service';
|
||||
import { LastSeenService } from '../services/client-metrics/last-seen/last-seen-service';
|
||||
import { InstanceStatsService } from '../features/instance-stats/instance-stats-service';
|
||||
import { FavoritesService } from '../services/favorites-service';
|
||||
import MaintenanceService from '../services/maintenance-service';
|
||||
|
@ -35,6 +35,7 @@ import { IProjectStatsStore } from './stores/project-stats-store-type';
|
||||
import { IImportTogglesStore } from '../features/export-import-toggles/import-toggles-store-type';
|
||||
import { IPrivateProjectStore } from '../features/private-project/privateProjectStoreType';
|
||||
import { IDependentFeaturesStore } from '../features/dependent-features/dependent-features-store-type';
|
||||
import { ILastSeenStore } from '../services/client-metrics/last-seen/types/last-seen-store-type';
|
||||
|
||||
export interface IUnleashStores {
|
||||
accessStore: IAccessStore;
|
||||
@ -74,6 +75,7 @@ export interface IUnleashStores {
|
||||
importTogglesStore: IImportTogglesStore;
|
||||
privateProjectStore: IPrivateProjectStore;
|
||||
dependentFeaturesStore: IDependentFeaturesStore;
|
||||
lastSeenStore: ILastSeenStore;
|
||||
}
|
||||
|
||||
export {
|
||||
@ -113,4 +115,5 @@ export {
|
||||
IImportTogglesStore,
|
||||
IPrivateProjectStore,
|
||||
IDependentFeaturesStore,
|
||||
ILastSeenStore,
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { FeatureToggle, FeatureToggleDTO, IVariant } from '../model';
|
||||
import { Store } from './store';
|
||||
import { LastSeenInput } from '../../services/client-metrics/last-seen-service';
|
||||
import { LastSeenInput } from '../../services/client-metrics/last-seen/last-seen-service';
|
||||
|
||||
export interface IFeatureToggleQuery {
|
||||
archived: boolean;
|
||||
|
26
src/migrations/20231003113443-last-seen-at-metrics-table.js
Normal file
26
src/migrations/20231003113443-last-seen-at-metrics-table.js
Normal file
@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function (db, callback) {
|
||||
db.runSql(
|
||||
`
|
||||
CREATE TABLE last_seen_at_metrics (
|
||||
feature_name VARCHAR(255),
|
||||
environment VARCHAR(100),
|
||||
last_seen_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
PRIMARY KEY (feature_name, environment),
|
||||
FOREIGN KEY (environment) REFERENCES environments(name) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_feature_name
|
||||
ON last_seen_at_metrics (feature_name);`,
|
||||
callback(),
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function (db, callback) {
|
||||
db.runSql(
|
||||
`DROP TABLE last_seen_at_metrics;
|
||||
`,
|
||||
callback(),
|
||||
);
|
||||
};
|
@ -46,6 +46,7 @@ process.nextTick(async () => {
|
||||
datadogJsonTemplate: true,
|
||||
dependentFeatures: true,
|
||||
transactionalDecorator: true,
|
||||
useLastSeenRefactor: true,
|
||||
},
|
||||
},
|
||||
authentication: {
|
||||
|
@ -206,7 +206,12 @@ test('A role with only CREATE_PROJECT_API_TOKEN can create project tokens', asyn
|
||||
});
|
||||
};
|
||||
|
||||
const { request, destroy } = await setupAppWithCustomAuth(stores, preHook);
|
||||
const { request, destroy } = await setupAppWithCustomAuth(
|
||||
stores,
|
||||
preHook,
|
||||
{},
|
||||
db.rawDatabase,
|
||||
);
|
||||
|
||||
await request
|
||||
.post('/api/admin/projects/default/api-tokens')
|
||||
|
@ -10,13 +10,17 @@ let app;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('token_api_serial', getLogger);
|
||||
app = await setupAppWithCustomConfig(db.stores, {
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
app = await setupAppWithCustomConfig(
|
||||
db.stores,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
db.rawDatabase,
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
@ -81,13 +81,17 @@ const getProjects = async () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('favorites_api_serial', getLogger);
|
||||
app = await setupAppWithAuth(db.stores, {
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
app = await setupAppWithAuth(
|
||||
db.stores,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
db.rawDatabase,
|
||||
);
|
||||
stores = db.stores;
|
||||
accessService = app.services.accessService;
|
||||
|
||||
|
@ -10,13 +10,17 @@ let apiTokenStore: ApiTokenStore;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('projects_api_serial', getLogger);
|
||||
app = await setupAppWithCustomConfig(db.stores, {
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
app = await setupAppWithCustomConfig(
|
||||
db.stores,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
db.rawDatabase,
|
||||
);
|
||||
apiTokenStore = db.stores.apiTokenStore;
|
||||
});
|
||||
|
||||
|
@ -11,13 +11,17 @@ let db: ITestDb;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('project_environments_api_serial', getLogger);
|
||||
app = await setupAppWithCustomConfig(db.stores, {
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
app = await setupAppWithCustomConfig(
|
||||
db.stores,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
db.rawDatabase,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
@ -11,13 +11,17 @@ let db: ITestDb;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('project_api_tokens_serial', getLogger);
|
||||
app = await setupAppWithCustomConfig(db.stores, {
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
app = await setupAppWithCustomConfig(
|
||||
db.stores,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
db.rawDatabase,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
@ -8,13 +8,17 @@ let user;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('project_health_api_serial', getLogger);
|
||||
app = await setupAppWithCustomConfig(db.stores, {
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
app = await setupAppWithCustomConfig(
|
||||
db.stores,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
db.rawDatabase,
|
||||
);
|
||||
user = await db.stores.userStore.insert({
|
||||
name: 'Some Name',
|
||||
email: 'test@getunleash.io',
|
||||
|
@ -10,13 +10,17 @@ let projectStore: ProjectStore;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('projects_api_serial', getLogger);
|
||||
app = await setupAppWithCustomConfig(db.stores, {
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
app = await setupAppWithCustomConfig(
|
||||
db.stores,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
db.rawDatabase,
|
||||
);
|
||||
projectStore = db.stores.projectStore;
|
||||
});
|
||||
|
||||
|
@ -11,7 +11,7 @@ let defaultToken;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('metrics_two_api_client', getLogger);
|
||||
app = await setupAppWithAuth(db.stores, {});
|
||||
app = await setupAppWithAuth(db.stores, {}, db.rawDatabase);
|
||||
defaultToken = await app.services.apiTokenService.createApiToken({
|
||||
type: ApiTokenType.CLIENT,
|
||||
project: 'default',
|
||||
|
@ -21,9 +21,13 @@ let db: ITestDb;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('proxy', getLogger);
|
||||
app = await setupAppWithAuth(db.stores, {
|
||||
frontendApiOrigins: ['https://example.com'],
|
||||
});
|
||||
app = await setupAppWithAuth(
|
||||
db.stores,
|
||||
{
|
||||
frontendApiOrigins: ['https://example.com'],
|
||||
},
|
||||
db.rawDatabase,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -263,8 +263,9 @@ export async function setupAppWithCustomAuth(
|
||||
preHook: Function,
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
customOptions?: any,
|
||||
db?: Db,
|
||||
): Promise<IUnleashTest> {
|
||||
return createApp(stores, IAuthType.CUSTOM, preHook, customOptions);
|
||||
return createApp(stores, IAuthType.CUSTOM, preHook, customOptions, db);
|
||||
}
|
||||
|
||||
export async function setupAppWithBaseUrl(
|
||||
|
@ -6,8 +6,8 @@ import { ApiTokenType, IApiToken } from '../../../lib/types/models/api-token';
|
||||
import { DEFAULT_ENV } from '../../../lib/util/constants';
|
||||
import { addDays, subDays } from 'date-fns';
|
||||
import ProjectService from '../../../lib/services/project-service';
|
||||
import { EventService } from '../../../lib/services';
|
||||
import { createProjectService } from '../../../lib/features';
|
||||
import { EventService } from '../../../lib/services';
|
||||
|
||||
let db;
|
||||
let stores;
|
||||
|
@ -1,7 +1,7 @@
|
||||
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-service';
|
||||
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;
|
||||
@ -21,7 +21,13 @@ afterAll(async () => {
|
||||
});
|
||||
|
||||
test('Should update last seen for known toggles', async () => {
|
||||
const service = new LastSeenService(stores, config);
|
||||
const service = new LastSeenService(
|
||||
{
|
||||
lastSeenStore: stores.lastSeenStore,
|
||||
featureToggleStore: stores.featureToggleStore,
|
||||
},
|
||||
config,
|
||||
);
|
||||
const time = Date.now() - 100;
|
||||
await stores.featureToggleStore.create('default', { name: 'ta1' });
|
||||
|
||||
@ -56,7 +62,14 @@ test('Should update last seen for known toggles', async () => {
|
||||
|
||||
test('Should not update last seen toggles with 0 metrics', async () => {
|
||||
// jest.useFakeTimers();
|
||||
const service = new LastSeenService(stores, config, 30);
|
||||
const service = new LastSeenService(
|
||||
{
|
||||
lastSeenStore: stores.lastSeenStore,
|
||||
featureToggleStore: stores.featureToggleStore,
|
||||
},
|
||||
config,
|
||||
30,
|
||||
);
|
||||
const time = Date.now();
|
||||
await stores.featureToggleStore.create('default', { name: 'tb1' });
|
||||
await stores.featureToggleStore.create('default', { name: 'tb2' });
|
||||
@ -96,7 +109,14 @@ test('Should not update last seen toggles with 0 metrics', async () => {
|
||||
|
||||
test('Should not update anything for 0 toggles', async () => {
|
||||
// jest.useFakeTimers();
|
||||
const service = new LastSeenService(stores, config, 30);
|
||||
const service = new LastSeenService(
|
||||
{
|
||||
lastSeenStore: stores.lastSeenStore,
|
||||
featureToggleStore: stores.featureToggleStore,
|
||||
},
|
||||
config,
|
||||
30,
|
||||
);
|
||||
const time = Date.now();
|
||||
await stores.featureToggleStore.create('default', { name: 'tb1' });
|
||||
await stores.featureToggleStore.create('default', { name: 'tb2' });
|
||||
|
@ -4,11 +4,11 @@ import ProjectHealthService from '../../../lib/services/project-health-service';
|
||||
import { createTestConfig } from '../../config/test-config';
|
||||
import { IUnleashStores } from '../../../lib/types';
|
||||
import { IUser } from '../../../lib/server-impl';
|
||||
import { EventService } from '../../../lib/services';
|
||||
import {
|
||||
createFeatureToggleService,
|
||||
createProjectService,
|
||||
} from '../../../lib/features';
|
||||
import { EventService } from '../../../lib/services';
|
||||
|
||||
let stores: IUnleashStores;
|
||||
let db: ITestDb;
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
IFeatureEnvironment,
|
||||
IVariant,
|
||||
} from 'lib/types/model';
|
||||
import { LastSeenInput } from '../../lib/services/client-metrics/last-seen-service';
|
||||
import { LastSeenInput } from '../../lib/services/client-metrics/last-seen/last-seen-service';
|
||||
import { EnvironmentFeatureNames } from '../../lib/db/feature-toggle-store';
|
||||
|
||||
export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
||||
|
1
src/test/fixtures/store.ts
vendored
1
src/test/fixtures/store.ts
vendored
@ -85,6 +85,7 @@ const createStores: () => IUnleashStores = () => {
|
||||
importTogglesStore: {} as IImportTogglesStore,
|
||||
privateProjectStore: {} as IPrivateProjectStore,
|
||||
dependentFeaturesStore: new FakeDependentFeaturesStore(),
|
||||
lastSeenStore: { setLastSeen: async () => {} },
|
||||
};
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user