Skip to content

Commit

Permalink
fix: serialize API token data correctly in instance stats (#7953)
Browse files Browse the repository at this point in the history
Turns out we've been trying to return API token data in instance stats
for a while, but that the serialization has failed. Serializing a JS map
just yields an empty object.

This PR fixes that serialization and also adds API tokens to the
instance stats schema (it wasn't before, but we did return it). Adding
it to the schema is also part of making resource usage visible as part
of the soft limits project.
  • Loading branch information
thomasheartman authored Aug 22, 2024
1 parent 3417039 commit e5cca66
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 8 deletions.
24 changes: 24 additions & 0 deletions src/lib/openapi/spec/instance-admin-stats-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,30 @@ export const instanceAdminStatsSchema = {
example: 0,
minimum: 0,
},
apiTokens: {
type: 'object',
description: 'The number of API tokens in Unleash, split by type',
properties: {
admin: {
type: 'number',
description: 'The number of admin tokens.',
minimum: 0,
example: 5,
},
client: {
type: 'number',
description: 'The number of client tokens.',
minimum: 0,
example: 5,
},
frontend: {
type: 'number',
description: 'The number of frontend tokens.',
minimum: 0,
example: 5,
},
},
},
sum: {
type: 'string',
description:
Expand Down
27 changes: 20 additions & 7 deletions src/lib/routes/admin-api/instance-admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import type { IUnleashServices } from '../../types/services';
import type { IUnleashConfig } from '../../types/option';
import Controller from '../controller';
import { NONE } from '../../types/permissions';
import type { UiConfigSchema } from '../../openapi/spec/ui-config-schema';
import type {
InstanceStatsService,
InstanceStatsSigned,
Expand All @@ -15,6 +14,8 @@ import {
createCsvResponseSchema,
createResponseSchema,
} from '../../openapi/util/create-response-schema';
import type { InstanceAdminStatsSchema } from '../../openapi';
import { serializeDates } from '../../types';

class InstanceAdminController extends Controller {
private instanceStatsService: InstanceStatsService;
Expand Down Expand Up @@ -128,25 +129,37 @@ class InstanceAdminController extends Controller {
};
}

private serializeStats(
instanceStats: InstanceStatsSigned,
): InstanceAdminStatsSchema {
const apiTokensObj = Object.fromEntries(
instanceStats.apiTokens.entries(),
);
return serializeDates({
...instanceStats,
apiTokens: apiTokensObj,
});
}

async getStatistics(
req: AuthedRequest,
res: Response<InstanceStatsSigned>,
_: AuthedRequest,
res: Response<InstanceAdminStatsSchema>,
): Promise<void> {
const instanceStats = await this.instanceStatsService.getSignedStats();
res.json(instanceStats);
res.json(this.serializeStats(instanceStats));
}

async getStatisticsCSV(
req: AuthedRequest,
res: Response<UiConfigSchema>,
_: AuthedRequest,
res: Response<InstanceAdminStatsSchema>,
): Promise<void> {
const instanceStats = await this.instanceStatsService.getSignedStats();
const fileName = `unleash-${
instanceStats.instanceId
}-${Date.now()}.csv`;

const json2csvParser = new Parser();
const csv = json2csvParser.parse(instanceStats);
const csv = json2csvParser.parse(this.serializeStats(instanceStats));

res.contentType('csv');
res.attachment(fileName);
Expand Down
40 changes: 39 additions & 1 deletion src/test/e2e/api/admin/instance-admin.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from '../../helpers/test-helper';
import getLogger from '../../../fixtures/no-logger';
import type { IUnleashStores } from '../../../../lib/types';
import { ApiTokenType } from '../../../../lib/types/models/api-token';

let app: IUnleashTest;
let db: ITestDb;
Expand Down Expand Up @@ -47,6 +48,43 @@ test('should return instance statistics', async () => {
});
});

test('api tokens are serialized correctly', async () => {
await app.services.apiTokenService.createApiTokenWithProjects({
tokenName: 'admin',
type: ApiTokenType.ADMIN,
environment: '*',
projects: ['*'],
});
await app.services.apiTokenService.createApiTokenWithProjects({
tokenName: 'frontend',
type: ApiTokenType.FRONTEND,
environment: 'default',
projects: ['*'],
});
await app.services.apiTokenService.createApiTokenWithProjects({
tokenName: 'client',
type: ApiTokenType.CLIENT,
environment: 'default',
projects: ['*'],
});

const { body } = await app.request
.get('/api/admin/instance-admin/statistics')
.expect('Content-Type', /json/)
.expect(200);

expect(body).toMatchObject({
apiTokens: { client: 1, admin: 1, frontend: 1 },
});

const { text: csv } = await app.request
.get('/api/admin/instance-admin/statistics/csv')
.expect('Content-Type', /text\/csv/)
.expect(200);

expect(csv).toMatch(/{""client"":1,""admin"":1,""frontend"":1}/);
});

test('should return instance statistics with correct number of projects', async () => {
await stores.projectStore.create({
id: 'test',
Expand Down Expand Up @@ -77,7 +115,7 @@ test('should return signed instance statistics', async () => {
});
});

test('should return instance statistics as CVS', async () => {
test('should return instance statistics as CSV', async () => {
await stores.featureToggleStore.create('default', {
name: 'TestStats2',
createdByUserId: 9999,
Expand Down

0 comments on commit e5cca66

Please sign in to comment.