diff --git a/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx b/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx index a70d00ea1f..ba028b891b 100644 --- a/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx +++ b/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx @@ -63,6 +63,8 @@ export const InstanceStats: FC = () => { title: 'Highest number of values used for a single constraint', value: stats?.maxConstraintValues, }, + { title: 'Release templates', value: stats?.releaseTemplates }, + { title: 'Release plans', value: stats?.releasePlans }, ]; if (stats?.versionEnterprise) { diff --git a/frontend/src/openapi/models/instanceAdminStatsSchema.ts b/frontend/src/openapi/models/instanceAdminStatsSchema.ts index f3f4e0387a..d19b3028b2 100644 --- a/frontend/src/openapi/models/instanceAdminStatsSchema.ts +++ b/frontend/src/openapi/models/instanceAdminStatsSchema.ts @@ -82,6 +82,16 @@ export interface InstanceAdminStatsSchema { * @minimum 0 */ projects?: number; + /** + * The number of release plans in this instance + * @minimum 0 + */ + releasePlans?: number; + /** + * The number of release templates in this instance + * @minimum 0 + */ + releaseTemplates?: number; /** * The number of roles defined in this instance * @minimum 0 diff --git a/src/lib/features/instance-stats/createInstanceStatsService.ts b/src/lib/features/instance-stats/createInstanceStatsService.ts index 79d5776d37..048fb1a45a 100644 --- a/src/lib/features/instance-stats/createInstanceStatsService.ts +++ b/src/lib/features/instance-stats/createInstanceStatsService.ts @@ -48,6 +48,8 @@ import { createFakeGetLicensedUsers, createGetLicensedUsers, } from './getLicensedUsers.js'; +import { ReleasePlanStore } from '../release-plans/release-plan-store.js'; +import { ReleasePlanTemplateStore } from '../release-plans/release-plan-template-store.js'; export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => { const { eventBus, getLogger, flagResolver } = config; @@ -104,6 +106,9 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => { getLogger, flagResolver, ); + + const releasePlanTemplateStore = new ReleasePlanTemplateStore(db, config); + const releasePlanStore = new ReleasePlanStore(db, config); const instanceStatsServiceStores = { featureToggleStore, userStore, @@ -122,6 +127,8 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => { featureStrategiesReadModel, featureStrategiesStore, trafficDataUsageStore, + releasePlanTemplateStore, + releasePlanStore, }; const versionServiceStores = { settingStore }; const getActiveUsers = createGetActiveUsers(db); @@ -160,6 +167,12 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => { const featureStrategiesReadModel = new FakeFeatureStrategiesReadModel(); const trafficDataUsageStore = new FakeTrafficDataUsageStore(); const featureStrategiesStore = new FakeFeatureStrategiesStore(); + const releasePlanTemplateStore = { + count: () => Promise.resolve(0), + } as ReleasePlanTemplateStore; + const releasePlanStore = { + count: () => Promise.resolve(0), + } as ReleasePlanStore; const instanceStatsServiceStores = { featureToggleStore, userStore, @@ -178,6 +191,8 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => { featureStrategiesReadModel, featureStrategiesStore, trafficDataUsageStore, + releasePlanTemplateStore, + releasePlanStore, }; const versionServiceStores = { settingStore }; diff --git a/src/lib/features/instance-stats/instance-stats-service.ts b/src/lib/features/instance-stats/instance-stats-service.ts index 7632381300..3df4592b64 100644 --- a/src/lib/features/instance-stats/instance-stats-service.ts +++ b/src/lib/features/instance-stats/instance-stats-service.ts @@ -31,6 +31,8 @@ import { format, minutesToMilliseconds } from 'date-fns'; import memoizee from 'memoizee'; import type { GetLicensedUsers } from './getLicensedUsers.js'; import type { IFeatureUsageInfo } from '../../services/version-service.js'; +import type { ReleasePlanTemplateStore } from '../release-plans/release-plan-template-store.js'; +import type { ReleasePlanStore } from '../release-plans/release-plan-store.js'; export type TimeRange = 'allTime' | '30d' | '7d'; @@ -70,6 +72,8 @@ export interface InstanceStats { maxEnvironmentStrategies: number; maxConstraints: number; maxConstraintValues: number; + releaseTemplates?: number; + releasePlans?: number; } export type InstanceStatsSigned = Omit & { @@ -126,6 +130,10 @@ export class InstanceStatsService { private trafficDataUsageStore: ITrafficDataUsageStore; + private releasePlanTemplateStore: ReleasePlanTemplateStore; + + private releasePlanStore: ReleasePlanStore; + constructor( { featureToggleStore, @@ -145,6 +153,8 @@ export class InstanceStatsService { featureStrategiesReadModel, featureStrategiesStore, trafficDataUsageStore, + releasePlanTemplateStore, + releasePlanStore, }: Pick< IUnleashStores, | 'featureToggleStore' @@ -164,6 +174,8 @@ export class InstanceStatsService { | 'featureStrategiesReadModel' | 'featureStrategiesStore' | 'trafficDataUsageStore' + | 'releasePlanTemplateStore' + | 'releasePlanStore' >, { getLogger, @@ -203,6 +215,8 @@ export class InstanceStatsService { this.featureStrategiesReadModel = featureStrategiesReadModel; this.featureStrategiesStore = featureStrategiesStore; this.trafficDataUsageStore = trafficDataUsageStore; + this.releasePlanTemplateStore = releasePlanTemplateStore; + this.releasePlanStore = releasePlanStore; } memory = new Map Promise>(); @@ -295,6 +309,20 @@ export class InstanceStatsService { }); } + async getReleaseTemplates(): Promise { + return this.memorize('getReleaseTemplates', async () => { + const count = await this.releasePlanTemplateStore.count(); + return count; + }); + } + + async getReleasePlans(): Promise { + return this.memorize('getReleasePlans', async () => { + const count = await this.releasePlanStore.count(); + return count; + }); + } + async getStats(): Promise { const versionInfo = await this.versionService.getVersionInfo(); const [ @@ -326,6 +354,8 @@ export class InstanceStatsService { maxEnvironmentStrategies, maxConstraintValues, maxConstraints, + releaseTemplates, + releasePlans, ] = await Promise.all([ this.getToggleCount(), this.getArchivedToggleCount(), @@ -370,6 +400,8 @@ export class InstanceStatsService { this.featureStrategiesReadModel, ), ), + this.getReleaseTemplates(), + this.getReleasePlans(), ]); return { @@ -408,6 +440,8 @@ export class InstanceStatsService { maxEnvironmentStrategies: maxEnvironmentStrategies?.count ?? 0, maxConstraintValues: maxConstraintValues?.count ?? 0, maxConstraints: maxConstraints?.count ?? 0, + releaseTemplates, + releasePlans, }; } @@ -435,6 +469,8 @@ export class InstanceStatsService { postgresVersion, licenseType, hostedBy, + releaseTemplates, + releasePlans, ] = await Promise.all([ this.getToggleCount(), this.getRegisteredUsers(), @@ -458,6 +494,8 @@ export class InstanceStatsService { this.postgresVersion(), this.getLicenseType(), this.getHostedBy(), + this.getReleaseTemplates(), + this.getReleasePlans(), ]); const versionInfo = await this.versionService.getVersionInfo(); @@ -493,6 +531,8 @@ export class InstanceStatsService { postgresVersion, licenseType, hostedBy, + releaseTemplates, + releasePlans, }; return featureInfo; } @@ -663,7 +703,7 @@ export class InstanceStatsService { .reduce((a, b) => a + b, 0); const sum = sha256( - `${instanceStats.instanceId}${instanceStats.users}${instanceStats.featureToggles}${totalProjects}${instanceStats.roles}${instanceStats.groups}${instanceStats.environments}${instanceStats.segments}`, + `${instanceStats.instanceId}${instanceStats.users}${instanceStats.featureToggles}${totalProjects}${instanceStats.roles}${instanceStats.groups}${instanceStats.environments}${instanceStats.segments}${instanceStats.releaseTemplates}${instanceStats.releasePlans}`, ); return { ...instanceStats, sum, projects: totalProjects }; } diff --git a/src/lib/openapi/spec/instance-admin-stats-schema.ts b/src/lib/openapi/spec/instance-admin-stats-schema.ts index b3d7e340ca..80728fc1e5 100644 --- a/src/lib/openapi/spec/instance-admin-stats-schema.ts +++ b/src/lib/openapi/spec/instance-admin-stats-schema.ts @@ -272,6 +272,18 @@ export const instanceAdminStatsSchema = { description: 'The highest number of constraint values used on a single constraint.', }, + releaseTemplates: { + type: 'integer', + minimum: 0, + example: 2, + description: 'The number of release templates in this instance', + }, + releasePlans: { + type: 'integer', + minimum: 0, + example: 1, + description: 'The number of release plans in this instance', + }, sum: { type: 'string', description: diff --git a/src/lib/routes/admin-api/instance-admin.ts b/src/lib/routes/admin-api/instance-admin.ts index 101cfa71cb..f3d48aa84e 100644 --- a/src/lib/routes/admin-api/instance-admin.ts +++ b/src/lib/routes/admin-api/instance-admin.ts @@ -131,6 +131,8 @@ class InstanceAdminController extends Controller { maxEnvironmentStrategies: 20, maxConstraints: 17, maxConstraintValues: 123, + releaseTemplates: 3, + releasePlans: 5, }; } diff --git a/src/lib/services/version-service.test.ts b/src/lib/services/version-service.test.ts index f492b162bd..376ea45e4a 100644 --- a/src/lib/services/version-service.test.ts +++ b/src/lib/services/version-service.test.ts @@ -43,6 +43,8 @@ const fakeTelemetryData = { postgresVersion: '17.1 (Debian 17.1-1.pgdg120+1)', licenseType: 'test', hostedBy: 'self-hosted', + releaseTemplates: 2, + releasePlans: 4, }; test('yields current versions', async () => { diff --git a/src/lib/services/version-service.ts b/src/lib/services/version-service.ts index c8db09d8d5..42af259ad6 100644 --- a/src/lib/services/version-service.ts +++ b/src/lib/services/version-service.ts @@ -52,6 +52,8 @@ export interface IFeatureUsageInfo { postgresVersion: string; licenseType: string; hostedBy: string; + releaseTemplates: number; + releasePlans: number; } export default class VersionService { diff --git a/src/test/e2e/api/admin/instance-admin.e2e.test.ts b/src/test/e2e/api/admin/instance-admin.e2e.test.ts index 8b169db94b..16624a762f 100644 --- a/src/test/e2e/api/admin/instance-admin.e2e.test.ts +++ b/src/test/e2e/api/admin/instance-admin.e2e.test.ts @@ -124,7 +124,7 @@ test('should return signed instance statistics', async () => { .expect((res) => { expect(res.body.instanceId).toBe('test-static'); expect(res.body.sum).toBe( - 'd9bac94bba7afa20d98f0a9d54a84b79a6668f8103b8f89db85d05d38e84f519', + 'a5fd70e5ba5dfa02644404d4d075cb7f783487f607fbc00e8e4bc0aef41fd81a', ); }); }); diff --git a/src/test/fixtures/store.ts b/src/test/fixtures/store.ts index 40c9bbb7da..e4406e85ca 100644 --- a/src/test/fixtures/store.ts +++ b/src/test/fixtures/store.ts @@ -137,9 +137,15 @@ const createStores: () => IUnleashStores = () => { uniqueConnectionReadModel: new UniqueConnectionReadModel( uniqueConnectionStore, ), - releasePlanStore: {} as ReleasePlanStore, - releasePlanMilestoneStore: {} as ReleasePlanMilestoneStore, - releasePlanTemplateStore: {} as ReleasePlanTemplateStore, + releasePlanStore: { + count: () => Promise.resolve(0), + } as ReleasePlanStore, + releasePlanMilestoneStore: { + count: () => Promise.resolve(0), + } as ReleasePlanMilestoneStore, + releasePlanTemplateStore: { + count: () => Promise.resolve(0), + } as ReleasePlanTemplateStore, releasePlanMilestoneStrategyStore: {} as ReleasePlanMilestoneStrategyStore, featureLinkStore: new FakeFeatureLinkStore(),