1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

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](847549234c)
and it did:
https://github.com/Unleash/unleash/actions/runs/11861854341/job/33060032638?pr=8776#step:9:294

Finally, cleaning up version service
This commit is contained in:
Gastón Fournier 2024-11-18 10:47:29 +01:00 committed by GitHub
parent 39d227c33b
commit f297d861ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 287 additions and 469 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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<IFeatureUsageInfo> {
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<number> {
return this.memorize('deprecatedFilteredCountFeaturesExported', () =>
this.eventStore.deprecatedFilteredCount({
type: FEATURES_EXPORTED,
}),
);
}
featuresImported(): Promise<number> {
return this.memorize('deprecatedFilteredCountFeaturesImported', () =>
this.eventStore.deprecatedFilteredCount({
type: FEATURES_IMPORTED,
}),
);
}
customStrategiesCount(): Promise<number> {
return this.memorize(
'customStrategiesCount',
async () =>
(await this.strategyStore.getEditableStrategies()).length,
);
}
customStrategiesInUseCount(): Promise<number> {
return this.memorize(
'customStrategiesInUseCount',
async () =>
await this.featureStrategiesStore.getCustomStrategiesInUseCount(),
);
}
postgresVersion(): Promise<string> {
return this.memorize('postgresVersion', () =>
this.settingStore.postgresVersion(),
);
}
groupCount(): Promise<number> {
return this.memorize('groupCount', () => this.groupStore.count());
}

View File

@ -126,7 +126,10 @@ export const scheduleServices = async (
);
schedulerService.schedule(
versionService.checkLatestVersion.bind(versionService),
() =>
versionService.checkLatestVersion(() =>
instanceStatsService.getFeatureUsageInfo(),
),
hoursToMilliseconds(48),
'checkLatestVersion',
);

View File

@ -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(

View File

@ -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

View File

@ -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();
});

View File

@ -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<IUnleashStores, 'settingStore'>,
{
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<void> {
async checkLatestVersion(
telemetryDataProvider: () => Promise<IFeatureUsageInfo>,
): Promise<void> {
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<IFeatureUsageInfo> {
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<string> {
return this.settingStore.postgresVersion();
}
async hasOIDC(): Promise<boolean> {
const settings = await this.settingStore.get<{ enabled: boolean }>(
'unleash.enterprise.auth.oidc',
);
return settings?.enabled || false;
}
async hasSAML(): Promise<boolean> {
const settings = await this.settingStore.get<{ enabled: boolean }>(
'unleash.enterprise.auth.saml',
);
return settings?.enabled || false;
}
async getVersionInfo(): Promise<IVersionHolder> {
const instanceId = await this.getInstanceId();
return {