mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	refactor: stabilize frontend apps reporting (#9880)
This commit is contained in:
		
							parent
							
								
									44b4ba7f60
								
							
						
					
					
						commit
						b0223e38ef
					
				| @ -3,12 +3,7 @@ import { SegmentReadModel } from '../segment/segment-read-model'; | ||||
| import type ClientMetricsServiceV2 from '../metrics/client-metrics/metrics-service-v2'; | ||||
| import SettingService from '../../services/setting-service'; | ||||
| import SettingStore from '../../db/setting-store'; | ||||
| import { | ||||
|     createEventsService, | ||||
|     createFakeEventsService, | ||||
|     createFakeFeatureToggleService, | ||||
|     createFeatureToggleService, | ||||
| } from '../index'; | ||||
| import { createEventsService, createFakeEventsService } from '../index'; | ||||
| import type ConfigurationRevisionService from '../feature-toggle/configuration-revision-service'; | ||||
| import { GlobalFrontendApiCache } from './global-frontend-api-cache'; | ||||
| import ClientFeatureToggleReadModel from './client-feature-toggle-read-model'; | ||||
| @ -17,6 +12,7 @@ import FakeSettingStore from '../../../test/fixtures/fake-setting-store'; | ||||
| import FakeClientFeatureToggleReadModel from './fake-client-feature-toggle-read-model'; | ||||
| import type { IUnleashConfig } from '../../types'; | ||||
| import type { Db } from '../../db/db'; | ||||
| import type ClientInstanceService from '../metrics/instance/instance-service'; | ||||
| 
 | ||||
| export const createFrontendApiService = ( | ||||
|     db: Db, | ||||
| @ -24,6 +20,7 @@ export const createFrontendApiService = ( | ||||
|     // client metrics service needs to be shared because it uses in-memory cache
 | ||||
|     clientMetricsServiceV2: ClientMetricsServiceV2, | ||||
|     configurationRevisionService: ConfigurationRevisionService, | ||||
|     clientInstanceService: ClientInstanceService, | ||||
| ): FrontendApiService => { | ||||
|     const segmentReadModel = new SegmentReadModel(db); | ||||
|     const settingStore = new SettingStore(db, config.getLogger); | ||||
| @ -33,8 +30,6 @@ export const createFrontendApiService = ( | ||||
|         config, | ||||
|         eventService, | ||||
|     ); | ||||
|     // TODO: remove this dependency after we migrate frontend API
 | ||||
|     const featureToggleService = createFeatureToggleService(db, config); | ||||
|     const clientFeatureToggleReadModel = new ClientFeatureToggleReadModel( | ||||
|         db, | ||||
|         config.eventBus, | ||||
| @ -47,12 +42,10 @@ export const createFrontendApiService = ( | ||||
|     ); | ||||
|     return new FrontendApiService( | ||||
|         config, | ||||
|         { segmentReadModel }, | ||||
|         { | ||||
|             featureToggleService, | ||||
|             clientMetricsServiceV2, | ||||
|             settingService, | ||||
|             configurationRevisionService, | ||||
|             clientInstanceService, | ||||
|         }, | ||||
|         globalFrontendApiCache, | ||||
|     ); | ||||
| @ -62,6 +55,7 @@ export const createFakeFrontendApiService = ( | ||||
|     config: IUnleashConfig, | ||||
|     clientMetricsServiceV2: ClientMetricsServiceV2, | ||||
|     configurationRevisionService: ConfigurationRevisionService, | ||||
|     clientInstanceService: ClientInstanceService, | ||||
| ): FrontendApiService => { | ||||
|     const segmentReadModel = new FakeSegmentReadModel(); | ||||
|     const settingStore = new FakeSettingStore(); | ||||
| @ -71,9 +65,6 @@ export const createFakeFrontendApiService = ( | ||||
|         config, | ||||
|         eventService, | ||||
|     ); | ||||
|     // TODO: remove this dependency after we migrate frontend API
 | ||||
|     const featureToggleService = | ||||
|         createFakeFeatureToggleService(config).featureToggleService; | ||||
|     const clientFeatureToggleReadModel = new FakeClientFeatureToggleReadModel(); | ||||
|     const globalFrontendApiCache = new GlobalFrontendApiCache( | ||||
|         config, | ||||
| @ -83,12 +74,10 @@ export const createFakeFrontendApiService = ( | ||||
|     ); | ||||
|     return new FrontendApiService( | ||||
|         config, | ||||
|         { segmentReadModel }, | ||||
|         { | ||||
|             featureToggleService, | ||||
|             clientMetricsServiceV2, | ||||
|             settingService, | ||||
|             configurationRevisionService, | ||||
|             clientInstanceService, | ||||
|         }, | ||||
|         globalFrontendApiCache, | ||||
|     ); | ||||
|  | ||||
| @ -4,11 +4,10 @@ import { | ||||
|     type IFlagResolver, | ||||
|     type IUnleashConfig, | ||||
|     type IUnleashServices, | ||||
|     type IUser, | ||||
|     NONE, | ||||
| } from '../../types'; | ||||
| import type { Logger } from '../../logger'; | ||||
| import ApiUser, { type IApiUser } from '../../types/api-user'; | ||||
| import type { IApiUser } from '../../types/api-user'; | ||||
| import { | ||||
|     type ClientMetricsSchema, | ||||
|     createRequestSchema, | ||||
| @ -228,13 +227,6 @@ export default class FrontendAPIController extends Controller { | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private resolveProject(user: IUser | IApiUser) { | ||||
|         if (user instanceof ApiUser) { | ||||
|             return user.projects; | ||||
|         } | ||||
|         return ['default']; | ||||
|     } | ||||
| 
 | ||||
|     private async registerFrontendApiMetrics( | ||||
|         req: ApiUserRequest<unknown, unknown, ClientMetricsSchema>, | ||||
|         res: Response, | ||||
| @ -248,28 +240,12 @@ export default class FrontendAPIController extends Controller { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const environment = | ||||
|             await this.services.frontendApiService.registerFrontendApiMetrics( | ||||
|                 req.user, | ||||
|                 req.body, | ||||
|                 req.ip, | ||||
|             ); | ||||
| 
 | ||||
|         if ( | ||||
|             req.body.instanceId && | ||||
|             req.headers['unleash-sdk'] && | ||||
|             this.flagResolver.isEnabled('registerFrontendClient') | ||||
|         ) { | ||||
|             const client = { | ||||
|                 appName: req.body.appName, | ||||
|                 instanceId: req.body.instanceId, | ||||
|                 sdkVersion: req.headers['unleash-sdk'] as string, | ||||
|                 sdkType: 'frontend' as const, | ||||
|                 environment: environment, | ||||
|                 projects: this.resolveProject(req.user), | ||||
|             }; | ||||
|             this.services.clientInstanceService.registerFrontendClient(client); | ||||
|         } | ||||
|         await this.services.frontendApiService.registerFrontendApiMetrics( | ||||
|             req.user, | ||||
|             req.body, | ||||
|             req.ip, | ||||
|             req.headers['unleash-sdk'], | ||||
|         ); | ||||
| 
 | ||||
|         res.sendStatus(200); | ||||
|     } | ||||
|  | ||||
| @ -48,7 +48,6 @@ test('frontend api service fetching features from global cache', async () => { | ||||
|     const frontendApiService = new FrontendApiService( | ||||
|         { getLogger: noLogger, eventBus } as unknown as Config, | ||||
|         irrelevant, | ||||
|         irrelevant, | ||||
|         globalFrontendApiCache, | ||||
|     ); | ||||
| 
 | ||||
|  | ||||
| @ -1,17 +1,17 @@ | ||||
| import crypto from 'node:crypto'; | ||||
| import type { | ||||
|     IAuditUser, | ||||
|     IFlagResolver, | ||||
|     IUnleashConfig, | ||||
|     IUnleashServices, | ||||
|     IUnleashStores, | ||||
|     IUser, | ||||
| } from '../../types'; | ||||
| import type { Logger } from '../../logger'; | ||||
| import type { | ||||
|     ClientMetricsSchema, | ||||
|     FrontendApiFeatureSchema, | ||||
| } from '../../openapi'; | ||||
| import type ApiUser from '../../types/api-user'; | ||||
| import type { IApiUser } from '../../types/api-user'; | ||||
| import ApiUser, { type IApiUser } from '../../types/api-user'; | ||||
| import { | ||||
|     type Context, | ||||
|     InMemStorageProvider, | ||||
| @ -31,17 +31,16 @@ import type { GlobalFrontendApiCache } from './global-frontend-api-cache'; | ||||
| 
 | ||||
| export type Config = Pick< | ||||
|     IUnleashConfig, | ||||
|     'getLogger' | 'frontendApi' | 'frontendApiOrigins' | 'eventBus' | ||||
|     | 'getLogger' | ||||
|     | 'frontendApi' | ||||
|     | 'frontendApiOrigins' | ||||
|     | 'eventBus' | ||||
|     | 'flagResolver' | ||||
| >; | ||||
| 
 | ||||
| export type Stores = Pick<IUnleashStores, 'segmentReadModel'>; | ||||
| 
 | ||||
| export type Services = Pick< | ||||
|     IUnleashServices, | ||||
|     | 'featureToggleService' | ||||
|     | 'clientMetricsServiceV2' | ||||
|     | 'settingService' | ||||
|     | 'configurationRevisionService' | ||||
|     'clientMetricsServiceV2' | 'settingService' | 'clientInstanceService' | ||||
| >; | ||||
| 
 | ||||
| export class FrontendApiService { | ||||
| @ -49,10 +48,10 @@ export class FrontendApiService { | ||||
| 
 | ||||
|     private readonly logger: Logger; | ||||
| 
 | ||||
|     private readonly stores: Stores; | ||||
| 
 | ||||
|     private readonly services: Services; | ||||
| 
 | ||||
|     private flagResolver: IFlagResolver; | ||||
| 
 | ||||
|     private readonly globalFrontendApiCache: GlobalFrontendApiCache; | ||||
| 
 | ||||
|     /** | ||||
| @ -67,14 +66,13 @@ export class FrontendApiService { | ||||
| 
 | ||||
|     constructor( | ||||
|         config: Config, | ||||
|         stores: Stores, | ||||
|         services: Services, | ||||
|         globalFrontendApiCache: GlobalFrontendApiCache, | ||||
|     ) { | ||||
|         this.config = config; | ||||
|         this.logger = config.getLogger('services/frontend-api-service.ts'); | ||||
|         this.stores = stores; | ||||
|         this.services = services; | ||||
|         this.flagResolver = config.flagResolver; | ||||
|         this.globalFrontendApiCache = globalFrontendApiCache; | ||||
|     } | ||||
| 
 | ||||
| @ -107,11 +105,19 @@ export class FrontendApiService { | ||||
|         return resultDefinitions; | ||||
|     } | ||||
| 
 | ||||
|     private resolveProject(user: IUser | IApiUser) { | ||||
|         if (user instanceof ApiUser) { | ||||
|             return user.projects; | ||||
|         } | ||||
|         return ['default']; | ||||
|     } | ||||
| 
 | ||||
|     async registerFrontendApiMetrics( | ||||
|         token: IApiUser, | ||||
|         metrics: ClientMetricsSchema, | ||||
|         ip: string, | ||||
|     ): Promise<string> { | ||||
|         sdkVersion?: string | string[], | ||||
|     ): Promise<void> { | ||||
|         FrontendApiService.assertExpectedTokenType(token); | ||||
| 
 | ||||
|         const environment = | ||||
| @ -128,7 +134,21 @@ export class FrontendApiService { | ||||
|             ip, | ||||
|         ); | ||||
| 
 | ||||
|         return environment; | ||||
|         if ( | ||||
|             metrics.instanceId && | ||||
|             typeof sdkVersion === 'string' && | ||||
|             this.flagResolver.isEnabled('registerFrontendClient') | ||||
|         ) { | ||||
|             const client = { | ||||
|                 appName: metrics.appName, | ||||
|                 instanceId: metrics.instanceId, | ||||
|                 sdkVersion: sdkVersion, | ||||
|                 sdkType: 'frontend' as const, | ||||
|                 environment: environment, | ||||
|                 projects: this.resolveProject(token), | ||||
|             }; | ||||
|             this.services.clientInstanceService.registerFrontendClient(client); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private async clientForFrontendApiToken(token: IApiUser): Promise<Unleash> { | ||||
|  | ||||
| @ -9,8 +9,9 @@ import { type ISettingStore, TEST_AUDIT_USER } from '../../lib/types'; | ||||
| import { frontendSettingsKey } from '../../lib/types/settings/frontend-settings'; | ||||
| import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store'; | ||||
| import { createFakeEventsService } from '../features'; | ||||
| import type { GlobalFrontendApiCache } from '../features/frontend-api/global-frontend-api-cache'; | ||||
| import type { Services } from '../features/frontend-api/frontend-api-service'; | ||||
| 
 | ||||
| const TEST_USER_ID = -9999; | ||||
| const createSettingService = ( | ||||
|     frontendApiOrigins: string[], | ||||
| ): { frontendApiService: FrontendApiService; settingStore: ISettingStore } => { | ||||
| @ -30,8 +31,11 @@ const createSettingService = ( | ||||
|     }; | ||||
| 
 | ||||
|     return { | ||||
|         //@ts-ignore
 | ||||
|         frontendApiService: new FrontendApiService(config, stores, services), | ||||
|         frontendApiService: new FrontendApiService( | ||||
|             config, | ||||
|             services as Services, | ||||
|             {} as GlobalFrontendApiCache, | ||||
|         ), | ||||
|         settingStore: stores.settingStore, | ||||
|     }; | ||||
| }; | ||||
|  | ||||
| @ -358,11 +358,13 @@ export const createServices = ( | ||||
|               config, | ||||
|               clientMetricsServiceV2, | ||||
|               configurationRevisionService, | ||||
|               clientInstanceService, | ||||
|           ) | ||||
|         : createFakeFrontendApiService( | ||||
|               config, | ||||
|               clientMetricsServiceV2, | ||||
|               configurationRevisionService, | ||||
|               clientInstanceService, | ||||
|           ); | ||||
| 
 | ||||
|     const edgeService = new EdgeService({ apiTokenService }, config); | ||||
|  | ||||
| @ -12,6 +12,7 @@ import { | ||||
| let app: IUnleashTest; | ||||
| let db: ITestDb; | ||||
| let defaultToken: IApiToken; | ||||
| let frontendToken: IApiToken; | ||||
| 
 | ||||
| const metrics = { | ||||
|     appName: 'appName', | ||||
| @ -56,6 +57,7 @@ beforeAll(async () => { | ||||
|             experimental: { | ||||
|                 flags: { | ||||
|                     strictSchemaValidation: true, | ||||
|                     registerFrontendClient: true, | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
| @ -69,6 +71,14 @@ beforeAll(async () => { | ||||
|             environment: 'default', | ||||
|             tokenName: 'tester', | ||||
|         }); | ||||
| 
 | ||||
|     frontendToken = | ||||
|         await app.services.apiTokenService.createApiTokenWithProjects({ | ||||
|             type: ApiTokenType.FRONTEND, | ||||
|             projects: ['default'], | ||||
|             environment: 'default', | ||||
|             tokenName: 'tester', | ||||
|         }); | ||||
| }); | ||||
| 
 | ||||
| afterEach(async () => { | ||||
| @ -180,6 +190,32 @@ test('should show correct application metrics', async () => { | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| test('should report frontend application instances', async () => { | ||||
|     await app.request | ||||
|         .post('/api/frontend/client/metrics') | ||||
|         .set('Authorization', frontendToken.secret) | ||||
|         .set('Unleash-Sdk', 'unleash-client-js:1.0.0') | ||||
|         .send(metrics) | ||||
|         .expect(200); | ||||
|     await app.services.clientInstanceService.bulkAdd(); | ||||
| 
 | ||||
|     const { body } = await app.request | ||||
|         .get( | ||||
|             `/api/admin/metrics/instances/${metrics.appName}/environment/default`, | ||||
|         ) | ||||
|         .expect(200); | ||||
| 
 | ||||
|     expect(body).toMatchObject({ | ||||
|         instances: [ | ||||
|             { | ||||
|                 instanceId: metrics.instanceId, | ||||
|                 clientIp: null, | ||||
|                 sdkVersion: 'unleash-client-js:1.0.0', | ||||
|             }, | ||||
|         ], | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| test('should show missing features and strategies', async () => { | ||||
|     await Promise.all([ | ||||
|         app.createFeature('toggle-name-1'), | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user