diff --git a/frontend/src/component/context/ContextList/ContextList/ContextList.test.tsx b/frontend/src/component/context/ContextList/ContextList/ContextList.test.tsx
index 90b523d12eb9..609c794ff835 100644
--- a/frontend/src/component/context/ContextList/ContextList/ContextList.test.tsx
+++ b/frontend/src/component/context/ContextList/ContextList/ContextList.test.tsx
@@ -24,9 +24,7 @@ const setupRoutes = () => {
test('should show the count of projects and features used in', async () => {
setupRoutes();
- render(
- ,
- );
+ render();
await screen.findByText('2 feature toggles');
await screen.findByText('3 projects');
diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap
index 7370414379c4..7780af340179 100644
--- a/src/lib/__snapshots__/create-config.test.ts.snap
+++ b/src/lib/__snapshots__/create-config.test.ts.snap
@@ -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": {
diff --git a/src/lib/db/feature-toggle-store.ts b/src/lib/db/feature-toggle-store.ts
index fcb988214d88..3d96050f32eb 100644
--- a/src/lib/db/feature-toggle-store.ts
+++ b/src/lib/db/feature-toggle-store.ts
@@ -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[] };
diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts
index 8e0ee4e1fbfe..6d4c41f0d952 100644
--- a/src/lib/db/index.ts
+++ b/src/lib/db/index.ts
@@ -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),
};
};
diff --git a/src/lib/features/project/createProjectService.ts b/src/lib/features/project/createProjectService.ts
index 58d2830ae724..e6e230ee6e76 100644
--- a/src/lib/features/project/createProjectService.ts
+++ b/src/lib/features/project/createProjectService.ts
@@ -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,
);
};
diff --git a/src/lib/routes/admin-api/project/api-token.ts b/src/lib/routes/admin-api/project/api-token.ts
index 0a2cf57c55c8..2cdc1ff013ca 100644
--- a/src/lib/routes/admin-api/project/api-token.ts
+++ b/src/lib/routes/admin-api/project/api-token.ts
@@ -149,8 +149,9 @@ export class ProjectApiTokenController extends Controller {
): Promise {
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,
diff --git a/src/lib/routes/client-api/metrics.test.ts b/src/lib/routes/client-api/metrics.test.ts
index 355039ab4c00..dfebb46d3eae 100644
--- a/src/lib/routes/client-api/metrics.test.ts
+++ b/src/lib/routes/client-api/metrics.test.ts
@@ -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';
-async function getSetup(opts?: IUnleashOptions) {
- const stores = createStores();
+let db;
+async function getSetup(opts?: IUnleashOptions) {
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();
});
diff --git a/src/lib/services/client-metrics/last-seen-service.test.ts b/src/lib/services/client-metrics/last-seen-service.test.ts
deleted file mode 100644
index 90f181772a7b..000000000000
--- a/src/lib/services/client-metrics/last-seen-service.test.ts
+++ /dev/null
@@ -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',
- },
- ]);
-});
diff --git a/src/lib/services/client-metrics/last-seen/createLastSeenService.ts b/src/lib/services/client-metrics/last-seen/createLastSeenService.ts
new file mode 100644
index 000000000000..1dac8feeeb48
--- /dev/null
+++ b/src/lib/services/client-metrics/last-seen/createLastSeenService.ts
@@ -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);
+};
diff --git a/src/lib/services/client-metrics/last-seen/fake-last-seen-read-model.ts b/src/lib/services/client-metrics/last-seen/fake-last-seen-read-model.ts
new file mode 100644
index 000000000000..119a64f4ae21
--- /dev/null
+++ b/src/lib/services/client-metrics/last-seen/fake-last-seen-read-model.ts
@@ -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 {
+ return Promise.resolve({});
+ }
+}
diff --git a/src/lib/services/client-metrics/last-seen/fake-last-seen-store.ts b/src/lib/services/client-metrics/last-seen/fake-last-seen-store.ts
new file mode 100644
index 000000000000..94b6be14452f
--- /dev/null
+++ b/src/lib/services/client-metrics/last-seen/fake-last-seen-store.ts
@@ -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 {
+ data.map((lastSeen) => lastSeen);
+ return Promise.resolve();
+ }
+}
diff --git a/src/lib/services/client-metrics/last-seen/last-seen-mapper.ts b/src/lib/services/client-metrics/last-seen/last-seen-mapper.ts
new file mode 100644
index 000000000000..06b075e5c16f
--- /dev/null
+++ b/src/lib/services/client-metrics/last-seen/last-seen-mapper.ts
@@ -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;
+ });
+ }
+}
diff --git a/src/lib/services/client-metrics/last-seen/last-seen-read-model.ts b/src/lib/services/client-metrics/last-seen/last-seen-read-model.ts
new file mode 100644
index 000000000000..9980f98332e8
--- /dev/null
+++ b/src/lib/services/client-metrics/last-seen/last-seen-read-model.ts
@@ -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 {
+ 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;
+ }
+}
diff --git a/src/lib/services/client-metrics/last-seen-service.ts b/src/lib/services/client-metrics/last-seen/last-seen-service.ts
similarity index 69%
rename from src/lib/services/client-metrics/last-seen-service.ts
rename to src/lib/services/client-metrics/last-seen/last-seen-service.ts
index c4d9564f4993..2f438a95c890 100644
--- a/src/lib/services/client-metrics/last-seen-service.ts
+++ b/src/lib/services/client-metrics/last-seen/last-seen-service.ts
@@ -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,
+ {
+ featureToggleStore,
+ lastSeenStore,
+ }: Pick,
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;
}
diff --git a/src/lib/services/client-metrics/last-seen/last-seen-store.ts b/src/lib/services/client-metrics/last-seen/last-seen-store.ts
new file mode 100644
index 000000000000..183cde017c92
--- /dev/null
+++ b/src/lib/services/client-metrics/last-seen/last-seen-store.ts
@@ -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 {
+ 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;
diff --git a/src/lib/services/client-metrics/last-seen/tests/last-seen-mapper.test.ts b/src/lib/services/client-metrics/last-seen/tests/last-seen-mapper.test.ts
new file mode 100644
index 000000000000..07d9ec7f5d2c
--- /dev/null
+++ b/src/lib/services/client-metrics/last-seen/tests/last-seen-mapper.test.ts
@@ -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),
+ );
+});
diff --git a/src/lib/services/client-metrics/last-seen/tests/last-seen-service.test.ts b/src/lib/services/client-metrics/last-seen/tests/last-seen-service.test.ts
new file mode 100644
index 000000000000..8859563d03cb
--- /dev/null
+++ b/src/lib/services/client-metrics/last-seen/tests/last-seen-service.test.ts
@@ -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);
+});
diff --git a/src/lib/services/client-metrics/last-seen/types/last-seen-read-model-type.ts b/src/lib/services/client-metrics/last-seen/types/last-seen-read-model-type.ts
new file mode 100644
index 000000000000..ec06efd63671
--- /dev/null
+++ b/src/lib/services/client-metrics/last-seen/types/last-seen-read-model-type.ts
@@ -0,0 +1,5 @@
+import { IFeatureLastSeenResults } from '../last-seen-read-model';
+
+export interface ILastSeenReadModel {
+ getForFeature(features: string[]): Promise;
+}
diff --git a/src/lib/services/client-metrics/last-seen/types/last-seen-store-type.ts b/src/lib/services/client-metrics/last-seen/types/last-seen-store-type.ts
new file mode 100644
index 000000000000..8670792f3044
--- /dev/null
+++ b/src/lib/services/client-metrics/last-seen/types/last-seen-store-type.ts
@@ -0,0 +1,5 @@
+import { LastSeenInput } from '../last-seen-service';
+
+export interface ILastSeenStore {
+ setLastSeen(data: LastSeenInput[]): Promise;
+}
diff --git a/src/lib/services/client-metrics/metrics-service-v2.test.ts b/src/lib/services/client-metrics/metrics-service-v2.test.ts
index db9336cfc0e5..c744689d9d9e 100644
--- a/src/lib/services/client-metrics/metrics-service-v2.test.ts
+++ b/src/lib/services/client-metrics/metrics-service-v2.test.ts
@@ -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);
diff --git a/src/lib/services/client-metrics/metrics-service-v2.ts b/src/lib/services/client-metrics/metrics-service-v2.ts
index e766110e44ea..db950e342f48 100644
--- a/src/lib/services/client-metrics/metrics-service-v2.ts
+++ b/src/lib/services/client-metrics/metrics-service-v2.ts
@@ -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';
diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts
index 0df9ca9d70db..9744a99bdad0 100644
--- a/src/lib/services/index.ts
+++ b/src/lib/services/index.ts
@@ -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,
diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts
index 302a8e5a3a4a..d26c710e1a11 100644
--- a/src/lib/services/project-service.ts
+++ b/src/lib/services/project-service.ts
@@ -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,
};
diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts
index a02a311e8916..e23e535278d4 100644
--- a/src/lib/types/experimental.ts
+++ b/src/lib/types/experimental.ts
@@ -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 = {
diff --git a/src/lib/types/services.ts b/src/lib/types/services.ts
index ff66b785fee4..486665e02667 100644
--- a/src/lib/types/services.ts
+++ b/src/lib/types/services.ts
@@ -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';
diff --git a/src/lib/types/stores.ts b/src/lib/types/stores.ts
index da1f7258fb33..ad78f996e905 100644
--- a/src/lib/types/stores.ts
+++ b/src/lib/types/stores.ts
@@ -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,
};
diff --git a/src/lib/types/stores/feature-toggle-store.ts b/src/lib/types/stores/feature-toggle-store.ts
index 9108519fb1eb..67bc103898b0 100644
--- a/src/lib/types/stores/feature-toggle-store.ts
+++ b/src/lib/types/stores/feature-toggle-store.ts
@@ -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;
diff --git a/src/migrations/20231003113443-last-seen-at-metrics-table.js b/src/migrations/20231003113443-last-seen-at-metrics-table.js
new file mode 100644
index 000000000000..70a83a03c659
--- /dev/null
+++ b/src/migrations/20231003113443-last-seen-at-metrics-table.js
@@ -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(),
+ );
+};
diff --git a/src/server-dev.ts b/src/server-dev.ts
index dd282fb59291..6f412585a9d0 100644
--- a/src/server-dev.ts
+++ b/src/server-dev.ts
@@ -46,6 +46,7 @@ process.nextTick(async () => {
datadogJsonTemplate: true,
dependentFeatures: true,
transactionalDecorator: true,
+ useLastSeenRefactor: true,
},
},
authentication: {
diff --git a/src/test/e2e/api/admin/api-token.auth.e2e.test.ts b/src/test/e2e/api/admin/api-token.auth.e2e.test.ts
index 97b711971959..b772366afe52 100644
--- a/src/test/e2e/api/admin/api-token.auth.e2e.test.ts
+++ b/src/test/e2e/api/admin/api-token.auth.e2e.test.ts
@@ -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')
diff --git a/src/test/e2e/api/admin/api-token.e2e.test.ts b/src/test/e2e/api/admin/api-token.e2e.test.ts
index d1f0ca695b4e..34ba4fd45859 100644
--- a/src/test/e2e/api/admin/api-token.e2e.test.ts
+++ b/src/test/e2e/api/admin/api-token.e2e.test.ts
@@ -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 () => {
diff --git a/src/test/e2e/api/admin/favorites.e2e.test.ts b/src/test/e2e/api/admin/favorites.e2e.test.ts
index 205b7b1819fe..84e8a5015177 100644
--- a/src/test/e2e/api/admin/favorites.e2e.test.ts
+++ b/src/test/e2e/api/admin/favorites.e2e.test.ts
@@ -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;
diff --git a/src/test/e2e/api/admin/project/api-token.e2e.test.ts b/src/test/e2e/api/admin/project/api-token.e2e.test.ts
index e85a31ba72e0..d0dbf769573f 100644
--- a/src/test/e2e/api/admin/project/api-token.e2e.test.ts
+++ b/src/test/e2e/api/admin/project/api-token.e2e.test.ts
@@ -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;
});
diff --git a/src/test/e2e/api/admin/project/environments.e2e.test.ts b/src/test/e2e/api/admin/project/environments.e2e.test.ts
index d951841aa0ef..fc28e8ebcfab 100644
--- a/src/test/e2e/api/admin/project/environments.e2e.test.ts
+++ b/src/test/e2e/api/admin/project/environments.e2e.test.ts
@@ -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 () => {
diff --git a/src/test/e2e/api/admin/project/project.api.tokens.e2e.test.ts b/src/test/e2e/api/admin/project/project.api.tokens.e2e.test.ts
index a1ad6ed8d103..9bb3a7e0b256 100644
--- a/src/test/e2e/api/admin/project/project.api.tokens.e2e.test.ts
+++ b/src/test/e2e/api/admin/project/project.api.tokens.e2e.test.ts
@@ -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 () => {
diff --git a/src/test/e2e/api/admin/project/project.health.e2e.test.ts b/src/test/e2e/api/admin/project/project.health.e2e.test.ts
index 62de38120932..5a7a6c4818f4 100644
--- a/src/test/e2e/api/admin/project/project.health.e2e.test.ts
+++ b/src/test/e2e/api/admin/project/project.health.e2e.test.ts
@@ -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',
diff --git a/src/test/e2e/api/admin/project/projects.e2e.test.ts b/src/test/e2e/api/admin/project/projects.e2e.test.ts
index e5089d199719..e77f05a13514 100644
--- a/src/test/e2e/api/admin/project/projects.e2e.test.ts
+++ b/src/test/e2e/api/admin/project/projects.e2e.test.ts
@@ -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;
});
diff --git a/src/test/e2e/api/client/metricsV2.e2e.test.ts b/src/test/e2e/api/client/metricsV2.e2e.test.ts
index f7cd95c17138..327707e2ca2c 100644
--- a/src/test/e2e/api/client/metricsV2.e2e.test.ts
+++ b/src/test/e2e/api/client/metricsV2.e2e.test.ts
@@ -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',
diff --git a/src/test/e2e/api/proxy/proxy.e2e.test.ts b/src/test/e2e/api/proxy/proxy.e2e.test.ts
index f18db74384e8..1f9c8955e332 100644
--- a/src/test/e2e/api/proxy/proxy.e2e.test.ts
+++ b/src/test/e2e/api/proxy/proxy.e2e.test.ts
@@ -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(() => {
diff --git a/src/test/e2e/helpers/test-helper.ts b/src/test/e2e/helpers/test-helper.ts
index 81ee8f4141e0..a9a559c75f2b 100644
--- a/src/test/e2e/helpers/test-helper.ts
+++ b/src/test/e2e/helpers/test-helper.ts
@@ -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 {
- return createApp(stores, IAuthType.CUSTOM, preHook, customOptions);
+ return createApp(stores, IAuthType.CUSTOM, preHook, customOptions, db);
}
export async function setupAppWithBaseUrl(
diff --git a/src/test/e2e/services/api-token-service.e2e.test.ts b/src/test/e2e/services/api-token-service.e2e.test.ts
index 0e131a8969d6..32e7620f73bc 100644
--- a/src/test/e2e/services/api-token-service.e2e.test.ts
+++ b/src/test/e2e/services/api-token-service.e2e.test.ts
@@ -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;
diff --git a/src/test/e2e/services/last-seen-service.e2e.test.ts b/src/test/e2e/services/last-seen-service.e2e.test.ts
index c00f05d2a94c..c07cd061ec8f 100644
--- a/src/test/e2e/services/last-seen-service.e2e.test.ts
+++ b/src/test/e2e/services/last-seen-service.e2e.test.ts
@@ -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' });
diff --git a/src/test/e2e/services/project-health-service.e2e.test.ts b/src/test/e2e/services/project-health-service.e2e.test.ts
index be6de7e8363e..1d67e3d2fdee 100644
--- a/src/test/e2e/services/project-health-service.e2e.test.ts
+++ b/src/test/e2e/services/project-health-service.e2e.test.ts
@@ -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;
diff --git a/src/test/fixtures/fake-feature-toggle-store.ts b/src/test/fixtures/fake-feature-toggle-store.ts
index 19a191211cd9..bb6835592eda 100644
--- a/src/test/fixtures/fake-feature-toggle-store.ts
+++ b/src/test/fixtures/fake-feature-toggle-store.ts
@@ -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 {
diff --git a/src/test/fixtures/store.ts b/src/test/fixtures/store.ts
index 805583442034..bfa2015695b2 100644
--- a/src/test/fixtures/store.ts
+++ b/src/test/fixtures/store.ts
@@ -85,6 +85,7 @@ const createStores: () => IUnleashStores = () => {
importTogglesStore: {} as IImportTogglesStore,
privateProjectStore: {} as IPrivateProjectStore,
dependentFeaturesStore: new FakeDependentFeaturesStore(),
+ lastSeenStore: { setLastSeen: async () => {} },
};
};