1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-04 00:18:40 +01:00

feat: outdated sdks api (#6539)

This commit is contained in:
Mateusz Kwasniewski 2024-03-13 15:56:22 +01:00 committed by GitHub
parent 3c22a302c7
commit 9438400e77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 160 additions and 23 deletions

View File

@ -205,6 +205,20 @@ export default class ClientInstanceStore implements IClientInstanceStore {
return rows.map(mapRow); return rows.map(mapRow);
} }
async groupApplicationsBySdk(): Promise<
{ sdkVersion: string; applications: string[] }[]
> {
const rows = await this.db
.select([
'sdk_version as sdkVersion',
this.db.raw('ARRAY_AGG(DISTINCT app_name) as applications'),
])
.from(TABLE)
.groupBy('sdk_version');
return rows;
}
async getDistinctApplications(): Promise<string[]> { async getDistinctApplications(): Promise<string[]> {
const rows = await this.db const rows = await this.db
.distinct('app_name') .distinct('app_name')

View File

@ -14,10 +14,7 @@ const config: SDKConfig = {
'unleash-client-php': '1.13.0', 'unleash-client-php': '1.13.0',
}; };
export function findOutdatedSDKs(sdkVersions: string[]): string[] { export const isOutdatedSdk = (sdkVersion: string) => {
const uniqueSdkVersions = Array.from(new Set(sdkVersions));
return uniqueSdkVersions.filter((sdkVersion) => {
const result = sdkVersion.split(':'); const result = sdkVersion.split(':');
if (result.length !== 2) return false; if (result.length !== 2) return false;
const [sdkName, version] = result; const [sdkName, version] = result;
@ -25,5 +22,10 @@ export function findOutdatedSDKs(sdkVersions: string[]): string[] {
if (!minVersion) return false; if (!minVersion) return false;
if (semver.lt(version, minVersion)) return true; if (semver.lt(version, minVersion)) return true;
return false; return false;
}); };
export function findOutdatedSDKs(sdkVersions: string[]): string[] {
const uniqueSdkVersions = Array.from(new Set(sdkVersions));
return uniqueSdkVersions.filter(isOutdatedSdk);
} }

View File

@ -22,7 +22,8 @@ import { IPrivateProjectChecker } from '../../private-project/privateProjectChec
import { IFlagResolver, SYSTEM_USER } from '../../../types'; import { IFlagResolver, SYSTEM_USER } from '../../../types';
import { ALL_PROJECTS, parseStrictSemVer } from '../../../util'; import { ALL_PROJECTS, parseStrictSemVer } from '../../../util';
import { Logger } from '../../../logger'; import { Logger } from '../../../logger';
import { findOutdatedSDKs } from './findOutdatedSdks'; import { findOutdatedSDKs, isOutdatedSdk } from './findOutdatedSdks';
import { OutdatedSdksSchema } from '../../../openapi/spec/outdated-sdks-schema';
export default class ClientInstanceService { export default class ClientInstanceService {
apps = {}; apps = {};
@ -261,6 +262,12 @@ export default class ClientInstanceService {
return this.clientInstanceStore.removeInstancesOlderThanTwoDays(); return this.clientInstanceStore.removeInstancesOlderThanTwoDays();
} }
async getOutdatedSdks(): Promise<OutdatedSdksSchema['sdks']> {
const sdkApps = await this.clientInstanceStore.groupApplicationsBySdk();
return sdkApps.filter((sdkApp) => isOutdatedSdk(sdkApp.sdkVersion));
}
async usesSdkOlderThan( async usesSdkOlderThan(
sdkName: string, sdkName: string,
sdkVersion: string, sdkVersion: string,

View File

@ -112,6 +112,7 @@ export * from './login-schema';
export * from './maintenance-schema'; export * from './maintenance-schema';
export * from './me-schema'; export * from './me-schema';
export * from './name-schema'; export * from './name-schema';
export * from './outdated-sdks-schema';
export * from './override-schema'; export * from './override-schema';
export * from './parameters-schema'; export * from './parameters-schema';
export * from './parent-feature-options-schema'; export * from './parent-feature-options-schema';

View File

@ -0,0 +1,41 @@
import { FromSchema } from 'json-schema-to-ts';
export const outdatedSdksSchema = {
$id: '#/components/schemas/outdatedSdksSchema',
type: 'object',
description: 'Data about outdated SDKs that should be upgraded.',
additionalProperties: false,
required: ['sdks'],
properties: {
sdks: {
type: 'array',
description: 'A list of SDKs',
items: {
type: 'object',
required: ['sdkVersion', 'applications'],
additionalProperties: false,
properties: {
sdkVersion: {
type: 'string',
description:
'An outdated SDK version identifier. Usually formatted as "unleash-client-<language>:<version>"',
example: 'unleash-client-java:7.0.0',
},
applications: {
type: 'array',
items: {
description: 'Name of the application',
type: 'string',
example: 'accounting',
},
description:
'A list of applications using the SDK version',
},
},
},
},
},
components: {},
} as const;
export type OutdatedSdksSchema = FromSchema<typeof outdatedSdksSchema>;

View File

@ -29,6 +29,10 @@ import {
applicationEnvironmentInstancesSchema, applicationEnvironmentInstancesSchema,
ApplicationEnvironmentInstancesSchema, ApplicationEnvironmentInstancesSchema,
} from '../../openapi/spec/application-environment-instances-schema'; } from '../../openapi/spec/application-environment-instances-schema';
import {
outdatedSdksSchema,
OutdatedSdksSchema,
} from '../../openapi/spec/outdated-sdks-schema';
class MetricsController extends Controller { class MetricsController extends Controller {
private logger: Logger; private logger: Logger;
@ -177,6 +181,25 @@ class MetricsController extends Controller {
}), }),
], ],
}); });
this.route({
method: 'get',
path: '/sdks/outdated',
handler: this.getOutdatedSdks,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['Unstable'],
operationId: 'getOutdatedSdks',
summary: 'Get outdated SDKs',
description:
'Returns a list of the outdated SDKS with the applications using them.',
responses: {
200: createResponseSchema('outdatedSdksSchema'),
...getStandardResponses(404),
},
}),
],
});
} }
async deprecated(req: Request, res: Response): Promise<void> { async deprecated(req: Request, res: Response): Promise<void> {
@ -277,6 +300,20 @@ class MetricsController extends Controller {
); );
} }
async getOutdatedSdks(req: Request, res: Response<OutdatedSdksSchema>) {
if (!this.flagResolver.isEnabled('sdkReporting')) {
throw new NotFoundError();
}
const outdatedSdks = await this.clientInstanceService.getOutdatedSdks();
this.openApiService.respondWithValidation(
200,
res,
outdatedSdksSchema.$id,
{ sdks: outdatedSdks },
);
}
async getApplicationEnvironmentInstances( async getApplicationEnvironmentInstances(
req: Request<{ appName: string; environment: string }>, req: Request<{ appName: string; environment: string }>,
res: Response<ApplicationEnvironmentInstancesSchema>, res: Response<ApplicationEnvironmentInstancesSchema>,

View File

@ -26,6 +26,9 @@ export interface IClientInstanceStore
environment: string, environment: string,
): Promise<IClientInstance[]>; ): Promise<IClientInstance[]>;
getBySdkName(sdkName: string): Promise<IClientInstance[]>; getBySdkName(sdkName: string): Promise<IClientInstance[]>;
groupApplicationsBySdk(): Promise<
{ sdkVersion: string; applications: string[] }[]
>;
getDistinctApplications(): Promise<string[]>; getDistinctApplications(): Promise<string[]>;
getDistinctApplicationsCount(daysBefore?: number): Promise<number>; getDistinctApplicationsCount(daysBefore?: number): Promise<number>;
deleteForApplication(appName: string): Promise<void>; deleteForApplication(appName: string): Promise<void>;

