From d896dbd0c784b78c22c3f027a9cde57cfc11ec07 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Mon, 9 Oct 2023 10:54:00 +0200 Subject: [PATCH] Fix/last seen at by environment (#4939) Initial architecture for last seen at by environment. --- .../__snapshots__/create-config.test.ts.snap | 2 + src/lib/db/feature-toggle-store.ts | 2 +- src/lib/db/index.ts | 2 + .../features/project/createProjectService.ts | 6 + src/lib/routes/admin-api/project/api-token.ts | 3 +- src/lib/routes/client-api/metrics.test.ts | 18 ++- .../client-metrics/last-seen-service.test.ts | 61 ---------- .../last-seen/createLastSeenService.ts | 34 ++++++ .../last-seen/fake-last-seen-read-model.ts | 9 ++ .../last-seen/fake-last-seen-store.ts | 9 ++ .../last-seen/last-seen-mapper.ts | 39 +++++++ .../last-seen/last-seen-read-model.ts | 41 +++++++ .../{ => last-seen}/last-seen-service.ts | 28 +++-- .../last-seen/last-seen-store.ts | 63 ++++++++++ .../last-seen/tests/last-seen-mapper.test.ts | 95 +++++++++++++++ .../last-seen/tests/last-seen-service.test.ts | 109 ++++++++++++++++++ .../types/last-seen-read-model-type.ts | 5 + .../last-seen/types/last-seen-store-type.ts | 5 + .../client-metrics/metrics-service-v2.test.ts | 10 +- .../client-metrics/metrics-service-v2.ts | 2 +- src/lib/services/index.ts | 30 ++--- src/lib/services/project-service.ts | 24 +++- src/lib/types/experimental.ts | 7 +- src/lib/types/services.ts | 2 +- src/lib/types/stores.ts | 3 + src/lib/types/stores/feature-toggle-store.ts | 2 +- ...231003113443-last-seen-at-metrics-table.js | 26 +++++ src/server-dev.ts | 1 + .../e2e/api/admin/api-token.auth.e2e.test.ts | 7 +- src/test/e2e/api/admin/api-token.e2e.test.ts | 14 ++- src/test/e2e/api/admin/favorites.e2e.test.ts | 14 ++- .../api/admin/project/api-token.e2e.test.ts | 14 ++- .../admin/project/environments.e2e.test.ts | 14 ++- .../project/project.api.tokens.e2e.test.ts | 14 ++- .../admin/project/project.health.e2e.test.ts | 14 ++- .../api/admin/project/projects.e2e.test.ts | 14 ++- src/test/e2e/api/client/metricsV2.e2e.test.ts | 2 +- src/test/e2e/api/proxy/proxy.e2e.test.ts | 10 +- src/test/e2e/helpers/test-helper.ts | 3 +- .../services/api-token-service.e2e.test.ts | 2 +- .../services/last-seen-service.e2e.test.ts | 28 ++++- .../project-health-service.e2e.test.ts | 2 +- .../fixtures/fake-feature-toggle-store.ts | 2 +- src/test/fixtures/store.ts | 1 + 44 files changed, 649 insertions(+), 144 deletions(-) delete mode 100644 src/lib/services/client-metrics/last-seen-service.test.ts create mode 100644 src/lib/services/client-metrics/last-seen/createLastSeenService.ts create mode 100644 src/lib/services/client-metrics/last-seen/fake-last-seen-read-model.ts create mode 100644 src/lib/services/client-metrics/last-seen/fake-last-seen-store.ts create mode 100644 src/lib/services/client-metrics/last-seen/last-seen-mapper.ts create mode 100644 src/lib/services/client-metrics/last-seen/last-seen-read-model.ts rename src/lib/services/client-metrics/{ => last-seen}/last-seen-service.ts (69%) create mode 100644 src/lib/services/client-metrics/last-seen/last-seen-store.ts create mode 100644 src/lib/services/client-metrics/last-seen/tests/last-seen-mapper.test.ts create mode 100644 src/lib/services/client-metrics/last-seen/tests/last-seen-service.test.ts create mode 100644 src/lib/services/client-metrics/last-seen/types/last-seen-read-model-type.ts create mode 100644 src/lib/services/client-metrics/last-seen/types/last-seen-store-type.ts create mode 100644 src/migrations/20231003113443-last-seen-at-metrics-table.js 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 3d9adaf45661..3efe05d19be2 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 () => {} }, }; };