diff --git a/src/lib/openapi/meta-schema-rules.test.ts b/src/lib/openapi/meta-schema-rules.test.ts index 2e840daa24..ccc1d73f67 100644 --- a/src/lib/openapi/meta-schema-rules.test.ts +++ b/src/lib/openapi/meta-schema-rules.test.ts @@ -113,7 +113,6 @@ const metaRules: Rule[] = [ 'groupsSchema', 'groupUserModelSchema', 'idSchema', - 'instanceAdminStatsSchema', 'legalValueSchema', 'loginSchema', 'maintenanceSchema', @@ -217,7 +216,6 @@ const metaRules: Rule[] = [ 'groupsSchema', 'groupUserModelSchema', 'idSchema', - 'instanceAdminStatsSchema', 'legalValueSchema', 'loginSchema', 'maintenanceSchema', diff --git a/src/lib/openapi/spec/instance-admin-stats-schema.ts b/src/lib/openapi/spec/instance-admin-stats-schema.ts index f875fc67f6..74593d0084 100644 --- a/src/lib/openapi/spec/instance-admin-stats-schema.ts +++ b/src/lib/openapi/spec/instance-admin-stats-schema.ts @@ -4,57 +4,132 @@ export const instanceAdminStatsSchema = { $id: '#/components/schemas/instanceAdminStatsSchema', type: 'object', additionalProperties: false, + description: + 'Information about an instance and statistics about usage of various features of Unleash', required: ['instanceId'], properties: { instanceId: { type: 'string', + description: + 'A unique identifier for this instance. Generated by the database migration scripts at first run. Typically a UUID.', + example: 'ed3861ae-78f9-4e8c-8e57-b57efc15f82b', }, timestamp: { type: 'string', format: 'date-time', nullable: true, + description: 'When these statistics were produced', + example: '2023-06-12T12:25:06Z', }, versionOSS: { type: 'string', + description: + 'The version of Unleash OSS that is bundled in this instance', + example: '5.1.7', }, versionEnterprise: { type: 'string', + description: + 'The version of Unleash Enterprise that is bundled in this instance', + example: '5.1.7', }, users: { type: 'number', + description: 'The number of users this instance has', + example: 8, + minimum: 0, }, featureToggles: { type: 'number', + description: 'The number of feature-toggles this instance has', + example: 47, + minimum: 0, }, projects: { type: 'number', + description: 'The number of projects defined in this instance.', + example: 3, + minimum: 0, }, contextFields: { type: 'number', + description: + 'The number of context fields defined in this instance.', + example: 7, + minimum: 0, }, roles: { type: 'number', + description: 'The number of roles defined in this instance', + example: 5, + minimum: 0, }, groups: { type: 'number', + description: 'The number of groups defined in this instance', + example: 12, + minimum: 0, }, environments: { type: 'number', + description: 'The number of environments defined in this instance', + example: 3, + minimum: 0, }, segments: { type: 'number', + description: 'The number of segments defined in this instance', + example: 19, + minimum: 0, }, strategies: { type: 'number', + description: 'The number of strategies defined in this instance', + example: 8, + minimum: 0, }, SAMLenabled: { - type: 'number', + type: 'boolean', + description: + 'Whether or not SAML authentication is enabled for this instance', + example: false, }, OIDCenabled: { - type: 'number', + type: 'boolean', + description: + 'Whether or not OIDC authentication is enabled for this instance', + example: true, + }, + clientApps: { + type: 'array', + description: + 'A count of connected applications in the last week, last month and all time since last restart', + items: { + type: 'object', + description: + 'An entry describing how many client applications has been observed over the defined range', + properties: { + range: { + type: 'string', + description: 'A description of a time range', + enum: ['allTime', '30d', '7d'], + example: '30d', + }, + count: { + type: 'number', + description: + 'The number of client applications that have been observed in this period', + example: 1, + }, + }, + }, }, sum: { type: 'string', + description: + 'A SHA-256 checksum of the instance statistics to be used to verify that the data in this object has not been tampered with', + example: + 'b023323477abb1eb145bebf3cdb30a1c2063e3edc1f7ae474ed8ed6c80de9a3b', }, }, components: {}, diff --git a/src/lib/openapi/util/create-response-schema.ts b/src/lib/openapi/util/create-response-schema.ts index 73e4e43d75..a6d7097ea1 100644 --- a/src/lib/openapi/util/create-response-schema.ts +++ b/src/lib/openapi/util/create-response-schema.ts @@ -36,6 +36,15 @@ export const createResponseSchema = ( }); }; +export const createCsvResponseSchema = ( + schemaName: string, + example: string, +): OpenAPIV3.ResponseObject => { + return createResponseSchemas(schemaName, { + 'text/csv': { example, ...schemaTyped('string') }, + }); +}; + export const resourceCreatedResponseSchema = ( schemaName: string, ): OpenAPIV3.ResponseObject => { diff --git a/src/lib/routes/admin-api/instance-admin.ts b/src/lib/routes/admin-api/instance-admin.ts index 48a7debdc9..120ee7218d 100644 --- a/src/lib/routes/admin-api/instance-admin.ts +++ b/src/lib/routes/admin-api/instance-admin.ts @@ -9,15 +9,21 @@ import { UiConfigSchema } from '../../openapi/spec/ui-config-schema'; import { InstanceStats, InstanceStatsService, + InstanceStatsSigned, } from '../../services/instance-stats-service'; import { OpenApiService } from '../../services/openapi-service'; -import { createResponseSchema } from '../../openapi/util/create-response-schema'; +import { + createCsvResponseSchema, + createResponseSchema, +} from '../../openapi/util/create-response-schema'; class InstanceAdminController extends Controller { private instanceStatsService: InstanceStatsService; private openApiService: OpenApiService; + private jsonCsvParser: Parser; + constructor( config: IUnleashConfig, { @@ -26,7 +32,7 @@ class InstanceAdminController extends Controller { }: Pick, ) { super(config); - + this.jsonCsvParser = new Parser(); this.openApiService = openApiService; this.instanceStatsService = instanceStatsService; @@ -35,6 +41,23 @@ class InstanceAdminController extends Controller { path: '/statistics/csv', handler: this.getStatisticsCSV, permission: NONE, + middleware: [ + openApiService.validPath({ + tags: ['Instance Admin'], + summary: 'Instance usage statistics', + description: + 'Provides statistics about various features of Unleash to allow for reporting of usage for self-hosted customers. The response contains data such as the number of users, groups, features, strategies, versions, etc.', + operationId: 'getInstanceAdminStatsCsv', + responses: { + 200: createCsvResponseSchema( + 'instanceAdminStatsSchemaCsv', + this.jsonCsvParser.parse( + this.instanceStatsExample(), + ), + ), + }, + }), + ], }); this.route({ @@ -46,6 +69,9 @@ class InstanceAdminController extends Controller { openApiService.validPath({ tags: ['Instance Admin'], operationId: 'getInstanceAdminStats', + summary: 'Instance usage statistics', + description: + 'Provides statistics about various features of Unleash to allow for reporting of usage for self-hosted customers. The response contains data such as the number of users, groups, features, strategies, versions, etc.', responses: { 200: createResponseSchema('instanceAdminStatsSchema'), }, @@ -55,6 +81,34 @@ class InstanceAdminController extends Controller { }); } + instanceStatsExample(): InstanceStatsSigned { + return { + OIDCenabled: true, + SAMLenabled: false, + clientApps: [ + { range: 'allTime', count: 15 }, + { range: '30d', count: 9 }, + { range: '7d', count: 5 }, + ], + contextFields: 6, + environments: 2, + featureExports: 0, + featureImports: 0, + featureToggles: 29, + groups: 3, + instanceId: 'ed3861ae-78f9-4e8c-8e57-b57efc15f82b', + projects: 1, + roles: 5, + segments: 2, + strategies: 8, + sum: 'some-sha256-hash', + timestamp: new Date(2023, 6, 12, 10, 0, 0, 0), + users: 10, + versionEnterprise: '5.1.7', + versionOSS: '5.1.7', + }; + } + async getStatistics( req: AuthedRequest, res: Response, diff --git a/src/lib/services/instance-stats-service.ts b/src/lib/services/instance-stats-service.ts index 78f73c69e0..e45bf3acd7 100644 --- a/src/lib/services/instance-stats-service.ts +++ b/src/lib/services/instance-stats-service.ts @@ -19,7 +19,7 @@ import VersionService from './version-service'; import { ISettingStore } from '../types/stores/settings-store'; import { FEATURES_EXPORTED, FEATURES_IMPORTED } from '../types'; -type TimeRange = 'allTime' | '30d' | '7d'; +export type TimeRange = 'allTime' | '30d' | '7d'; export interface InstanceStats { instanceId: string; @@ -42,7 +42,7 @@ export interface InstanceStats { clientApps: { range: TimeRange; count: number }[]; } -interface InstanceStatsSigned extends InstanceStats { +export interface InstanceStatsSigned extends InstanceStats { sum: string; } diff --git a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap index 8cd1895cb5..b82828fc0b 100644 --- a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap +++ b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap @@ -3043,55 +3043,122 @@ The provider you choose for your addon dictates what properties the \`parameters }, "instanceAdminStatsSchema": { "additionalProperties": false, + "description": "Information about an instance and statistics about usage of various features of Unleash", "properties": { "OIDCenabled": { - "type": "number", + "description": "Whether or not OIDC authentication is enabled for this instance", + "example": true, + "type": "boolean", }, "SAMLenabled": { - "type": "number", + "description": "Whether or not SAML authentication is enabled for this instance", + "example": false, + "type": "boolean", + }, + "clientApps": { + "description": "A count of connected applications in the last week, last month and all time since last restart", + "items": { + "description": "An entry describing how many client applications has been observed over the defined range", + "properties": { + "count": { + "description": "The number of client applications that have been observed in this period", + "example": 1, + "type": "number", + }, + "range": { + "description": "A description of a time range", + "enum": [ + "allTime", + "30d", + "7d", + ], + "example": "30d", + "type": "string", + }, + }, + "type": "object", + }, + "type": "array", }, "contextFields": { + "description": "The number of context fields defined in this instance.", + "example": 7, + "minimum": 0, "type": "number", }, "environments": { + "description": "The number of environments defined in this instance", + "example": 3, + "minimum": 0, "type": "number", }, "featureToggles": { + "description": "The number of feature-toggles this instance has", + "example": 47, + "minimum": 0, "type": "number", }, "groups": { + "description": "The number of groups defined in this instance", + "example": 12, + "minimum": 0, "type": "number", }, "instanceId": { + "description": "A unique identifier for this instance. Generated by the database migration scripts at first run. Typically a UUID.", + "example": "ed3861ae-78f9-4e8c-8e57-b57efc15f82b", "type": "string", }, "projects": { + "description": "The number of projects defined in this instance.", + "example": 3, + "minimum": 0, "type": "number", }, "roles": { + "description": "The number of roles defined in this instance", + "example": 5, + "minimum": 0, "type": "number", }, "segments": { + "description": "The number of segments defined in this instance", + "example": 19, + "minimum": 0, "type": "number", }, "strategies": { + "description": "The number of strategies defined in this instance", + "example": 8, + "minimum": 0, "type": "number", }, "sum": { + "description": "A SHA-256 checksum of the instance statistics to be used to verify that the data in this object has not been tampered with", + "example": "b023323477abb1eb145bebf3cdb30a1c2063e3edc1f7ae474ed8ed6c80de9a3b", "type": "string", }, "timestamp": { + "description": "When these statistics were produced", + "example": "2023-06-12T12:25:06Z", "format": "date-time", "nullable": true, "type": "string", }, "users": { + "description": "The number of users this instance has", + "example": 8, + "minimum": 0, "type": "number", }, "versionEnterprise": { + "description": "The version of Unleash Enterprise that is bundled in this instance", + "example": "5.1.7", "type": "string", }, "versionOSS": { + "description": "The version of Unleash OSS that is bundled in this instance", + "example": "5.1.7", "type": "string", }, }, @@ -8310,6 +8377,7 @@ If the provided project does not exist, the list of events will be empty.", "/api/admin/instance-admin/statistics": { "get": { "deprecated": true, + "description": "Provides statistics about various features of Unleash to allow for reporting of usage for self-hosted customers. The response contains data such as the number of users, groups, features, strategies, versions, etc.", "operationId": "getInstanceAdminStats", "responses": { "200": { @@ -8323,6 +8391,31 @@ If the provided project does not exist, the list of events will be empty.", "description": "instanceAdminStatsSchema", }, }, + "summary": "Instance usage statistics", + "tags": [ + "Instance Admin", + ], + }, + }, + "/api/admin/instance-admin/statistics/csv": { + "get": { + "description": "Provides statistics about various features of Unleash to allow for reporting of usage for self-hosted customers. The response contains data such as the number of users, groups, features, strategies, versions, etc.", + "operationId": "getInstanceAdminStatsCsv", + "responses": { + "200": { + "content": { + "text/csv": { + "example": ""OIDCenabled","SAMLenabled","clientApps","contextFields","environments","featureExports","featureImports","featureToggles","groups","instanceId","projects","roles","segments","strategies","sum","timestamp","users","versionEnterprise","versionOSS" +true,false,"[{""range"":""allTime"",""count"":15},{""range"":""30d"",""count"":9},{""range"":""7d"",""count"":5}]",6,2,0,0,29,3,"ed3861ae-78f9-4e8c-8e57-b57efc15f82b",1,5,2,8,"some-sha256-hash","2023-07-12T10:00:00.000Z",10,"5.1.7","5.1.7"", + "schema": { + "type": "string", + }, + }, + }, + "description": "instanceAdminStatsSchemaCsv", + }, + }, + "summary": "Instance usage statistics", "tags": [ "Instance Admin", ],