From f297d861ea2c9eec2d1cbe3103512ad6e6940ab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Mon, 18 Nov 2024 10:47:29 +0100 Subject: [PATCH] chore: use memoized stats for version (#8776) ## About the changes Builds on top of #8766 to use memoized results from stats-service. Because stats service depends on version service, and to avoid making the version service depend on stats service creating a cyclic dependency. I've introduced a telemetry data provider. It's not clean code, but it does the job. After validating this works as expected I'll clean up Added an e2e test validating that the replacement is correct: [8475492](https://github.com/Unleash/unleash/pull/8776/commits/847549234c61bf01ce67f2a2849ff75004a9bbe6) and it did: https://github.com/Unleash/unleash/actions/runs/11861854341/job/33060032638?pr=8776#step:9:294 Finally, cleaning up version service --- .../createInstanceStatsService.ts | 42 +-- .../instance-stats-service.test.ts | 7 +- .../instance-stats/instance-stats-service.ts | 158 ++++++++-- .../features/scheduler/schedule-services.ts | 5 +- src/lib/metrics.test.ts | 7 +- src/lib/services/index.ts | 21 +- src/lib/services/version-service.test.ts | 297 +++++++----------- src/lib/services/version-service.ts | 219 +------------ 8 files changed, 287 insertions(+), 469 deletions(-) diff --git a/src/lib/features/instance-stats/createInstanceStatsService.ts b/src/lib/features/instance-stats/createInstanceStatsService.ts index bf673dd5f3..f1718c1491 100644 --- a/src/lib/features/instance-stats/createInstanceStatsService.ts +++ b/src/lib/features/instance-stats/createInstanceStatsService.ts @@ -102,6 +102,12 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => { const trafficDataUsageStore = new TrafficDataUsageStore(db, getLogger); + const featureStrategiesStore = new FeatureStrategyStore( + db, + eventBus, + getLogger, + flagResolver, + ); const instanceStatsServiceStores = { featureToggleStore, userStore, @@ -118,27 +124,14 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => { apiTokenStore, clientMetricsStoreV2, featureStrategiesReadModel, + featureStrategiesStore, trafficDataUsageStore, }; - const featureStrategiesStore = new FeatureStrategyStore( - db, - eventBus, - getLogger, - flagResolver, - ); - const versionServiceStores = { - ...instanceStatsServiceStores, - featureStrategiesStore, - }; + const versionServiceStores = { settingStore }; const getActiveUsers = createGetActiveUsers(db); const getProductionChanges = createGetProductionChanges(db); const getLicencedUsers = createGetLicensedUsers(db); - const versionService = new VersionService( - versionServiceStores, - config, - getActiveUsers, - getProductionChanges, - ); + const versionService = new VersionService(versionServiceStores, config); const instanceStatsService = new InstanceStatsService( instanceStatsServiceStores, @@ -170,7 +163,7 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => { const clientMetricsStoreV2 = new FakeClientMetricsStoreV2(); const featureStrategiesReadModel = new FakeFeatureStrategiesReadModel(); const trafficDataUsageStore = new FakeTrafficDataUsageStore(); - + const featureStrategiesStore = new FakeFeatureStrategiesStore(); const instanceStatsServiceStores = { featureToggleStore, userStore, @@ -187,22 +180,15 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => { apiTokenStore, clientMetricsStoreV2, featureStrategiesReadModel, + featureStrategiesStore, trafficDataUsageStore, }; - const featureStrategiesStore = new FakeFeatureStrategiesStore(); - const versionServiceStores = { - ...instanceStatsServiceStores, - featureStrategiesStore, - }; + + const versionServiceStores = { settingStore }; const getActiveUsers = createFakeGetActiveUsers(); const getLicensedUsers = createFakeGetLicensedUsers(); const getProductionChanges = createFakeGetProductionChanges(); - const versionService = new VersionService( - versionServiceStores, - config, - getActiveUsers, - getProductionChanges, - ); + const versionService = new VersionService(versionServiceStores, config); const instanceStatsService = new InstanceStatsService( instanceStatsServiceStores, diff --git a/src/lib/features/instance-stats/instance-stats-service.test.ts b/src/lib/features/instance-stats/instance-stats-service.test.ts index 43fffce9ec..fc878cb653 100644 --- a/src/lib/features/instance-stats/instance-stats-service.test.ts +++ b/src/lib/features/instance-stats/instance-stats-service.test.ts @@ -27,12 +27,7 @@ beforeEach(() => { const config = createTestConfig(); flagResolver = config.flagResolver; stores = createStores(); - versionService = new VersionService( - stores, - config, - createFakeGetActiveUsers(), - createFakeGetProductionChanges(), - ); + versionService = new VersionService(stores, config); clientInstanceStore = stores.clientInstanceStore; instanceStatsService = new InstanceStatsService( stores, diff --git a/src/lib/features/instance-stats/instance-stats-service.ts b/src/lib/features/instance-stats/instance-stats-service.ts index dc7f5223dc..2349ce9281 100644 --- a/src/lib/features/instance-stats/instance-stats-service.ts +++ b/src/lib/features/instance-stats/instance-stats-service.ts @@ -6,6 +6,7 @@ import type { IClientMetricsStoreV2, IEventStore, IFeatureStrategiesReadModel, + IFeatureStrategiesStore, ITrafficDataUsageStore, IUnleashStores, } from '../../types/stores'; @@ -33,6 +34,7 @@ import type { GetProductionChanges } from './getProductionChanges'; import { format, minutesToMilliseconds } from 'date-fns'; import memoizee from 'memoizee'; import type { GetLicensedUsers } from './getLicensedUsers'; +import type { IFeatureUsageInfo } from '../../services/version-service'; export type TimeRange = 'allTime' | '30d' | '7d'; @@ -124,6 +126,8 @@ export class InstanceStatsService { private featureStrategiesReadModel: IFeatureStrategiesReadModel; + private featureStrategiesStore: IFeatureStrategiesStore; + private trafficDataUsageStore: ITrafficDataUsageStore; constructor( @@ -143,6 +147,7 @@ export class InstanceStatsService { apiTokenStore, clientMetricsStoreV2, featureStrategiesReadModel, + featureStrategiesStore, trafficDataUsageStore, }: Pick< IUnleashStores, @@ -161,6 +166,7 @@ export class InstanceStatsService { | 'apiTokenStore' | 'clientMetricsStoreV2' | 'featureStrategiesReadModel' + | 'featureStrategiesStore' | 'trafficDataUsageStore' >, { @@ -186,13 +192,20 @@ export class InstanceStatsService { this.eventStore = eventStore; this.clientInstanceStore = clientInstanceStore; this.logger = getLogger('services/stats-service.js'); - this.getActiveUsers = getActiveUsers; - this.getLicencedUsers = getLicencedUsers; - this.getProductionChanges = getProductionChanges; + this.getActiveUsers = () => + this.memorize('getActiveUsers', getActiveUsers.bind(this)); + this.getLicencedUsers = () => + this.memorize('getLicencedUsers', getLicencedUsers.bind(this)); + this.getProductionChanges = () => + this.memorize( + 'getProductionChanges', + getProductionChanges.bind(this), + ); this.apiTokenStore = apiTokenStore; this.clientMetricsStore = clientMetricsStoreV2; this.flagResolver = flagResolver; this.featureStrategiesReadModel = featureStrategiesReadModel; + this.featureStrategiesStore = featureStrategiesStore; this.trafficDataUsageStore = trafficDataUsageStore; } @@ -323,8 +336,8 @@ export class InstanceStatsService { this.getRegisteredUsers(), this.countServiceAccounts(), this.countApiTokensByType(), - this.memorize('getActiveUsers', this.getActiveUsers.bind(this)), - this.memorize('getLicencedUsers', this.getLicencedUsers.bind(this)), + this.getActiveUsers(), + this.getLicencedUsers(), this.getProjectModeCount(), this.contextFieldCount(), this.groupCount(), @@ -339,20 +352,9 @@ export class InstanceStatsService { this.hasPasswordAuth(), this.hasSCIM(), this.appCount ? this.appCount : this.getLabeledAppCounts(), - this.memorize('deprecatedFilteredCountFeaturesExported', () => - this.eventStore.deprecatedFilteredCount({ - type: FEATURES_EXPORTED, - }), - ), - this.memorize('deprecatedFilteredCountFeaturesImported', () => - this.eventStore.deprecatedFilteredCount({ - type: FEATURES_IMPORTED, - }), - ), - this.memorize( - 'getProductionChanges', - this.getProductionChanges.bind(this), - ), + this.featuresExported(), + this.featuresImported(), + this.getProductionChanges(), this.countPreviousDayHourlyMetricsBuckets(), this.memorize( 'maxFeatureEnvironmentStrategies', @@ -413,6 +415,124 @@ export class InstanceStatsService { }; } + async getFeatureUsageInfo(): Promise { + const [ + featureToggles, + users, + projectModeCount, + contextFields, + groups, + roles, + customRootRoles, + customRootRolesInUse, + environments, + segments, + strategies, + SAMLenabled, + OIDCenabled, + featureExports, + featureImports, + customStrategies, + customStrategiesInUse, + userActive, + productionChanges, + postgresVersion, + ] = await Promise.all([ + this.getToggleCount(), + this.getRegisteredUsers(), + this.getProjectModeCount(), + this.contextFieldCount(), + this.groupCount(), + this.roleCount(), + this.customRolesCount(), + this.customRolesCountInUse(), + this.environmentCount(), + this.segmentCount(), + this.strategiesCount(), + this.hasSAML(), + this.hasOIDC(), + this.featuresExported(), + this.featuresImported(), + this.customStrategiesCount(), + this.customStrategiesInUseCount(), + this.getActiveUsers(), + this.getProductionChanges(), + this.postgresVersion(), + ]); + const versionInfo = await this.versionService.getVersionInfo(); + + const featureInfo = { + featureToggles, + users, + projects: projectModeCount + .map((p) => p.count) + .reduce((a, b) => a + b, 0), + contextFields, + groups, + roles, + customRootRoles, + customRootRolesInUse, + environments, + segments, + strategies, + SAMLenabled, + OIDCenabled, + featureExports, + featureImports, + customStrategies, + customStrategiesInUse, + instanceId: versionInfo.instanceId, + versionOSS: versionInfo.current.oss, + versionEnterprise: versionInfo.current.enterprise, + activeUsers30: userActive.last30, + activeUsers60: userActive.last60, + activeUsers90: userActive.last90, + productionChanges30: productionChanges.last30, + productionChanges60: productionChanges.last60, + productionChanges90: productionChanges.last90, + postgresVersion, + }; + return featureInfo; + } + + featuresExported(): Promise { + return this.memorize('deprecatedFilteredCountFeaturesExported', () => + this.eventStore.deprecatedFilteredCount({ + type: FEATURES_EXPORTED, + }), + ); + } + + featuresImported(): Promise { + return this.memorize('deprecatedFilteredCountFeaturesImported', () => + this.eventStore.deprecatedFilteredCount({ + type: FEATURES_IMPORTED, + }), + ); + } + + customStrategiesCount(): Promise { + return this.memorize( + 'customStrategiesCount', + async () => + (await this.strategyStore.getEditableStrategies()).length, + ); + } + + customStrategiesInUseCount(): Promise { + return this.memorize( + 'customStrategiesInUseCount', + async () => + await this.featureStrategiesStore.getCustomStrategiesInUseCount(), + ); + } + + postgresVersion(): Promise { + return this.memorize('postgresVersion', () => + this.settingStore.postgresVersion(), + ); + } + groupCount(): Promise { return this.memorize('groupCount', () => this.groupStore.count()); } diff --git a/src/lib/features/scheduler/schedule-services.ts b/src/lib/features/scheduler/schedule-services.ts index 69d3a218fc..ad5a93bbe7 100644 --- a/src/lib/features/scheduler/schedule-services.ts +++ b/src/lib/features/scheduler/schedule-services.ts @@ -126,7 +126,10 @@ export const scheduleServices = async ( ); schedulerService.schedule( - versionService.checkLatestVersion.bind(versionService), + () => + versionService.checkLatestVersion(() => + instanceStatsService.getFeatureUsageInfo(), + ), hoursToMilliseconds(48), 'checkLatestVersion', ); diff --git a/src/lib/metrics.test.ts b/src/lib/metrics.test.ts index 4206024281..d72b6cc61f 100644 --- a/src/lib/metrics.test.ts +++ b/src/lib/metrics.test.ts @@ -63,12 +63,7 @@ beforeAll(async () => { eventStore = stores.eventStore; environmentStore = new FakeEnvironmentStore(); stores.environmentStore = environmentStore; - const versionService = new VersionService( - stores, - config, - createFakeGetActiveUsers(), - createFakeGetProductionChanges(), - ); + const versionService = new VersionService(stores, config); db = await dbInit('metrics_test', getLogger); featureLifeCycleReadModel = new FeatureLifecycleReadModel( diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 3015a22f76..f5a921858c 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -82,10 +82,6 @@ import { createFakePrivateProjectChecker, createPrivateProjectChecker, } from '../features/private-project/createPrivateProjectChecker'; -import { - createFakeGetActiveUsers, - createGetActiveUsers, -} from '../features/instance-stats/getActiveUsers'; import { DependentFeaturesService } from '../features/dependent-features/dependent-features-service'; import { createDependentFeaturesService, @@ -97,10 +93,6 @@ import { createFakeLastSeenService, createLastSeenService, } from '../features/metrics/last-seen/createLastSeenService'; -import { - createFakeGetProductionChanges, - createGetProductionChanges, -} from '../features/instance-stats/getProductionChanges'; import { createClientFeatureToggleService, createFakeClientFeatureToggleService, @@ -247,19 +239,8 @@ export const createServices = ( const accountService = new AccountService(stores, config, { accessService, }); - const getActiveUsers = db - ? createGetActiveUsers(db) - : createFakeGetActiveUsers(); - const getProductionChanges = db - ? createGetProductionChanges(db) - : createFakeGetProductionChanges(); - const versionService = new VersionService( - stores, - config, - getActiveUsers, - getProductionChanges, - ); + const versionService = new VersionService(stores, config); const healthService = new HealthService(stores, config); const userFeedbackService = new UserFeedbackService(stores, config); const changeRequestAccessReadModel = db diff --git a/src/lib/services/version-service.test.ts b/src/lib/services/version-service.test.ts index 57371ba279..354005a623 100644 --- a/src/lib/services/version-service.test.ts +++ b/src/lib/services/version-service.test.ts @@ -3,10 +3,7 @@ import createStores from '../../test/fixtures/store'; import version from '../util/version'; import getLogger from '../../test/fixtures/no-logger'; import VersionService from './version-service'; -import { v4 as uuidv4 } from 'uuid'; import { randomId } from '../util/random-id'; -import { createFakeGetActiveUsers } from '../features/instance-stats/getActiveUsers'; -import { createFakeGetProductionChanges } from '../features/instance-stats/getProductionChanges'; beforeAll(() => { nock.disableNetConnect(); @@ -16,10 +13,39 @@ afterAll(() => { nock.enableNetConnect(); }); +const fakeTelemetryData = { + featureToggles: 0, + users: 0, + projects: 1, + contextFields: 3, + groups: 0, + roles: 5, + customRootRoles: 0, + customRootRolesInUse: 0, + environments: 1, + segments: 0, + strategies: 3, + SAMLenabled: false, + OIDCenabled: false, + featureExports: 0, + featureImports: 0, + customStrategies: 3, + customStrategiesInUse: 0, + instanceId: '1460588e-d5f4-4ac2-9962-c8631f6b8dad', + versionOSS: '6.4.1', + versionEnterprise: '', + activeUsers30: 0, + activeUsers60: 0, + activeUsers90: 0, + productionChanges30: 0, + productionChanges60: 0, + productionChanges90: 0, + postgresVersion: '17.1 (Debian 17.1-1.pgdg120+1)', +}; + test('yields current versions', async () => { const url = `https://${randomId()}.example.com`; const stores = createStores(); - await stores.settingStore.insert('instanceInfo', { id: '1234abc' }); const latest = { oss: '5.0.0', enterprise: '5.0.0', @@ -33,17 +59,12 @@ test('yields current versions', async () => { versions: latest, }), ]); - const service = new VersionService( - stores, - { - getLogger, - versionCheck: { url, enable: true }, - telemetry: true, - }, - createFakeGetActiveUsers(), - createFakeGetProductionChanges(), - ); - await service.checkLatestVersion(); + const service = new VersionService(stores, { + getLogger, + versionCheck: { url, enable: true }, + telemetry: true, + }); + await service.checkLatestVersion(() => Promise.resolve(fakeTelemetryData)); const versionInfo = await service.getVersionInfo(); expect(scope.isDone()).toEqual(true); expect(versionInfo.current.oss).toBe(version); @@ -56,7 +77,6 @@ test('supports setting enterprise version as well', async () => { const url = `https://${randomId()}.example.com`; const stores = createStores(); const enterpriseVersion = '3.7.0'; - await stores.settingStore.insert('instanceInfo', { id: '1234abc' }); const latest = { oss: '4.0.0', enterprise: '4.0.0', @@ -71,18 +91,13 @@ test('supports setting enterprise version as well', async () => { }), ]); - const service = new VersionService( - stores, - { - getLogger, - versionCheck: { url, enable: true }, - enterpriseVersion, - telemetry: true, - }, - createFakeGetActiveUsers(), - createFakeGetProductionChanges(), - ); - await service.checkLatestVersion(); + const service = new VersionService(stores, { + getLogger, + versionCheck: { url, enable: true }, + enterpriseVersion, + telemetry: true, + }); + await service.checkLatestVersion(() => Promise.resolve(fakeTelemetryData)); const versionInfo = await service.getVersionInfo(); expect(scope.isDone()).toEqual(true); expect(versionInfo.current.oss).toBe(version); @@ -95,7 +110,6 @@ test('if version check is not enabled should not make any calls', async () => { const url = `https://${randomId()}.example.com`; const stores = createStores(); const enterpriseVersion = '3.7.0'; - await stores.settingStore.insert('instanceInfo', { id: '1234abc' }); const latest = { oss: '4.0.0', enterprise: '4.0.0', @@ -110,18 +124,13 @@ test('if version check is not enabled should not make any calls', async () => { }), ]); - const service = new VersionService( - stores, - { - getLogger, - versionCheck: { url, enable: false }, - enterpriseVersion, - telemetry: true, - }, - createFakeGetActiveUsers(), - createFakeGetProductionChanges(), - ); - await service.checkLatestVersion(); + const service = new VersionService(stores, { + getLogger, + versionCheck: { url, enable: false }, + enterpriseVersion, + telemetry: true, + }); + await service.checkLatestVersion(() => Promise.resolve(fakeTelemetryData)); const versionInfo = await service.getVersionInfo(); expect(scope.isDone()).toEqual(false); expect(versionInfo.current.oss).toBe(version); @@ -135,7 +144,6 @@ test('sets featureinfo', async () => { const url = `https://${randomId()}.example.com`; const stores = createStores(); const enterpriseVersion = '4.0.0'; - await stores.settingStore.insert('instanceInfo', { id: '1234abc' }); const latest = { oss: '4.0.0', enterprise: '4.0.0', @@ -146,8 +154,10 @@ test('sets featureinfo', async () => { '/', (body) => body.featureInfo && - body.featureInfo.featureToggles === 0 && - body.featureInfo.environments === 0, + body.featureInfo.featureToggles === + fakeTelemetryData.featureToggles && + body.featureInfo.environments === + fakeTelemetryData.environments, ) .reply(() => [ 200, @@ -157,18 +167,13 @@ test('sets featureinfo', async () => { }), ]); - const service = new VersionService( - stores, - { - getLogger, - versionCheck: { url, enable: true }, - enterpriseVersion, - telemetry: true, - }, - createFakeGetActiveUsers(), - createFakeGetProductionChanges(), - ); - await service.checkLatestVersion(); + const service = new VersionService(stores, { + getLogger, + versionCheck: { url, enable: true }, + enterpriseVersion, + telemetry: true, + }); + await service.checkLatestVersion(() => Promise.resolve(fakeTelemetryData)); expect(scope.isDone()).toEqual(true); nock.cleanAll(); }); @@ -177,19 +182,6 @@ test('counts toggles', async () => { const url = `https://${randomId()}.example.com`; const stores = createStores(); const enterpriseVersion = '4.0.0'; - await stores.settingStore.insert('instanceInfo', { id: '1234abc' }); - await stores.settingStore.insert('unleash.enterprise.auth.oidc', { - enabled: true, - }); - await stores.featureToggleStore.create('project', { - name: uuidv4(), - createdByUserId: 9999, - }); - await stores.strategyStore.createStrategy({ - name: uuidv4(), - editable: true, - parameters: [], - }); const latest = { oss: '4.0.0', enterprise: '4.0.0', @@ -200,11 +192,15 @@ test('counts toggles', async () => { '/', (body) => body.featureInfo && - body.featureInfo.featureToggles === 1 && - body.featureInfo.environments === 0 && - body.featureInfo.customStrategies === 1 && - body.featureInfo.customStrategiesInUse === 3 && - body.featureInfo.OIDCenabled, + body.featureInfo.featureToggles === + fakeTelemetryData.featureToggles && + body.featureInfo.environments === + fakeTelemetryData.environments && + body.featureInfo.customStrategies === + fakeTelemetryData.customStrategies && + body.featureInfo.customStrategiesInUse === + fakeTelemetryData.customRootRolesInUse && + body.featureInfo.OIDCenabled === fakeTelemetryData.OIDCenabled, ) .reply(() => [ 200, @@ -214,18 +210,13 @@ test('counts toggles', async () => { }), ]); - const service = new VersionService( - stores, - { - getLogger, - versionCheck: { url, enable: true }, - enterpriseVersion, - telemetry: true, - }, - createFakeGetActiveUsers(), - createFakeGetProductionChanges(), - ); - await service.checkLatestVersion(); + const service = new VersionService(stores, { + getLogger, + versionCheck: { url, enable: true }, + enterpriseVersion, + telemetry: true, + }); + await service.checkLatestVersion(() => Promise.resolve(fakeTelemetryData)); expect(scope.isDone()).toEqual(true); nock.cleanAll(); }); @@ -234,34 +225,7 @@ test('counts custom strategies', async () => { const url = `https://${randomId()}.example.com`; const stores = createStores(); const enterpriseVersion = '4.0.0'; - const strategyName = uuidv4(); - const toggleName = uuidv4(); - await stores.settingStore.insert('instanceInfo', { id: '1234abc' }); - await stores.settingStore.insert('unleash.enterprise.auth.oidc', { - enabled: true, - }); - await stores.featureToggleStore.create('project', { - name: toggleName, - createdByUserId: 9999, - }); - await stores.strategyStore.createStrategy({ - name: strategyName, - editable: true, - parameters: [], - }); - await stores.strategyStore.createStrategy({ - name: uuidv4(), - editable: true, - parameters: [], - }); - await stores.featureStrategiesStore.createStrategyFeatureEnv({ - featureName: toggleName, - projectId: 'project', - environment: 'default', - strategyName: strategyName, - parameters: {}, - constraints: [], - }); + const latest = { oss: '4.0.0', enterprise: '4.0.0', @@ -272,11 +236,15 @@ test('counts custom strategies', async () => { '/', (body) => body.featureInfo && - body.featureInfo.featureToggles === 1 && - body.featureInfo.environments === 0 && - body.featureInfo.customStrategies === 2 && - body.featureInfo.customStrategiesInUse === 3 && - body.featureInfo.OIDCenabled, + body.featureInfo.featureToggles === + fakeTelemetryData.featureToggles && + body.featureInfo.environments === + fakeTelemetryData.environments && + body.featureInfo.customStrategies === + fakeTelemetryData.customStrategies && + body.featureInfo.customStrategiesInUse === + fakeTelemetryData.customRootRolesInUse && + body.featureInfo.OIDCenabled === fakeTelemetryData.OIDCenabled, ) .reply(() => [ 200, @@ -286,18 +254,13 @@ test('counts custom strategies', async () => { }), ]); - const service = new VersionService( - stores, - { - getLogger, - versionCheck: { url, enable: true }, - enterpriseVersion, - telemetry: true, - }, - createFakeGetActiveUsers(), - createFakeGetProductionChanges(), - ); - await service.checkLatestVersion(); + const service = new VersionService(stores, { + getLogger, + versionCheck: { url, enable: true }, + enterpriseVersion, + telemetry: true, + }); + await service.checkLatestVersion(() => Promise.resolve(fakeTelemetryData)); expect(scope.isDone()).toEqual(true); nock.cleanAll(); }); @@ -306,26 +269,22 @@ test('counts active users', async () => { const url = `https://${randomId()}.example.com`; const stores = createStores(); const enterpriseVersion = '4.0.0'; - await stores.settingStore.insert('instanceInfo', { id: '1234abc' }); const latest = { oss: '4.0.0', enterprise: '4.0.0', }; - const fakeActiveUsers = createFakeGetActiveUsers({ - last7: 2, - last30: 5, - last60: 10, - last90: 20, - }); - const fakeProductionChanges = createFakeGetProductionChanges(); + const scope = nock(url) .post( '/', (body) => body.featureInfo && - body.featureInfo.activeUsers30 === 5 && - body.featureInfo.activeUsers60 === 10 && - body.featureInfo.activeUsers90 === 20, + body.featureInfo.activeUsers30 === + fakeTelemetryData.activeUsers30 && + body.featureInfo.activeUsers60 === + fakeTelemetryData.activeUsers60 && + body.featureInfo.activeUsers90 === + fakeTelemetryData.activeUsers90, ) .reply(() => [ 200, @@ -335,18 +294,13 @@ test('counts active users', async () => { }), ]); - const service = new VersionService( - stores, - { - getLogger, - versionCheck: { url, enable: true }, - enterpriseVersion, - telemetry: true, - }, - fakeActiveUsers, - fakeProductionChanges, - ); - await service.checkLatestVersion(); + const service = new VersionService(stores, { + getLogger, + versionCheck: { url, enable: true }, + enterpriseVersion, + telemetry: true, + }); + await service.checkLatestVersion(() => Promise.resolve(fakeTelemetryData)); expect(scope.isDone()).toEqual(true); nock.cleanAll(); }); @@ -354,25 +308,21 @@ test('Counts production changes', async () => { const url = `https://${randomId()}.example.com`; const stores = createStores(); const enterpriseVersion = '4.0.0'; - await stores.settingStore.insert('instanceInfo', { id: '1234abc' }); const latest = { oss: '4.0.0', enterprise: '4.0.0', }; - const fakeActiveUsers = createFakeGetActiveUsers(); - const fakeProductionChanges = createFakeGetProductionChanges({ - last30: 5, - last60: 10, - last90: 20, - }); const scope = nock(url) .post( '/', (body) => body.featureInfo && - body.featureInfo.productionChanges30 === 5 && - body.featureInfo.productionChanges60 === 10 && - body.featureInfo.productionChanges90 === 20, + body.featureInfo.productionChanges30 === + fakeTelemetryData.productionChanges30 && + body.featureInfo.productionChanges60 === + fakeTelemetryData.productionChanges60 && + body.featureInfo.productionChanges90 === + fakeTelemetryData.productionChanges90, ) .reply(() => [ 200, @@ -382,18 +332,13 @@ test('Counts production changes', async () => { }), ]); - const service = new VersionService( - stores, - { - getLogger, - versionCheck: { url, enable: true }, - enterpriseVersion, - telemetry: true, - }, - fakeActiveUsers, - fakeProductionChanges, - ); - await service.checkLatestVersion(); + const service = new VersionService(stores, { + getLogger, + versionCheck: { url, enable: true }, + enterpriseVersion, + telemetry: true, + }); + await service.checkLatestVersion(() => Promise.resolve(fakeTelemetryData)); expect(scope.isDone()).toEqual(true); nock.cleanAll(); }); diff --git a/src/lib/services/version-service.ts b/src/lib/services/version-service.ts index 4e2648e638..798e9406a2 100644 --- a/src/lib/services/version-service.ts +++ b/src/lib/services/version-service.ts @@ -1,26 +1,9 @@ import fetch from 'make-fetch-happen'; -import type { - IContextFieldStore, - IEnvironmentStore, - IEventStore, - IFeatureStrategiesStore, - IFeatureToggleStore, - IGroupStore, - IProjectStore, - IRoleStore, - ISegmentStore, - IUnleashStores, - IUserStore, -} from '../types/stores'; +import type { IUnleashStores } from '../types/stores'; import type { IUnleashConfig } from '../types/option'; import version from '../util/version'; import type { Logger } from '../logger'; import type { ISettingStore } from '../types/stores/settings-store'; -import type { IStrategyStore } from '../types'; -import { FEATURES_EXPORTED, FEATURES_IMPORTED } from '../types'; -import { CUSTOM_ROOT_ROLE_TYPE } from '../util'; -import type { GetActiveUsers } from '../features/instance-stats/getActiveUsers'; -import type { GetProductionChanges } from '../features/instance-stats/getProductionChanges'; export interface IVersionInfo { oss: string; @@ -73,32 +56,6 @@ export default class VersionService { private settingStore: ISettingStore; - private strategyStore: IStrategyStore; - - private userStore: IUserStore; - - private featureToggleStore: IFeatureToggleStore; - - private projectStore: IProjectStore; - - private environmentStore: IEnvironmentStore; - - private contextFieldStore: IContextFieldStore; - - private groupStore: IGroupStore; - - private roleStore: IRoleStore; - - private segmentStore: ISegmentStore; - - private eventStore: IEventStore; - - private featureStrategiesStore: IFeatureStrategiesStore; - - private getActiveUsers: GetActiveUsers; - - private getProductionChanges: GetProductionChanges; - private current: IVersionInfo; private latest?: IVersionInfo; @@ -116,34 +73,7 @@ export default class VersionService { private timer: NodeJS.Timeout; constructor( - { - settingStore, - strategyStore, - userStore, - featureToggleStore, - projectStore, - environmentStore, - contextFieldStore, - groupStore, - roleStore, - segmentStore, - eventStore, - featureStrategiesStore, - }: Pick< - IUnleashStores, - | 'settingStore' - | 'strategyStore' - | 'userStore' - | 'featureToggleStore' - | 'projectStore' - | 'environmentStore' - | 'contextFieldStore' - | 'groupStore' - | 'roleStore' - | 'segmentStore' - | 'eventStore' - | 'featureStrategiesStore' - >, + { settingStore }: Pick, { getLogger, versionCheck, @@ -153,24 +83,9 @@ export default class VersionService { IUnleashConfig, 'getLogger' | 'versionCheck' | 'enterpriseVersion' | 'telemetry' >, - getActiveUsers: GetActiveUsers, - getProductionChanges: GetProductionChanges, ) { this.logger = getLogger('lib/services/version-service.js'); this.settingStore = settingStore; - this.strategyStore = strategyStore; - this.userStore = userStore; - this.featureToggleStore = featureToggleStore; - this.projectStore = projectStore; - this.environmentStore = environmentStore; - this.contextFieldStore = contextFieldStore; - this.groupStore = groupStore; - this.roleStore = roleStore; - this.segmentStore = segmentStore; - this.eventStore = eventStore; - this.getActiveUsers = getActiveUsers; - this.getProductionChanges = getProductionChanges; - this.featureStrategiesStore = featureStrategiesStore; this.current = { oss: version, enterprise: enterpriseVersion || '', @@ -199,7 +114,9 @@ export default class VersionService { return this.instanceId; } - async checkLatestVersion(): Promise { + async checkLatestVersion( + telemetryDataProvider: () => Promise, + ): Promise { const instanceId = await this.getInstanceId(); this.logger.debug( `Checking for newest version for instanceId=${instanceId}`, @@ -212,8 +129,7 @@ export default class VersionService { }; if (this.telemetryEnabled) { - versionPayload.featureInfo = - await this.getFeatureUsageInfo(); + versionPayload.featureInfo = await telemetryDataProvider(); } if (this.versionCheckUrl) { const res = await fetch(this.versionCheckUrl, { @@ -241,129 +157,6 @@ export default class VersionService { } } } - - async getFeatureUsageInfo(): Promise { - const [ - featureToggles, - users, - projects, - contextFields, - groups, - roles, - customRootRoles, - customRootRolesInUse, - environments, - segments, - strategies, - SAMLenabled, - OIDCenabled, - featureExports, - featureImports, - userActive, - productionChanges, - postgresVersion, - ] = await Promise.all([ - this.featureToggleStore.count({ - archived: false, - }), - this.userStore.count(), - this.projectStore.count(), - this.contextFieldStore.count(), - this.groupStore.count(), - this.roleStore.count(), - this.roleStore.filteredCount({ - type: CUSTOM_ROOT_ROLE_TYPE, - }), - this.roleStore.filteredCountInUse({ type: CUSTOM_ROOT_ROLE_TYPE }), - this.environmentStore.count(), - this.segmentStore.count(), - this.strategyStore.count(), - this.hasSAML(), - this.hasOIDC(), - this.eventStore.deprecatedFilteredCount({ - type: FEATURES_EXPORTED, - }), - this.eventStore.deprecatedFilteredCount({ - type: FEATURES_IMPORTED, - }), - this.userStats(), - this.productionChanges(), - this.postgresVersion(), - ]); - const versionInfo = await this.getVersionInfo(); - const customStrategies = - await this.strategyStore.getEditableStrategies(); - const customStrategiesInUse = - await this.featureStrategiesStore.getCustomStrategiesInUseCount(); - const featureInfo = { - featureToggles, - users, - projects, - contextFields, - groups, - roles, - customRootRoles, - customRootRolesInUse, - environments, - segments, - strategies, - SAMLenabled, - OIDCenabled, - featureExports, - featureImports, - customStrategies: customStrategies.length, - customStrategiesInUse: customStrategiesInUse, - instanceId: versionInfo.instanceId, - versionOSS: versionInfo.current.oss, - versionEnterprise: versionInfo.current.enterprise, - activeUsers30: userActive.last30, - activeUsers60: userActive.last60, - activeUsers90: userActive.last90, - productionChanges30: productionChanges.last30, - productionChanges60: productionChanges.last60, - productionChanges90: productionChanges.last90, - postgresVersion, - }; - return featureInfo; - } - - async userStats(): Promise<{ - last30: number; - last60: number; - last90: number; - }> { - const { last30, last60, last90 } = await this.getActiveUsers(); - return { last30, last60, last90 }; - } - - async productionChanges(): Promise<{ - last30: number; - last60: number; - last90: number; - }> { - return this.getProductionChanges(); - } - - async postgresVersion(): Promise { - return this.settingStore.postgresVersion(); - } - - async hasOIDC(): Promise { - const settings = await this.settingStore.get<{ enabled: boolean }>( - 'unleash.enterprise.auth.oidc', - ); - - return settings?.enabled || false; - } - - async hasSAML(): Promise { - const settings = await this.settingStore.get<{ enabled: boolean }>( - 'unleash.enterprise.auth.saml', - ); - - return settings?.enabled || false; - } - async getVersionInfo(): Promise { const instanceId = await this.getInstanceId(); return {