View File

@ -85,7 +85,7 @@ afterAll(async () => {
await db.destroy(); await db.destroy();
}); });
test('should show correct number of total', async () => { test('should show correct application metrics', async () => {
await Promise.all([ await Promise.all([
app.createFeature('toggle-name-1'), app.createFeature('toggle-name-1'),
app.createFeature('toggle-name-2'), app.createFeature('toggle-name-2'),
@ -94,7 +94,7 @@ test('should show correct number of total', async () => {
appName: metrics.appName, appName: metrics.appName,
instanceId: metrics.instanceId, instanceId: metrics.instanceId,
strategies: ['default'], strategies: ['default'],
sdkVersion: 'unleash-client-test', sdkVersion: 'unleash-client-node:3.2.1',
started: Date.now(), started: Date.now(),
interval: 10, interval: 10,
}), }),
@ -102,7 +102,7 @@ test('should show correct number of total', async () => {
appName: metrics.appName, appName: metrics.appName,
instanceId: 'another-instance', instanceId: 'another-instance',
strategies: ['default'], strategies: ['default'],
sdkVersion: 'unleash-client-test2', sdkVersion: 'unleash-client-node:3.2.2',
started: Date.now(), started: Date.now(),
interval: 10, interval: 10,
}), }),
@ -129,7 +129,10 @@ test('should show correct number of total', async () => {
{ {
instanceCount: 2, instanceCount: 2,
name: 'default', name: 'default',
sdks: ['unleash-client-test', 'unleash-client-test2'], sdks: [
'unleash-client-node:3.2.1',
'unleash-client-node:3.2.2',
],
}, },
], ],
featureCount: 3, featureCount: 3,
@ -143,12 +146,31 @@ test('should show correct number of total', async () => {
) )
.expect(200); .expect(200);
expect(instancesBody).toMatchObject({ expect(
instances: [ instancesBody.instances.sort((a, b) =>
{ instanceId: 'instanceId', sdkVersion: 'unleash-client-test' }, a.instanceId.localeCompare(b.instanceId),
),
).toMatchObject([
{ {
instanceId: 'another-instance', instanceId: 'another-instance',
sdkVersion: 'unleash-client-test2', sdkVersion: 'unleash-client-node:3.2.2',
},
{ instanceId: 'instanceId', sdkVersion: 'unleash-client-node:3.2.1' },
]);
const { body: outdatedSdks } = await app.request
.get(`/api/admin/metrics/sdks/outdated`)
.expect(200);
expect(outdatedSdks).toMatchObject({
sdks: [
{
sdkVersion: 'unleash-client-node:3.2.1',
applications: ['appName'],
},
{
sdkVersion: 'unleash-client-node:3.2.2',
applications: ['appName'],
}, },
], ],
}); });

View File

@ -4,6 +4,7 @@ import {
INewClientInstance, INewClientInstance,
} from '../../lib/types/stores/client-instance-store'; } from '../../lib/types/stores/client-instance-store';
import NotFoundError from '../../lib/error/notfound-error'; import NotFoundError from '../../lib/error/notfound-error';
import groupBy from 'lodash.groupby';
export default class FakeClientInstanceStore implements IClientInstanceStore { export default class FakeClientInstanceStore implements IClientInstanceStore {
instances: IClientInstance[] = []; instances: IClientInstance[] = [];
@ -32,10 +33,19 @@ export default class FakeClientInstanceStore implements IClientInstanceStore {
} }
async getBySdkName(sdkName: string): Promise<IClientInstance[]> { async getBySdkName(sdkName: string): Promise<IClientInstance[]> {
return Promise.resolve( return this.instances.filter((instance) =>
this.instances.filter((instance) =>
instance.sdkVersion?.startsWith(sdkName), instance.sdkVersion?.startsWith(sdkName),
), );
}
async groupApplicationsBySdk(): Promise<
{ sdkVersion: string; applications: string[] }[]
> {
return Object.entries(groupBy(this.instances, 'sdkVersion')).map(
([sdkVersion, apps]) => ({
sdkVersion,
applications: apps.map((item) => item.appName),
}),
); );
} }