mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-23 00:22:19 +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