Skip to content

Commit

Permalink
feat: introduce new term licensed users (#8737)
Browse files Browse the repository at this point in the history
Introducing new term Licensed users.
Added query to read it from database and extensive tests to cover the
logic.
  • Loading branch information
sjaanus authored Nov 13, 2024
1 parent bc7511a commit 940182a
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/lib/features/instance-stats/createInstanceStatsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -141,6 +146,7 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
versionService,
getActiveUsers,
getProductionChanges,
getLicencedUsers,
);

return instanceStatsService;
Expand Down Expand Up @@ -189,6 +195,7 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => {
featureStrategiesStore,
};
const getActiveUsers = createFakeGetActiveUsers();
const getLicensedUsers = createFakeGetLicensedUsers();
const getProductionChanges = createFakeGetProductionChanges();
const versionService = new VersionService(
versionServiceStores,
Expand All @@ -203,6 +210,7 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => {
versionService,
getActiveUsers,
getProductionChanges,
getLicensedUsers,
);

return instanceStatsService;
Expand Down
63 changes: 63 additions & 0 deletions src/lib/features/instance-stats/getLicensedUsers.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -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}[email protected]`,
email_hash: `${uniqueId}[email protected]`,
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 changes: 28 additions & 0 deletions src/lib/features/instance-stats/getLicensedUsers.ts
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,6 +32,7 @@ beforeEach(() => {
versionService,
createFakeGetActiveUsers(),
createFakeGetProductionChanges(),
createFakeGetLicensedUsers(),
);

const { collectAggDbMetrics } = registerPrometheusMetrics(
Expand Down
9 changes: 9 additions & 0 deletions src/lib/features/instance-stats/instance-stats-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -113,6 +115,8 @@ export class InstanceStatsService {

getActiveUsers: GetActiveUsers;

getLicencedUsers: GetLicensedUsers;

getProductionChanges: GetProductionChanges;

private featureStrategiesReadModel: IFeatureStrategiesReadModel;
Expand Down Expand Up @@ -163,6 +167,7 @@ export class InstanceStatsService {
versionService: VersionService,
getActiveUsers: GetActiveUsers,
getProductionChanges: GetProductionChanges,
getLicencedUsers: GetLicensedUsers,
) {
this.strategyStore = strategyStore;
this.userStore = userStore;
Expand All @@ -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;
Expand Down Expand Up @@ -247,6 +253,7 @@ export class InstanceStatsService {
serviceAccounts,
apiTokens,
activeUsers,
licensedUsers,
projects,
contextFields,
groups,
Expand Down Expand Up @@ -275,6 +282,7 @@ export class InstanceStatsService {
this.countServiceAccounts(),
this.countApiTokensByType(),
this.getActiveUsers(),
this.getLicencedUsers(),
this.getProjectModeCount(),
this.contextFieldCount(),
this.groupCount(),
Expand Down Expand Up @@ -311,6 +319,7 @@ export class InstanceStatsService {
serviceAccounts,
apiTokens,
activeUsers,
licensedUsers,
featureToggles,
archivedFeatureToggles,
projects,
Expand Down
2 changes: 2 additions & 0 deletions src/lib/metrics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -84,6 +85,7 @@ beforeAll(async () => {
versionService,
createFakeGetActiveUsers(),
createFakeGetProductionChanges(),
createFakeGetLicensedUsers(),
);

schedulerService = new SchedulerService(
Expand Down
7 changes: 7 additions & 0 deletions src/lib/openapi/spec/instance-admin-stats-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/lib/routes/admin-api/instance-admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit 940182a

Please sign in to comment.