mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-09 01:17:06 +02:00
OAS for client-api metrics.ts (#1753)
* OAS for client-api metrics.ts * Fix PR comments * Fix PR comments * Fix test * Renamed and synced with proxy * Renamed and synced with proxy * Renamed and synced with proxy * add tests * Update python.md Revert doc * added 400 response, more tests * PR comment * PR comment
This commit is contained in:
parent
e875e67d24
commit
a607dea284
@ -89,6 +89,8 @@ import { eventSchema } from './spec/event-schema';
|
|||||||
import { eventsSchema } from './spec/events-schema';
|
import { eventsSchema } from './spec/events-schema';
|
||||||
import { featureEventsSchema } from './spec/feature-events-schema';
|
import { featureEventsSchema } from './spec/feature-events-schema';
|
||||||
import { clientApplicationSchema } from './spec/client-application-schema';
|
import { clientApplicationSchema } from './spec/client-application-schema';
|
||||||
|
import { clientMetricsSchema } from './spec/client-metrics-schema';
|
||||||
|
import { dateSchema } from './spec/date-schema';
|
||||||
import { clientVariantSchema } from './spec/client-variant-schema';
|
import { clientVariantSchema } from './spec/client-variant-schema';
|
||||||
import { IServerOption } from '../types';
|
import { IServerOption } from '../types';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
@ -104,6 +106,7 @@ export const schemas = {
|
|||||||
applicationSchema,
|
applicationSchema,
|
||||||
applicationsSchema,
|
applicationsSchema,
|
||||||
clientApplicationSchema,
|
clientApplicationSchema,
|
||||||
|
clientMetricsSchema,
|
||||||
cloneFeatureSchema,
|
cloneFeatureSchema,
|
||||||
clientFeatureSchema,
|
clientFeatureSchema,
|
||||||
clientFeaturesSchema,
|
clientFeaturesSchema,
|
||||||
@ -117,6 +120,7 @@ export const schemas = {
|
|||||||
createFeatureSchema,
|
createFeatureSchema,
|
||||||
createFeatureStrategySchema,
|
createFeatureStrategySchema,
|
||||||
createUserSchema,
|
createUserSchema,
|
||||||
|
dateSchema,
|
||||||
emailSchema,
|
emailSchema,
|
||||||
environmentSchema,
|
environmentSchema,
|
||||||
environmentsSchema,
|
environmentsSchema,
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`clientMetricsSchema should fail when required field is missing 1`] = `
|
||||||
|
Object {
|
||||||
|
"errors": Array [
|
||||||
|
Object {
|
||||||
|
"instancePath": "/bucket",
|
||||||
|
"keyword": "required",
|
||||||
|
"message": "must have required property 'stop'",
|
||||||
|
"params": Object {
|
||||||
|
"missingProperty": "stop",
|
||||||
|
},
|
||||||
|
"schemaPath": "#/properties/bucket/required",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"schema": "#/components/schemas/clientMetricsSchema",
|
||||||
|
}
|
||||||
|
`;
|
63
src/lib/openapi/spec/client-metrics-schema.test.ts
Normal file
63
src/lib/openapi/spec/client-metrics-schema.test.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { validateSchema } from '../validate';
|
||||||
|
import { ClientMetricsSchema } from './client-metrics-schema';
|
||||||
|
|
||||||
|
test('clientMetricsSchema full', () => {
|
||||||
|
const data: ClientMetricsSchema = {
|
||||||
|
appName: 'a',
|
||||||
|
instanceId: 'some-id',
|
||||||
|
environment: 'some-env',
|
||||||
|
bucket: {
|
||||||
|
start: Date.now(),
|
||||||
|
stop: Date.now(),
|
||||||
|
toggles: {
|
||||||
|
someToggle: {
|
||||||
|
yes: 52,
|
||||||
|
no: 2,
|
||||||
|
variants: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
validateSchema('#/components/schemas/clientMetricsSchema', data),
|
||||||
|
).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clientMetricsSchema should ignore additional properties without failing when required fields are there', () => {
|
||||||
|
expect(
|
||||||
|
validateSchema('#/components/schemas/clientMetricsSchema', {
|
||||||
|
appName: 'a',
|
||||||
|
someParam: 'some-value',
|
||||||
|
bucket: {
|
||||||
|
start: Date.now(),
|
||||||
|
stop: Date.now(),
|
||||||
|
toggles: {
|
||||||
|
someToggle: {
|
||||||
|
yes: 52,
|
||||||
|
variants: {},
|
||||||
|
someOtherParam: 'some-other-value',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clientMetricsSchema should fail when required field is missing', () => {
|
||||||
|
expect(
|
||||||
|
validateSchema('#/components/schemas/clientMetricsSchema', {
|
||||||
|
appName: 'a',
|
||||||
|
bucket: {
|
||||||
|
start: Date.now(),
|
||||||
|
toggles: {
|
||||||
|
someToggle: {
|
||||||
|
yes: 52,
|
||||||
|
variants: {},
|
||||||
|
someOtherParam: 'some-other-value',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
61
src/lib/openapi/spec/client-metrics-schema.ts
Normal file
61
src/lib/openapi/spec/client-metrics-schema.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { FromSchema } from 'json-schema-to-ts';
|
||||||
|
import { dateSchema } from './date-schema';
|
||||||
|
|
||||||
|
export const clientMetricsSchema = {
|
||||||
|
$id: '#/components/schemas/clientMetricsSchema',
|
||||||
|
type: 'object',
|
||||||
|
required: ['appName', 'bucket'],
|
||||||
|
properties: {
|
||||||
|
appName: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
instanceId: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
bucket: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['start', 'stop', 'toggles'],
|
||||||
|
properties: {
|
||||||
|
start: {
|
||||||
|
$ref: '#/components/schemas/dateSchema',
|
||||||
|
},
|
||||||
|
stop: {
|
||||||
|
$ref: '#/components/schemas/dateSchema',
|
||||||
|
},
|
||||||
|
toggles: {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
yes: {
|
||||||
|
type: 'integer',
|
||||||
|
minimum: 0,
|
||||||
|
},
|
||||||
|
no: {
|
||||||
|
type: 'integer',
|
||||||
|
minimum: 0,
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: {
|
||||||
|
type: 'integer',
|
||||||
|
minimum: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
schemas: {
|
||||||
|
dateSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ClientMetricsSchema = FromSchema<typeof clientMetricsSchema>;
|
9
src/lib/openapi/spec/date-schema.ts
Normal file
9
src/lib/openapi/spec/date-schema.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { FromSchema } from 'json-schema-to-ts';
|
||||||
|
|
||||||
|
export const dateSchema = {
|
||||||
|
$id: '#/components/schemas/dateSchema',
|
||||||
|
oneOf: [{ type: 'string', format: 'date-time' }, { type: 'number' }],
|
||||||
|
components: {},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type DateSchema = FromSchema<typeof dateSchema>;
|
@ -3,7 +3,12 @@ export const unauthorizedResponse = {
|
|||||||
'Authorization information is missing or invalid. Provide a valid API token as the `authorization` header, e.g. `authorization:*.*.my-admin-token`.',
|
'Authorization information is missing or invalid. Provide a valid API token as the `authorization` header, e.g. `authorization:*.*.my-admin-token`.',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const badRequestResponse = {
|
||||||
|
description: 'The request data do not match what we expect.',
|
||||||
|
} as const;
|
||||||
|
|
||||||
const standardResponses = {
|
const standardResponses = {
|
||||||
|
400: badRequestResponse,
|
||||||
401: unauthorizedResponse,
|
401: unauthorizedResponse,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -4,8 +4,7 @@ import getApp from '../../app';
|
|||||||
import { createTestConfig } from '../../../test/config/test-config';
|
import { createTestConfig } from '../../../test/config/test-config';
|
||||||
import { clientMetricsSchema } from '../../services/client-metrics/schema';
|
import { clientMetricsSchema } from '../../services/client-metrics/schema';
|
||||||
import { createServices } from '../../services';
|
import { createServices } from '../../services';
|
||||||
import { IUnleashStores } from '../../types';
|
import { IUnleashOptions, IUnleashStores } from '../../types';
|
||||||
import { IUnleashOptions } from '../../server-impl';
|
|
||||||
|
|
||||||
async function getSetup(opts?: IUnleashOptions) {
|
async function getSetup(opts?: IUnleashOptions) {
|
||||||
const stores = createStores();
|
const stores = createStores();
|
||||||
@ -209,3 +208,48 @@ test('should set lastSeen on toggle', async () => {
|
|||||||
|
|
||||||
expect(toggle.lastSeenAt).toBeTruthy();
|
expect(toggle.lastSeenAt).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should return a 400 when required fields are missing', async () => {
|
||||||
|
stores.featureToggleStore.create('default', {
|
||||||
|
name: 'toggleLastSeen',
|
||||||
|
});
|
||||||
|
await request
|
||||||
|
.post('/api/client/metrics')
|
||||||
|
.send({
|
||||||
|
appName: 'demo',
|
||||||
|
bucket: {
|
||||||
|
start: Date.now(),
|
||||||
|
toggles: {
|
||||||
|
toggleLastSeen: {
|
||||||
|
yes: 200,
|
||||||
|
no: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return a 200 if required fields are there', async () => {
|
||||||
|
stores.featureToggleStore.create('default', {
|
||||||
|
name: 'toggleLastSeen',
|
||||||
|
});
|
||||||
|
await request
|
||||||
|
.post('/api/client/metrics')
|
||||||
|
.send({
|
||||||
|
appName: 'demo',
|
||||||
|
someParam: 'some-value',
|
||||||
|
somOtherParam: 'some--other-value',
|
||||||
|
bucket: {
|
||||||
|
start: Date.now(),
|
||||||
|
stop: Date.now(),
|
||||||
|
toggles: {
|
||||||
|
toggleLastSeen: {
|
||||||
|
yes: 200,
|
||||||
|
no: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.expect(202);
|
||||||
|
});
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import Controller from '../controller';
|
import Controller from '../controller';
|
||||||
import { IUnleashServices } from '../../types';
|
import { IUnleashConfig, IUnleashServices } from '../../types';
|
||||||
import { IUnleashConfig } from '../../types/option';
|
|
||||||
import ClientInstanceService from '../../services/client-metrics/instance-service';
|
import ClientInstanceService from '../../services/client-metrics/instance-service';
|
||||||
import { Logger } from '../../logger';
|
import { Logger } from '../../logger';
|
||||||
import { IAuthRequest } from '../unleash-types';
|
import { IAuthRequest } from '../unleash-types';
|
||||||
@ -11,21 +10,29 @@ import ClientMetricsServiceV2 from '../../services/client-metrics/metrics-servic
|
|||||||
import { User } from '../../server-impl';
|
import { User } from '../../server-impl';
|
||||||
import { IClientApp } from '../../types/model';
|
import { IClientApp } from '../../types/model';
|
||||||
import { NONE } from '../../types/permissions';
|
import { NONE } from '../../types/permissions';
|
||||||
|
import { OpenApiService } from '../../services/openapi-service';
|
||||||
|
import { createRequestSchema, createResponseSchema } from '../../openapi';
|
||||||
|
import { getStandardResponses } from '../../openapi/util/standard-responses';
|
||||||
|
|
||||||
export default class ClientMetricsController extends Controller {
|
export default class ClientMetricsController extends Controller {
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
|
|
||||||
clientInstanceService: ClientInstanceService;
|
clientInstanceService: ClientInstanceService;
|
||||||
|
|
||||||
|
openApiService: OpenApiService;
|
||||||
|
|
||||||
metricsV2: ClientMetricsServiceV2;
|
metricsV2: ClientMetricsServiceV2;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
{
|
{
|
||||||
clientInstanceService,
|
clientInstanceService,
|
||||||
clientMetricsServiceV2,
|
clientMetricsServiceV2,
|
||||||
|
openApiService,
|
||||||
}: Pick<
|
}: Pick<
|
||||||
IUnleashServices,
|
IUnleashServices,
|
||||||
'clientInstanceService' | 'clientMetricsServiceV2'
|
| 'clientInstanceService'
|
||||||
|
| 'clientMetricsServiceV2'
|
||||||
|
| 'openApiService'
|
||||||
>,
|
>,
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
) {
|
) {
|
||||||
@ -34,9 +41,26 @@ export default class ClientMetricsController extends Controller {
|
|||||||
|
|
||||||
this.logger = getLogger('/api/client/metrics');
|
this.logger = getLogger('/api/client/metrics');
|
||||||
this.clientInstanceService = clientInstanceService;
|
this.clientInstanceService = clientInstanceService;
|
||||||
|
this.openApiService = openApiService;
|
||||||
this.metricsV2 = clientMetricsServiceV2;
|
this.metricsV2 = clientMetricsServiceV2;
|
||||||
|
|
||||||
this.post('/', this.registerMetrics, NONE);
|
this.route({
|
||||||
|
method: 'post',
|
||||||
|
path: '',
|
||||||
|
handler: this.registerMetrics,
|
||||||
|
permission: NONE,
|
||||||
|
middleware: [
|
||||||
|
openApiService.validPath({
|
||||||
|
tags: ['client'],
|
||||||
|
operationId: 'registerClientMetrics',
|
||||||
|
requestBody: createRequestSchema('clientMetricsSchema'),
|
||||||
|
responses: {
|
||||||
|
...getStandardResponses(400),
|
||||||
|
202: createResponseSchema('emptyResponse'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveEnvironment(user: User, data: IClientApp) {
|
private resolveEnvironment(user: User, data: IClientApp) {
|
||||||
@ -55,8 +79,11 @@ export default class ClientMetricsController extends Controller {
|
|||||||
data.environment = this.resolveEnvironment(user, data);
|
data.environment = this.resolveEnvironment(user, data);
|
||||||
await this.clientInstanceService.registerInstance(data, clientIp);
|
await this.clientInstanceService.registerInstance(data, clientIp);
|
||||||
|
|
||||||
await this.metricsV2.registerClientMetrics(data, clientIp);
|
try {
|
||||||
|
await this.metricsV2.registerClientMetrics(data, clientIp);
|
||||||
return res.status(202).end();
|
return res.status(202).end();
|
||||||
|
} catch (e) {
|
||||||
|
return res.status(400).end();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -466,6 +466,63 @@ Object {
|
|||||||
],
|
],
|
||||||
"type": "object",
|
"type": "object",
|
||||||
},
|
},
|
||||||
|
"clientMetricsSchema": Object {
|
||||||
|
"properties": Object {
|
||||||
|
"appName": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"bucket": Object {
|
||||||
|
"properties": Object {
|
||||||
|
"start": Object {
|
||||||
|
"$ref": "#/components/schemas/dateSchema",
|
||||||
|
},
|
||||||
|
"stop": Object {
|
||||||
|
"$ref": "#/components/schemas/dateSchema",
|
||||||
|
},
|
||||||
|
"toggles": Object {
|
||||||
|
"additionalProperties": Object {
|
||||||
|
"properties": Object {
|
||||||
|
"no": Object {
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer",
|
||||||
|
},
|
||||||
|
"variants": Object {
|
||||||
|
"additionalProperties": Object {
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer",
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"yes": Object {
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": Array [
|
||||||
|
"start",
|
||||||
|
"stop",
|
||||||
|
"toggles",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"environment": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"instanceId": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": Array [
|
||||||
|
"appName",
|
||||||
|
"bucket",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
"clientVariantSchema": Object {
|
"clientVariantSchema": Object {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": Object {
|
"properties": Object {
|
||||||
@ -703,6 +760,17 @@ Object {
|
|||||||
],
|
],
|
||||||
"type": "object",
|
"type": "object",
|
||||||
},
|
},
|
||||||
|
"dateSchema": Object {
|
||||||
|
"oneOf": Array [
|
||||||
|
Object {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"type": "number",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
"emailSchema": Object {
|
"emailSchema": Object {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": Object {
|
"properties": Object {
|
||||||
@ -5464,6 +5532,40 @@ If the provided project does not exist, the list of events will be empty.",
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"/api/client/metrics": Object {
|
||||||
|
"post": Object {
|
||||||
|
"operationId": "registerClientMetrics",
|
||||||
|
"requestBody": Object {
|
||||||
|
"content": Object {
|
||||||
|
"application/json": Object {
|
||||||
|
"schema": Object {
|
||||||
|
"$ref": "#/components/schemas/clientMetricsSchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "clientMetricsSchema",
|
||||||
|
"required": true,
|
||||||
|
},
|
||||||
|
"responses": Object {
|
||||||
|
"202": Object {
|
||||||
|
"content": Object {
|
||||||
|
"application/json": Object {
|
||||||
|
"schema": Object {
|
||||||
|
"$ref": "#/components/schemas/emptyResponse",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "emptyResponse",
|
||||||
|
},
|
||||||
|
"400": Object {
|
||||||
|
"description": "The request data do not match what we expect.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tags": Array [
|
||||||
|
"client",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
"/api/client/register": Object {
|
"/api/client/register": Object {
|
||||||
"post": Object {
|
"post": Object {
|
||||||
"operationId": "registerClientApplication",
|
"operationId": "registerClientApplication",
|
||||||
|
Loading…
Reference in New Issue
Block a user