From 77d5156ebad2714aaca7bc40e49d594381d10437 Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Tue, 7 May 2024 09:32:46 +0300 Subject: [PATCH] feat: start exposing environment metrics from feature endpoint (#6986) We want to start showing same donut that we do show in project page. This is setting it up for UI. --- .../feature-toggle-strategies-store.ts | 36 +++++++++++-- .../tests/feature-toggles.e2e.test.ts | 51 ++++++++++++++++++- src/lib/types/model.ts | 2 + 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts b/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts index 7583ad095c..e7351ffc99 100644 --- a/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts +++ b/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts @@ -373,11 +373,37 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { userId, }: ILoadFeatureToggleWithEnvsParams): Promise { const stopTimer = this.timer('getFeatureAdmin'); - let query = this.db('features_view') + const query = this.db.with('metrics', (queryBuilder) => { + queryBuilder + .sum('yes as yes') + .sum('no as no') + .select(['client_metrics_env.environment']) + .from('client_metrics_env') + .where( + 'client_metrics_env.timestamp', + '>=', + this.db.raw("NOW() - INTERVAL '1 hour'"), + ) + .andWhere('client_metrics_env.feature_name', featureName) + .groupBy(['client_metrics_env.environment']); + }); + + query + .from('features_view') .where('name', featureName) .modify(FeatureToggleStore.filterByArchived, archived); - let selectColumns = ['features_view.*'] as (string | Raw)[]; + let selectColumns = ['features_view.*', 'yes', 'no'] as ( + | string + | Raw + )[]; + + // add metrics + query.leftJoin( + 'metrics', + 'metrics.environment', + 'features_view.environment', + ); query.leftJoin('last_seen_at_metrics', function () { this.on( @@ -390,13 +416,14 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { 'features_view.name', ); }); + // Override feature view for now selectColumns.push( 'last_seen_at_metrics.last_seen_at as env_last_seen_at', ); if (userId) { - query = query.leftJoin(`favorite_features`, function () { + query.leftJoin(`favorite_features`, function () { this.on( 'favorite_features.feature', 'features_view.name', @@ -409,7 +436,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { ), ]; } - const rows = await query.select(selectColumns); stopTimer(); @@ -463,6 +489,8 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { acc.variants = Array.from(currentVariants.values()); env.enabled = r.enabled; + env.yes = Number(r.yes) || 0; + env.no = Number(r.no) || 0; env.type = r.environment_type; env.sortOrder = r.environment_sort_order; if (!env.strategies) { diff --git a/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts b/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts index d473c6d908..abd55bc546 100644 --- a/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts +++ b/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts @@ -16,7 +16,7 @@ import { FEATURE_STRATEGY_REMOVE, } from '../../../types/events'; import ApiUser from '../../../types/api-user'; -import { ApiTokenType } from '../../../types/models/api-token'; +import { ApiTokenType, type IApiToken } from '../../../types/models/api-token'; import IncompatibleProjectError from '../../../error/incompatible-project-error'; import { type IStrategyConfig, @@ -36,6 +36,7 @@ import { ForbiddenError } from '../../../error'; let app: IUnleashTest; let db: ITestDb; +let defaultToken: IApiToken; const sortOrderFirst = 0; const sortOrderSecond = 10; const TESTUSERID = 3333; @@ -103,6 +104,14 @@ beforeAll(async () => { }, db.rawDatabase, ); + + defaultToken = + await app.services.apiTokenService.createApiTokenWithProjects({ + type: ApiTokenType.CLIENT, + projects: ['default'], + environment: 'default', + tokenName: 'tester', + }); }); afterEach(async () => { @@ -3684,3 +3693,43 @@ test('should return correct data structure for /api/admin/features', async () => strategies: [], }); }); + +test('can get evaluation metrics', async () => { + await app.createFeature('metric-feature'); + + const now = new Date(); + await app.request + .post('/api/client/metrics') + .set('Authorization', defaultToken.secret) + .send({ + appName: 'appName', + instanceId: 'instanceId', + bucket: { + start: now, + stop: now, + toggles: { + 'metric-feature': { + yes: 123, + no: 321, + }, + }, + }, + }) + .expect(202); + + await app.services.clientMetricsServiceV2.bulkAdd(); + + const { body } = await app.request.get( + '/api/admin/projects/default/features/metric-feature', + ); + expect(body).toMatchObject({ + name: 'metric-feature', + environments: [ + { + name: 'default', + yes: 123, + no: 321, + }, + ], + }); +}); diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 5c572d312c..63460ab199 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -214,6 +214,8 @@ export interface IEnvironmentOverview extends IEnvironmentBase { variantCount: number; hasStrategies?: boolean; hasEnabledStrategies?: boolean; + yes?: number; + no?: number; } export interface IFeatureOverview {