1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: applicaton usage endpoint (#4548)

This commit is contained in:
Jaanus Sellin 2023-08-23 12:00:22 +03:00 committed by GitHub
parent 0e162362e6
commit 1fbd8b6ef8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 168 additions and 17 deletions

View File

@ -7,6 +7,7 @@ import {
import { Logger, LogProvider } from '../logger';
import { IApplicationQuery } from '../types/query';
import { Db } from './db';
import { IFlagResolver } from '../types';
const COLUMNS = [
'app_name',
@ -35,8 +36,45 @@ const mapRow: (any) => IClientApplication = (row) => ({
icon: row.icon,
lastSeen: row.last_seen,
announced: row.announced,
project: row.project,
environment: row.environment,
});
const reduceRows = (rows: any[]): IClientApplication[] => {
const appsObj = rows.reduce((acc, row) => {
// extracting project and environment from usage table
const { project, environment } = row;
const existingApp = acc[row.app_name];
if (existingApp) {
const existingProject = existingApp.usage.find(
(usage) => usage.project === project,
);
if (existingProject) {
existingProject.environments.push(environment);
} else {
existingApp.usage.push({
project: project,
environments: [environment],
});
}
} else {
acc[row.app_name] = {
...mapRow(row),
usage:
project && environment
? [{ project, environments: [environment] }]
: [],
};
}
return acc;
}, {});
return Object.values(appsObj);
};
const remapRow = (input) => {
const temp = {
app_name: input.appName,
@ -72,10 +110,18 @@ export default class ClientApplicationsStore
{
private db: Db;
private flagResolver: IFlagResolver;
private logger: Logger;
constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) {
constructor(
db: Db,
eventBus: EventEmitter,
getLogger: LogProvider,
flagResolver: IFlagResolver,
) {
this.db = db;
this.flagResolver = flagResolver;
this.logger = getLogger('client-applications-store.ts');
}
@ -147,15 +193,38 @@ export default class ClientApplicationsStore
async getAppsForStrategy(
query: IApplicationQuery,
): Promise<IClientApplication[]> {
const rows = await this.db.select(COLUMNS).from(TABLE);
const apps = rows.map(mapRow);
if (this.flagResolver.isEnabled('newApplicationList')) {
const rows = await this.db
.select([
...COLUMNS.map((column) => `${TABLE}.${column}`),
'project',
'environment',
])
.from(TABLE)
.leftJoin(
TABLE_USAGE,
`${TABLE_USAGE}.app_name`,
`${TABLE}.app_name`,
);
const apps = reduceRows(rows);
if (query.strategyName) {
return apps.filter((app) =>
app.strategies.includes(query.strategyName),
);
if (query.strategyName) {
return apps.filter((app) =>
app.strategies.includes(query.strategyName),
);
}
return apps;
} else {
const rows = await this.db.select(COLUMNS).from(TABLE);
const apps = rows.map(mapRow);
if (query.strategyName) {
return apps.filter((app) =>
app.strategies.includes(query.strategyName),
);
}
return apps;
}
return apps;
}
async getUnannounced(): Promise<IClientApplication[]> {

View File

@ -54,6 +54,7 @@ export const createStores = (
db,
eventBus,
getLogger,
config.flagResolver,
),
clientInstanceStore: new ClientInstanceStore(db, eventBus, getLogger),
clientMetricsStoreV2: new ClientMetricsStoreV2(

View File

@ -12,6 +12,7 @@ import {
apiTokenSchema,
apiTokensSchema,
applicationSchema,
applicationUsageSchema,
applicationsSchema,
batchFeaturesSchema,
bulkToggleFeaturesSchema,
@ -215,6 +216,7 @@ export const schemas: UnleashSchemas = {
apiTokenSchema,
apiTokensSchema,
applicationSchema,
applicationUsageSchema,
applicationsSchema,
batchFeaturesSchema,
batchStaleSchema,

View File

@ -1,4 +1,5 @@
import { FromSchema } from 'json-schema-to-ts';
import { applicationUsageSchema } from './application-usage-schema';
export const applicationSchema = {
$id: '#/components/schemas/applicationSchema',
@ -50,8 +51,17 @@ export const applicationSchema = {
type: 'string',
example: 'https://github.com/favicon.ico',
},
usage: {
description: 'The list of projects the application has been using.',
type: 'array',
items: {
$ref: '#/components/schemas/applicationUsageSchema',
},
},
},
components: {
applicationUsageSchema,
},
components: {},
} as const;
export type ApplicationSchema = FromSchema<typeof applicationSchema>;

View File

@ -0,0 +1,28 @@
import { FromSchema } from 'json-schema-to-ts';
export const applicationUsageSchema = {
$id: '#/components/schemas/applicationUsageSchema',
type: 'object',
description: 'Data about an project that have been used by applications.',
additionalProperties: false,
required: ['project', 'environments'],
properties: {
project: {
description: 'Name of the project',
type: 'string',
example: 'main-project',
},
environments: {
description:
'Which environments have been accessed in this project.',
type: 'array',
items: {
type: 'string',
},
example: ['development', 'production'],
},
},
components: {},
} as const;
export type ApplicationUsageSchema = FromSchema<typeof applicationUsageSchema>;

View File

@ -1,5 +1,6 @@
import { applicationSchema } from './application-schema';
import { FromSchema } from 'json-schema-to-ts';
import { applicationUsageSchema } from './application-usage-schema';
export const applicationsSchema = {
$id: '#/components/schemas/applicationsSchema',
@ -20,6 +21,7 @@ export const applicationsSchema = {
components: {
schemas: {
applicationSchema,
applicationUsageSchema,
},
},
} as const;

View File

@ -153,3 +153,4 @@ export * from './strategy-variant-schema';
export * from './client-segment-schema';
export * from './update-feature-type-lifetime-schema';
export * from './create-group-schema';
export * from './application-usage-schema';

View File

@ -1,4 +1,3 @@
import { applicationSchema } from './schema';
import { APPLICATION_CREATED, CLIENT_REGISTER } from '../../types/events';
import { IApplication } from './models';
import { IUnleashStores } from '../../types/stores';
@ -205,8 +204,7 @@ export default class ClientInstanceService {
}
async createApplication(input: IApplication): Promise<void> {
const applicationData = await applicationSchema.validateAsync(input);
await this.clientApplicationsStore.upsert(applicationData);
await this.clientApplicationsStore.upsert(input);
}
async removeInstancesOlderThanTwoDays(): Promise<void> {

View File

@ -25,5 +25,7 @@ export interface IApplication {
createdAt?: Date;
instances?: IClientInstance[];
seenToggles?: Record<string, any>;
project?: string;
environment?: string;
links?: Record<string, string>;
}

View File

@ -1,6 +1,10 @@
import { Store } from './store';
import { IApplicationQuery } from '../query';
export interface IClientApplicationUsage {
project: string;
environments: string[];
}
export interface IClientApplication {
appName: string;
updatedAt: Date;
@ -13,6 +17,7 @@ export interface IClientApplication {
color: string;
icon: string;
strategies: string[];
usage?: IClientApplicationUsage[];
}
export interface IClientApplicationsStore

View File

@ -1,13 +1,24 @@
import dbInit, { ITestDb } from '../../helpers/database-init';
import { IUnleashTest, setupApp } from '../../helpers/test-helper';
import {
IUnleashTest,
setupAppWithCustomConfig,
} from '../../helpers/test-helper';
import getLogger from '../../../fixtures/no-logger';
let app: IUnleashTest;
let db: ITestDb;
beforeAll(async () => {
db = await dbInit('metrics_serial', getLogger);
app = await setupApp(db.stores);
db = await dbInit('metrics_serial', getLogger, {
experimental: { flags: { newApplicationList: true } },
});
app = await setupAppWithCustomConfig(db.stores, {
experimental: {
flags: {
newApplicationList: true,
},
},
});
});
beforeEach(async () => {
@ -44,6 +55,14 @@ beforeEach(async () => {
appName: 'deletable-app',
instanceId: 'inst-1',
});
await app.services.clientInstanceService.createApplication({
appName: 'usage-app',
strategies: ['default'],
description: 'Some desc',
project: 'default',
environment: 'dev',
});
});
afterAll(async () => {
@ -74,7 +93,7 @@ test('should get list of applications', async () => {
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
expect(res.body.applications).toHaveLength(3);
expect(res.body.applications).toHaveLength(4);
});
});
@ -89,7 +108,7 @@ test('should delete application', async () => {
.get('/api/admin/metrics/applications')
.expect('Content-Type', /json/)
.expect((res) => {
expect(res.body.applications).toHaveLength(2);
expect(res.body.applications).toHaveLength(3);
});
});
@ -101,3 +120,17 @@ test('deleting an application should be idempotent, so expect 200', async () =>
expect(res.status).toBe(200);
});
});
test('should get list of application usage', async () => {
const { body } = await app.request
.get('/api/admin/metrics/applications')
.expect('Content-Type', /json/)
.expect(200);
const application = body.applications.find(
(selectableApp) => selectableApp.appName === 'usage-app',
);
expect(application).toMatchObject({
appName: 'usage-app',
usage: [{ project: 'default', environments: ['dev'] }],
});
});