Skip to content

Commit

Permalink
feat: outdated sdks api (#6539)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew committed Mar 13, 2024
1 parent 3c22a30 commit 9438400
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 23 deletions.
14 changes: 14 additions & 0 deletions src/lib/db/client-instance-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,20 @@ export default class ClientInstanceStore implements IClientInstanceStore {
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[]> {
const rows = await this.db
.distinct('app_name')
Expand Down
20 changes: 11 additions & 9 deletions src/lib/features/metrics/instance/findOutdatedSdks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ const config: SDKConfig = {
'unleash-client-php': '1.13.0',
};

export const isOutdatedSdk = (sdkVersion: string) => {
const result = sdkVersion.split(':');
if (result.length !== 2) return false;
const [sdkName, version] = result;
const minVersion = config[sdkName];
if (!minVersion) return false;
if (semver.lt(version, minVersion)) return true;
return false;
};

export function findOutdatedSDKs(sdkVersions: string[]): string[] {
const uniqueSdkVersions = Array.from(new Set(sdkVersions));

return uniqueSdkVersions.filter((sdkVersion) => {
const result = sdkVersion.split(':');
if (result.length !== 2) return false;
const [sdkName, version] = result;
const minVersion = config[sdkName];
if (!minVersion) return false;
if (semver.lt(version, minVersion)) return true;
return false;
});
return uniqueSdkVersions.filter(isOutdatedSdk);
}
9 changes: 8 additions & 1 deletion src/lib/features/metrics/instance/instance-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import { IPrivateProjectChecker } from '../../private-project/privateProjectChec
import { IFlagResolver, SYSTEM_USER } from '../../../types';
import { ALL_PROJECTS, parseStrictSemVer } from '../../../util';
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 {
apps = {};
Expand Down Expand Up @@ -261,6 +262,12 @@ export default class ClientInstanceService {
return this.clientInstanceStore.removeInstancesOlderThanTwoDays();
}

async getOutdatedSdks(): Promise<OutdatedSdksSchema['sdks']> {
const sdkApps = await this.clientInstanceStore.groupApplicationsBySdk();

return sdkApps.filter((sdkApp) => isOutdatedSdk(sdkApp.sdkVersion));
}

async usesSdkOlderThan(
sdkName: string,
sdkVersion: string,
Expand Down
1 change: 1 addition & 0 deletions src/lib/openapi/spec/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export * from './login-schema';
export * from './maintenance-schema';
export * from './me-schema';
export * from './name-schema';
export * from './outdated-sdks-schema';
export * from './override-schema';
export * from './parameters-schema';
export * from './parent-feature-options-schema';
Expand Down
41 changes: 41 additions & 0 deletions src/lib/openapi/spec/outdated-sdks-schema.ts
Original file line number Diff line number Diff line change
@@ -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>;
37 changes: 37 additions & 0 deletions src/lib/routes/admin-api/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import {
applicationEnvironmentInstancesSchema,
ApplicationEnvironmentInstancesSchema,
} from '../../openapi/spec/application-environment-instances-schema';
import {
outdatedSdksSchema,
OutdatedSdksSchema,
} from '../../openapi/spec/outdated-sdks-schema';

class MetricsController extends Controller {
private logger: Logger;
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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(
req: Request<{ appName: string; environment: string }>,
res: Response<ApplicationEnvironmentInstancesSchema>,
Expand Down
3 changes: 3 additions & 0 deletions src/lib/types/stores/client-instance-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export interface IClientInstanceStore
environment: string,
): Promise<IClientInstance[]>;
getBySdkName(sdkName: string): Promise<IClientInstance[]>;
groupApplicationsBySdk(): Promise<
{ sdkVersion: string; applications: string[] }[]
>;
getDistinctApplications(): Promise<string[]>;
getDistinctApplicationsCount(daysBefore?: number): Promise<number>;
deleteForApplication(appName: string): Promise<void>;
Expand Down
40 changes: 31 additions & 9 deletions src/test/e2e/api/admin/applications.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ afterAll(async () => {
await db.destroy();
});

test('should show correct number of total', async () => {
test('should show correct application metrics', async () => {
await Promise.all([
app.createFeature('toggle-name-1'),
app.createFeature('toggle-name-2'),
Expand All @@ -94,15 +94,15 @@ test('should show correct number of total', async () => {
appName: metrics.appName,
instanceId: metrics.instanceId,
strategies: ['default'],
sdkVersion: 'unleash-client-test',
sdkVersion: 'unleash-client-node:3.2.1',
started: Date.now(),
interval: 10,
}),
app.request.post('/api/client/register').send({
appName: metrics.appName,
instanceId: 'another-instance',
strategies: ['default'],
sdkVersion: 'unleash-client-test2',
sdkVersion: 'unleash-client-node:3.2.2',
started: Date.now(),
interval: 10,
}),
Expand All @@ -129,7 +129,10 @@ test('should show correct number of total', async () => {
{
instanceCount: 2,
name: 'default',
sdks: ['unleash-client-test', 'unleash-client-test2'],
sdks: [
'unleash-client-node:3.2.1',
'unleash-client-node:3.2.2',
],
},
],
featureCount: 3,
Expand All @@ -143,12 +146,31 @@ test('should show correct number of total', async () => {
)
.expect(200);

expect(instancesBody).toMatchObject({
instances: [
{ instanceId: 'instanceId', sdkVersion: 'unleash-client-test' },
expect(
instancesBody.instances.sort((a, b) =>
a.instanceId.localeCompare(b.instanceId),
),
).toMatchObject([
{
instanceId: 'another-instance',
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'],
},
{
instanceId: 'another-instance',
sdkVersion: 'unleash-client-test2',
sdkVersion: 'unleash-client-node:3.2.2',
applications: ['appName'],
},
],
});
Expand Down
18 changes: 14 additions & 4 deletions src/test/fixtures/fake-client-instance-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
INewClientInstance,
} from '../../lib/types/stores/client-instance-store';
import NotFoundError from '../../lib/error/notfound-error';
import groupBy from 'lodash.groupby';

export default class FakeClientInstanceStore implements IClientInstanceStore {
instances: IClientInstance[] = [];
Expand Down Expand Up @@ -32,10 +33,19 @@ export default class FakeClientInstanceStore implements IClientInstanceStore {
}

async getBySdkName(sdkName: string): Promise<IClientInstance[]> {
return Promise.resolve(
this.instances.filter((instance) =>
instance.sdkVersion?.startsWith(sdkName),
),
return this.instances.filter((instance) =>
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),
}),
);
}

Expand Down

0 comments on commit 9438400

Please sign in to comment.