1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

feat: Spike frontend applications registration (#9846)

This commit is contained in:
Mateusz Kwasniewski 2025-04-28 09:01:07 +02:00 committed by GitHub
parent 3ac087e0f6
commit 1ccc6cae19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 80 additions and 12 deletions

View File

@ -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',

View File

@ -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<unknown, unknown, ClientMetricsSchema>,
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);
}

View File

@ -111,7 +111,7 @@ export class FrontendApiService {
token: IApiUser,
metrics: ClientMetricsSchema,
ip: string,
): Promise<void> {
): Promise<string> {
FrontendApiService.assertExpectedTokenType(token);
const environment =
@ -127,6 +127,8 @@ export class FrontendApiService {
},
ip,
);
return environment;
}
private async clientForFrontendApiToken(token: IApiUser): Promise<Unleash> {

View File

@ -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<IClientApp, 'instanceId'>,
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);

View File

@ -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 = {

View File

@ -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 {

View File

@ -63,6 +63,7 @@ process.nextTick(async () => {
flagsOverviewSearch: true,
cleanupReminder: true,
strictSchemaValidation: true,
registerFrontendClient: true,
},
},
authentication: {