1
0
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:
Jaanus Sellin 2024-11-13 14:32:58 +02:00 committed by GitHub
parent bc7511abd4
commit 940182aaf0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 120 additions and 0 deletions

View File

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

View 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);
});

View 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);

View File

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

View File

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

View File

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

View File

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

View File

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