mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: introduce new term licensed users (#8737)
Introducing new term Licensed users. Added query to read it from database and extensive tests to cover the logic.
This commit is contained in:
		
							parent
							
								
									bc7511abd4
								
							
						
					
					
						commit
						940182aaf0
					
				| @ -44,6 +44,10 @@ import { FeatureStrategiesReadModel } from '../feature-toggle/feature-strategies | ||||
| import { FakeFeatureStrategiesReadModel } from '../feature-toggle/fake-feature-strategies-read-model'; | ||||
| import { TrafficDataUsageStore } from '../traffic-data-usage/traffic-data-usage-store'; | ||||
| import { FakeTrafficDataUsageStore } from '../traffic-data-usage/fake-traffic-data-usage-store'; | ||||
| import { | ||||
|     createFakeGetLicensedUsers, | ||||
|     createGetLicensedUsers, | ||||
| } from './getLicensedUsers'; | ||||
| 
 | ||||
| export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => { | ||||
|     const { eventBus, getLogger, flagResolver } = config; | ||||
| @ -128,6 +132,7 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => { | ||||
|     }; | ||||
|     const getActiveUsers = createGetActiveUsers(db); | ||||
|     const getProductionChanges = createGetProductionChanges(db); | ||||
|     const getLicencedUsers = createGetLicensedUsers(db); | ||||
|     const versionService = new VersionService( | ||||
|         versionServiceStores, | ||||
|         config, | ||||
| @ -141,6 +146,7 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => { | ||||
|         versionService, | ||||
|         getActiveUsers, | ||||
|         getProductionChanges, | ||||
|         getLicencedUsers, | ||||
|     ); | ||||
| 
 | ||||
|     return instanceStatsService; | ||||
| @ -189,6 +195,7 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => { | ||||
|         featureStrategiesStore, | ||||
|     }; | ||||
|     const getActiveUsers = createFakeGetActiveUsers(); | ||||
|     const getLicensedUsers = createFakeGetLicensedUsers(); | ||||
|     const getProductionChanges = createFakeGetProductionChanges(); | ||||
|     const versionService = new VersionService( | ||||
|         versionServiceStores, | ||||
| @ -203,6 +210,7 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => { | ||||
|         versionService, | ||||
|         getActiveUsers, | ||||
|         getProductionChanges, | ||||
|         getLicensedUsers, | ||||
|     ); | ||||
| 
 | ||||
|     return instanceStatsService; | ||||
|  | ||||
							
								
								
									
										63
									
								
								src/lib/features/instance-stats/getLicensedUsers.e2e.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/lib/features/instance-stats/getLicensedUsers.e2e.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | ||||
| import { | ||||
|     createGetLicensedUsers, | ||||
|     type GetLicensedUsers, | ||||
| } from './getLicensedUsers'; | ||||
| import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init'; | ||||
| import getLogger from '../../../test/fixtures/no-logger'; | ||||
| 
 | ||||
| let db: ITestDb; | ||||
| let getLicensedUsers: GetLicensedUsers; | ||||
| 
 | ||||
| const mockUser = (deletedDaysAgo: number | null, uniqueId: number) => { | ||||
|     const deletedAt = | ||||
|         deletedDaysAgo !== null | ||||
|             ? new Date(Date.now() - deletedDaysAgo * 24 * 60 * 60 * 1000) | ||||
|             : null; | ||||
|     return { | ||||
|         email: `${uniqueId}.user@example.com`, | ||||
|         email_hash: `${uniqueId}.user@example.com`, | ||||
|         deleted_at: deletedAt, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| beforeAll(async () => { | ||||
|     db = await dbInit('licensed_users_serial', getLogger); | ||||
|     getLicensedUsers = createGetLicensedUsers(db.rawDatabase); | ||||
| }); | ||||
| 
 | ||||
| afterEach(async () => { | ||||
|     await db.rawDatabase('users').delete(); | ||||
| }); | ||||
| 
 | ||||
| afterAll(async () => { | ||||
|     await db.destroy(); | ||||
| }); | ||||
| 
 | ||||
| test('should return 0 users when no users are present', async () => { | ||||
|     await expect(getLicensedUsers()).resolves.toEqual(0); | ||||
| }); | ||||
| 
 | ||||
| test('should return 1 active user with no deletion date', async () => { | ||||
|     await db.rawDatabase('users').insert(mockUser(null, 1)); | ||||
|     await expect(getLicensedUsers()).resolves.toEqual(1); | ||||
| }); | ||||
| 
 | ||||
| test('should count user as active if deleted within 30 days', async () => { | ||||
|     await db.rawDatabase('users').insert(mockUser(29, 2)); | ||||
|     await expect(getLicensedUsers()).resolves.toEqual(1); | ||||
| }); | ||||
| 
 | ||||
| test('should not count user as active if deleted more than 30 days ago', async () => { | ||||
|     await db.rawDatabase('users').insert(mockUser(31, 3)); | ||||
|     await expect(getLicensedUsers()).resolves.toEqual(0); | ||||
| }); | ||||
| 
 | ||||
| test('should return correct count for multiple users with mixed deletion statuses', async () => { | ||||
|     const users = [ | ||||
|         ...Array.from({ length: 10 }, (_, userId) => mockUser(null, userId)), // 10 active users
 | ||||
|         ...Array.from({ length: 5 }, (_, userId) => mockUser(29, userId + 10)), // 5 users deleted within 30 days
 | ||||
|         ...Array.from({ length: 3 }, (_, userId) => mockUser(31, userId + 15)), // 3 users deleted more than 30 days ago
 | ||||
|     ]; | ||||
|     await db.rawDatabase('users').insert(users); | ||||
|     await expect(getLicensedUsers()).resolves.toEqual(15); | ||||
| }); | ||||
							
								
								
									
										28
									
								
								src/lib/features/instance-stats/getLicensedUsers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/lib/features/instance-stats/getLicensedUsers.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| import type { Db } from '../../server-impl'; | ||||
| 
 | ||||
| export type GetLicensedUsers = () => Promise<number>; | ||||
| 
 | ||||
| export const createGetLicensedUsers = | ||||
|     (db: Db): GetLicensedUsers => | ||||
|     async () => { | ||||
|         const result = await db('users') | ||||
|             .countDistinct('email_hash as activeCount') | ||||
|             .whereNotNull('email_hash') | ||||
|             .andWhere(function () { | ||||
|                 this.whereNull('deleted_at').orWhere( | ||||
|                     'deleted_at', | ||||
|                     '>=', | ||||
|                     db.raw("NOW() - INTERVAL '30 days'"), | ||||
|                 ); | ||||
|             }) | ||||
|             .first(); | ||||
| 
 | ||||
|         return Number(result?.activeCount ?? 0); | ||||
|     }; | ||||
| 
 | ||||
| export const createFakeGetLicensedUsers = | ||||
|     ( | ||||
|         licencedUsers: Awaited<ReturnType<GetLicensedUsers>> = 0, | ||||
|     ): GetLicensedUsers => | ||||
|     () => | ||||
|         Promise.resolve(licencedUsers); | ||||
| @ -7,6 +7,7 @@ import { createFakeGetProductionChanges } from './getProductionChanges'; | ||||
| import { registerPrometheusMetrics } from '../../metrics'; | ||||
| import { register } from 'prom-client'; | ||||
| import type { IClientInstanceStore } from '../../types'; | ||||
| import { createFakeGetLicensedUsers } from './getLicensedUsers'; | ||||
| let instanceStatsService: InstanceStatsService; | ||||
| let versionService: VersionService; | ||||
| let clientInstanceStore: IClientInstanceStore; | ||||
| @ -31,6 +32,7 @@ beforeEach(() => { | ||||
|         versionService, | ||||
|         createFakeGetActiveUsers(), | ||||
|         createFakeGetProductionChanges(), | ||||
|         createFakeGetLicensedUsers(), | ||||
|     ); | ||||
| 
 | ||||
|     const { collectAggDbMetrics } = registerPrometheusMetrics( | ||||
|  | ||||
| @ -31,6 +31,7 @@ import type { GetActiveUsers } from './getActiveUsers'; | ||||
| import type { ProjectModeCount } from '../project/project-store'; | ||||
| import type { GetProductionChanges } from './getProductionChanges'; | ||||
| import { format } from 'date-fns'; | ||||
| import type { GetLicensedUsers } from './getLicensedUsers'; | ||||
| 
 | ||||
| export type TimeRange = 'allTime' | '30d' | '7d'; | ||||
| 
 | ||||
| @ -59,6 +60,7 @@ export interface InstanceStats { | ||||
|     OIDCenabled: boolean; | ||||
|     clientApps: { range: TimeRange; count: number }[]; | ||||
|     activeUsers: Awaited<ReturnType<GetActiveUsers>>; | ||||
|     licensedUsers: Awaited<ReturnType<GetLicensedUsers>>; | ||||
|     productionChanges: Awaited<ReturnType<GetProductionChanges>>; | ||||
|     previousDayMetricsBucketsCount: { | ||||
|         enabledCount: number; | ||||
| @ -113,6 +115,8 @@ export class InstanceStatsService { | ||||
| 
 | ||||
|     getActiveUsers: GetActiveUsers; | ||||
| 
 | ||||
|     getLicencedUsers: GetLicensedUsers; | ||||
| 
 | ||||
|     getProductionChanges: GetProductionChanges; | ||||
| 
 | ||||
|     private featureStrategiesReadModel: IFeatureStrategiesReadModel; | ||||
| @ -163,6 +167,7 @@ export class InstanceStatsService { | ||||
|         versionService: VersionService, | ||||
|         getActiveUsers: GetActiveUsers, | ||||
|         getProductionChanges: GetProductionChanges, | ||||
|         getLicencedUsers: GetLicensedUsers, | ||||
|     ) { | ||||
|         this.strategyStore = strategyStore; | ||||
|         this.userStore = userStore; | ||||
| @ -179,6 +184,7 @@ export class InstanceStatsService { | ||||
|         this.clientInstanceStore = clientInstanceStore; | ||||
|         this.logger = getLogger('services/stats-service.js'); | ||||
|         this.getActiveUsers = getActiveUsers; | ||||
|         this.getLicencedUsers = getLicencedUsers; | ||||
|         this.getProductionChanges = getProductionChanges; | ||||
|         this.apiTokenStore = apiTokenStore; | ||||
|         this.clientMetricsStore = clientMetricsStoreV2; | ||||
| @ -247,6 +253,7 @@ export class InstanceStatsService { | ||||
|             serviceAccounts, | ||||
|             apiTokens, | ||||
|             activeUsers, | ||||
|             licensedUsers, | ||||
|             projects, | ||||
|             contextFields, | ||||
|             groups, | ||||
| @ -275,6 +282,7 @@ export class InstanceStatsService { | ||||
|             this.countServiceAccounts(), | ||||
|             this.countApiTokensByType(), | ||||
|             this.getActiveUsers(), | ||||
|             this.getLicencedUsers(), | ||||
|             this.getProjectModeCount(), | ||||
|             this.contextFieldCount(), | ||||
|             this.groupCount(), | ||||
| @ -311,6 +319,7 @@ export class InstanceStatsService { | ||||
|             serviceAccounts, | ||||
|             apiTokens, | ||||
|             activeUsers, | ||||
|             licensedUsers, | ||||
|             featureToggles, | ||||
|             archivedFeatureToggles, | ||||
|             projects, | ||||
|  | ||||
| @ -38,6 +38,7 @@ import getLogger from '../test/fixtures/no-logger'; | ||||
| import dbInit, { type ITestDb } from '../test/e2e/helpers/database-init'; | ||||
| import { FeatureLifecycleStore } from './features/feature-lifecycle/feature-lifecycle-store'; | ||||
| import { FeatureLifecycleReadModel } from './features/feature-lifecycle/feature-lifecycle-read-model'; | ||||
| import { createFakeGetLicensedUsers } from './features/instance-stats/getLicensedUsers'; | ||||
| 
 | ||||
| const monitor = createMetricsMonitor(); | ||||
| const eventBus = new EventEmitter(); | ||||
| @ -84,6 +85,7 @@ beforeAll(async () => { | ||||
|         versionService, | ||||
|         createFakeGetActiveUsers(), | ||||
|         createFakeGetProductionChanges(), | ||||
|         createFakeGetLicensedUsers(), | ||||
|     ); | ||||
| 
 | ||||
|     schedulerService = new SchedulerService( | ||||
|  | ||||
| @ -95,6 +95,13 @@ export const instanceAdminStatsSchema = { | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|         licensedUsers: { | ||||
|             type: 'integer', | ||||
|             description: | ||||
|                 'The number of users who had access to Unleash within the last 30 days, including those who may have been deleted during this period.', | ||||
|             example: 10, | ||||
|             minimum: 0, | ||||
|         }, | ||||
|         productionChanges: { | ||||
|             type: 'object', | ||||
|             description: | ||||
|  | ||||
| @ -107,6 +107,7 @@ class InstanceAdminController extends Controller { | ||||
|             sum: 'some-sha256-hash', | ||||
|             timestamp: new Date(2023, 6, 12, 10, 0, 0, 0), | ||||
|             users: 10, | ||||
|             licensedUsers: 12, | ||||
|             serviceAccounts: 2, | ||||
|             apiTokens: new Map([]), | ||||
|             versionEnterprise: '5.1.7', | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user