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 | 23 +- src/lib/services/version-service.test.ts | 297 +++++++----------- src/lib/services/version-service.ts | 219 +------------ 8 files changed, 288 insertions(+), 470 deletions(-) diff --git a/src/lib/features/instance-stats/createInstanceStatsService.ts b/src/lib/features/instance-stats/createInstanceStatsService.ts index bf673dd5f3e5..f1718c149166 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, - trafficDataUsageStore, - }; - const featureStrategiesStore = new FeatureStrategyStore( - db, - eventBus, - getLogger, - flagResolver, - ); - const versionServiceStores = { - ...instanceStatsServiceStores, featureStrategiesStore, + trafficDataUsageStore, }; + 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, - trafficDataUsageStore, - }; - const featureStrategiesStore = new FakeFeatureStrategiesStore(); - const versionServiceStores = { - ...instanceStatsServiceStores, featureStrategiesStore, + trafficDataUsageStore, }; + + 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 43fffce9eca1..fc878cb65327 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 dc7f5223dc1b..2349ce9281fd 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 69d3a218fc1e..ad5a93bbe7ac 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 4206024281be..d72b6cc61f2f 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 3015a22f76c5..f5a921858c54 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 57371ba279b8..354005a62386 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 4e2648e638e3..798e9406a23d 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 {