UNPKG

unleash-server

Version:

Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.

204 lines • 9.21 kB
import Controller from '../../../routes/controller.js'; import { NONE } from '../../../types/permissions.js'; import { createRequestSchema } from '../../../openapi/util/create-request-schema.js'; import { emptyResponse, getStandardResponses, } from '../../../openapi/util/standard-responses.js'; import rateLimit from 'express-rate-limit'; import { minutesToMilliseconds } from 'date-fns'; import { clientMetricsEnvBulkSchema, customMetricsSchema, } from '../shared/schema.js'; export default class ClientMetricsController extends Controller { constructor({ clientInstanceService, clientMetricsServiceV2, openApiService, customMetricsService, }, config) { super(config); const { getLogger } = config; this.logger = getLogger('/api/client/metrics'); this.clientInstanceService = clientInstanceService; this.openApiService = openApiService; this.metricsV2 = clientMetricsServiceV2; this.customMetricsService = customMetricsService; this.flagResolver = config.flagResolver; this.route({ method: 'post', path: '', handler: this.registerMetrics, permission: NONE, middleware: [ openApiService.validPath({ tags: ['Client'], summary: 'Register client usage metrics', description: `Registers usage metrics. Stores information about how many times each flag was evaluated to enabled and disabled within a time frame. If provided, this operation will also store data on how many times each feature flag's variants were displayed to the end user.`, operationId: 'registerClientMetrics', requestBody: createRequestSchema('clientMetricsSchema'), responses: { ...getStandardResponses(400), 202: emptyResponse, 204: emptyResponse, }, }), rateLimit({ windowMs: minutesToMilliseconds(1), max: config.metricsRateLimiting.clientMetricsMaxPerMinute, validate: false, standardHeaders: true, legacyHeaders: false, }), ], }); this.route({ method: 'post', path: '/bulk', handler: this.bulkMetrics, permission: NONE, middleware: [ this.openApiService.validPath({ tags: ['Unleash Edge'], summary: 'Send metrics in bulk', description: `This operation accepts batched metrics from any client. Metrics will be inserted into Unleash's metrics storage`, operationId: 'clientBulkMetrics', requestBody: createRequestSchema('bulkMetricsSchema'), responses: { 202: emptyResponse, ...getStandardResponses(400, 413, 415), }, }), ], }); this.route({ method: 'post', path: '/custom', handler: this.customMetrics, permission: NONE, middleware: [ this.openApiService.validPath({ tags: ['Client'], summary: 'Send custom metrics', description: `This operation accepts custom metrics from clients. These metrics will be exposed via Prometheus in Unleash.`, operationId: 'clientCustomMetrics', requestBody: createRequestSchema('customMetricsSchema'), responses: { 202: emptyResponse, ...getStandardResponses(400), }, }), rateLimit({ windowMs: minutesToMilliseconds(1), max: config.metricsRateLimiting.clientMetricsMaxPerMinute, validate: false, standardHeaders: true, legacyHeaders: false, }), ], }); // Note: Custom metrics GET endpoints are now handled by the admin API } async processPromiseResults(promises) { const results = await Promise.allSettled(promises); const rejected = results.filter((result) => result.status === 'rejected'); if (rejected.length) { this.logger.warn('Some promise tasks failed', rejected.map((r) => r.reason?.message || r.reason)); } return rejected.length === 0; } async registerMetrics(req, res) { if (this.config.flagResolver.isEnabled('disableMetrics')) { res.status(204).end(); } else { try { const { body: data, ip: clientIp, user } = req; const { impactMetrics, ...metricsData } = data; metricsData.environment = this.metricsV2.resolveMetricsEnvironment(user, metricsData); await this.clientInstanceService.registerInstance(metricsData, clientIp); await this.metricsV2.registerClientMetrics(metricsData, clientIp); if (this.flagResolver.isEnabled('impactMetrics') && impactMetrics) { await this.metricsV2.registerImpactMetrics(impactMetrics); } res.getHeaderNames().forEach((header) => { res.removeHeader(header); }); res.status(202).end(); } catch (_e) { res.status(400).end(); } } } async customMetrics(req, res) { if (this.config.flagResolver.isEnabled('disableMetrics')) { res.status(204).end(); } else { try { const { body } = req; // Use Joi validation for custom metrics await customMetricsSchema.validateAsync(body); // Process and store custom metrics if (body.metrics && Array.isArray(body.metrics)) { const validMetrics = body.metrics.filter((metric) => typeof metric.name === 'string' && typeof metric.value === 'number'); if (validMetrics.length < body.metrics.length) { this.logger.warn('Some invalid metric types found, skipping'); } this.customMetricsService.addMetrics(validMetrics); } res.status(202).end(); } catch (e) { this.logger.error('Failed to process custom metrics', e); res.status(400).end(); } } } async bulkMetrics(req, res) { if (this.config.flagResolver.isEnabled('disableMetrics')) { res.status(204).end(); } else { const { body, ip: clientIp } = req; const { metrics, applications, impactMetrics } = body; const promises = []; try { for (const app of applications) { if (app.sdkType === 'frontend' && typeof app.sdkVersion === 'string') { this.clientInstanceService.registerFrontendClient({ appName: app.appName, instanceId: app.instanceId, environment: app.environment, sdkType: app.sdkType, sdkVersion: app.sdkVersion, projects: app.projects, }); } else { promises.push(this.clientInstanceService.registerBackendClient(app, clientIp)); } } if (metrics && metrics.length > 0) { const data = await clientMetricsEnvBulkSchema.validateAsync(metrics); const { user } = req; const acceptedEnvironment = this.metricsV2.resolveUserEnvironment(user); const filteredData = data.filter((metric) => metric.environment === acceptedEnvironment); promises.push(this.metricsV2.registerBulkMetrics(filteredData)); } if (this.flagResolver.isEnabled('impactMetrics') && impactMetrics && impactMetrics.length > 0) { promises.push(this.metricsV2.registerImpactMetrics(impactMetrics)); } const ok = await this.processPromiseResults(promises); if (!ok) { res.status(400).end(); } else { res.status(202).end(); } } catch (_e) { await this.processPromiseResults(promises); res.status(400).end(); } } } } //# sourceMappingURL=metrics.js.map