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:
parent
0e162362e6
commit
1fbd8b6ef8
@ -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[]> {
|
||||
|
@ -54,6 +54,7 @@ export const createStores = (
|
||||
db,
|
||||
eventBus,
|
||||
getLogger,
|
||||
config.flagResolver,
|
||||
),
|
||||
clientInstanceStore: new ClientInstanceStore(db, eventBus, getLogger),
|
||||
clientMetricsStoreV2: new ClientMetricsStoreV2(
|
||||
|
@ -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,
|
||||
|
@ -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>;
|
||||
|
28
src/lib/openapi/spec/application-usage-schema.ts
Normal file
28
src/lib/openapi/spec/application-usage-schema.ts
Normal 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>;
|
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -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> {
|
||||
|
@ -25,5 +25,7 @@ export interface IApplication {
|
||||
createdAt?: Date;
|
||||
instances?: IClientInstance[];
|
||||
seenToggles?: Record<string, any>;
|
||||
project?: string;
|
||||
environment?: string;
|
||||
links?: Record<string, string>;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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'] }],
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user