From 1ccc6cae19523df105684eefbdd884070617c106 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Mon, 28 Apr 2025 09:01:07 +0200 Subject: [PATCH] feat: Spike frontend applications registration (#9846) --- src/lib/db/client-instance-store.ts | 1 + .../frontend-api/frontend-api-controller.ts | 55 ++++++++++++++++--- .../frontend-api/frontend-api-service.ts | 4 +- .../metrics/instance/instance-service.ts | 13 ++++- src/lib/types/experimental.ts | 7 ++- src/lib/types/model.ts | 11 ++++ src/server-dev.ts | 1 + 7 files changed, 80 insertions(+), 12 deletions(-) diff --git a/src/lib/db/client-instance-store.ts b/src/lib/db/client-instance-store.ts index 9aed17892e..bcf5451adb 100644 --- a/src/lib/db/client-instance-store.ts +++ b/src/lib/db/client-instance-store.ts @@ -35,6 +35,7 @@ const mapToDb = (client) => ({ app_name: client.appName, instance_id: client.instanceId, sdk_version: client.sdkVersion || '', + sdk_type: client.sdkType, client_ip: client.clientIp, last_seen: client.lastSeen || 'now()', environment: client.environment || 'default', diff --git a/src/lib/features/frontend-api/frontend-api-controller.ts b/src/lib/features/frontend-api/frontend-api-controller.ts index beb4b35779..fa90ed5f33 100644 --- a/src/lib/features/frontend-api/frontend-api-controller.ts +++ b/src/lib/features/frontend-api/frontend-api-controller.ts @@ -1,17 +1,23 @@ import type { Request, Response } from 'express'; import Controller from '../../routes/controller'; -import { type IUnleashConfig, type IUnleashServices, NONE } from '../../types'; +import { + type IFlagResolver, + type IUnleashConfig, + type IUnleashServices, + type IUser, + NONE, +} from '../../types'; import type { Logger } from '../../logger'; -import type { IApiUser } from '../../types/api-user'; +import ApiUser, { type IApiUser } from '../../types/api-user'; import { type ClientMetricsSchema, createRequestSchema, createResponseSchema, emptyResponse, - getStandardResponses, type FrontendApiClientSchema, frontendApiFeaturesSchema, type FrontendApiFeaturesSchema, + getStandardResponses, } from '../../openapi'; import type { Context } from 'unleash-client'; import { enrichContextWithIp } from './index'; @@ -34,7 +40,10 @@ interface ApiUserRequest< type Services = Pick< IUnleashServices, - 'settingService' | 'frontendApiService' | 'openApiService' + | 'settingService' + | 'frontendApiService' + | 'openApiService' + | 'clientInstanceService' >; export default class FrontendAPIController extends Controller { @@ -44,10 +53,13 @@ export default class FrontendAPIController extends Controller { private timer: Function; + private flagResolver: IFlagResolver; + constructor(config: IUnleashConfig, services: Services) { super(config); this.logger = config.getLogger('frontend-api-controller.ts'); this.services = services; + this.flagResolver = config.flagResolver; this.timer = (functionName: string) => metricsHelper.wrapTimer(config.eventBus, FUNCTION_TIME, { @@ -216,6 +228,13 @@ 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, res: Response, @@ -229,11 +248,29 @@ export default class FrontendAPIController extends Controller { return; } - await this.services.frontendApiService.registerFrontendApiMetrics( - req.user, - req.body, - req.ip, - ); + 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); + } + res.sendStatus(200); } diff --git a/src/lib/features/frontend-api/frontend-api-service.ts b/src/lib/features/frontend-api/frontend-api-service.ts index 66e9a3b151..82ae233d28 100644 --- a/src/lib/features/frontend-api/frontend-api-service.ts +++ b/src/lib/features/frontend-api/frontend-api-service.ts @@ -111,7 +111,7 @@ export class FrontendApiService { token: IApiUser, metrics: ClientMetricsSchema, ip: string, - ): Promise { + ): Promise { FrontendApiService.assertExpectedTokenType(token); const environment = @@ -127,6 +127,8 @@ export class FrontendApiService { }, ip, ); + + return environment; } private async clientForFrontendApiToken(token: IApiUser): Promise { diff --git a/src/lib/features/metrics/instance/instance-service.ts b/src/lib/features/metrics/instance/instance-service.ts index d975ed3e73..03309c147d 100644 --- a/src/lib/features/metrics/instance/instance-service.ts +++ b/src/lib/features/metrics/instance/instance-service.ts @@ -13,7 +13,11 @@ import type { import type { IFeatureToggleStore } from '../../feature-toggle/types/feature-toggle-store-type'; import type { IStrategyStore } from '../../../types/stores/strategy-store'; import type { IClientInstanceStore } from '../../../types/stores/client-instance-store'; -import type { IClientApp, ISdkHeartbeat } from '../../../types/model'; +import type { + IClientApp, + IFrontendClientApp, + ISdkHeartbeat, +} from '../../../types/model'; import { clientRegisterSchema } from '../shared/schema'; import type { IClientMetricsStoreV2 } from '../client-metrics/client-metrics-store-v2-type'; @@ -104,6 +108,12 @@ export default class ClientInstanceService { }); } + public registerFrontendClient(data: IFrontendClientApp): void { + data.createdBy = SYSTEM_USER.username!; + + this.seenClients[this.clientKey(data)] = data; + } + public async registerClient( data: PartialSome, clientIp: string, @@ -111,6 +121,7 @@ export default class ClientInstanceService { const value = await clientRegisterSchema.validateAsync(data); value.clientIp = clientIp; value.createdBy = SYSTEM_USER.username!; + value.sdkType = 'backend'; this.seenClients[this.clientKey(value)] = value; this.eventBus.emit(CLIENT_REGISTERED, value); diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 7d5f990a0e..890d23fe21 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -69,7 +69,8 @@ export type IFlagKey = | 'flagsOverviewSearch' | 'flagsReleaseManagementUI' | 'cleanupReminder' - | 'removeInactiveApplications'; + | 'removeInactiveApplications' + | 'registerFrontendClient'; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; @@ -330,6 +331,10 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_REMOVE_INACTIVE_APPLICATIONS, false, ), + registerFrontendClient: parseEnvVarBoolean( + process.env.UNLEASH_EXPERIMENTAL_REGISTER_FRONTEND_CLIENT, + false, + ), }; export const defaultExperimentalOptions: IExperimentalOptions = { diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 625d37422b..b1179b793e 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -460,6 +460,16 @@ export interface IRoleIdentifier { roleName?: RoleName; } +export interface IFrontendClientApp { + appName: string; + instanceId: string; + sdkVersion: string; + sdkType: 'frontend'; + environment: string; + projects: string[]; + createdBy?: string; +} + export interface IClientApp { appName: string; instanceId: string; @@ -478,6 +488,7 @@ export interface IClientApp { platformVersion?: string; yggdrasilVersion?: string; specVersion?: string; + sdkType?: 'frontend' | 'backend'; } export interface IAppFeature { diff --git a/src/server-dev.ts b/src/server-dev.ts index 6f8ef9e23d..69ec039c8d 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -63,6 +63,7 @@ process.nextTick(async () => { flagsOverviewSearch: true, cleanupReminder: true, strictSchemaValidation: true, + registerFrontendClient: true, }, }, authentication: {