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

refactor: stabilize frontend apps reporting (#9880)

This commit is contained in:
Mateusz Kwasniewski 2025-05-01 15:43:03 +02:00 committed by GitHub
parent 44b4ba7f60
commit b0223e38ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 94 additions and 68 deletions

View File

@ -3,12 +3,7 @@ import { SegmentReadModel } from '../segment/segment-read-model';
import type ClientMetricsServiceV2 from '../metrics/client-metrics/metrics-service-v2'; import type ClientMetricsServiceV2 from '../metrics/client-metrics/metrics-service-v2';
import SettingService from '../../services/setting-service'; import SettingService from '../../services/setting-service';
import SettingStore from '../../db/setting-store'; import SettingStore from '../../db/setting-store';
import { import { createEventsService, createFakeEventsService } from '../index';
createEventsService,
createFakeEventsService,
createFakeFeatureToggleService,
createFeatureToggleService,
} from '../index';
import type ConfigurationRevisionService from '../feature-toggle/configuration-revision-service'; import type ConfigurationRevisionService from '../feature-toggle/configuration-revision-service';
import { GlobalFrontendApiCache } from './global-frontend-api-cache'; import { GlobalFrontendApiCache } from './global-frontend-api-cache';
import ClientFeatureToggleReadModel from './client-feature-toggle-read-model'; 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 FakeClientFeatureToggleReadModel from './fake-client-feature-toggle-read-model';
import type { IUnleashConfig } from '../../types'; import type { IUnleashConfig } from '../../types';
import type { Db } from '../../db/db'; import type { Db } from '../../db/db';
import type ClientInstanceService from '../metrics/instance/instance-service';
export const createFrontendApiService = ( export const createFrontendApiService = (
db: Db, db: Db,
@ -24,6 +20,7 @@ export const createFrontendApiService = (
// client metrics service needs to be shared because it uses in-memory cache // client metrics service needs to be shared because it uses in-memory cache
clientMetricsServiceV2: ClientMetricsServiceV2, clientMetricsServiceV2: ClientMetricsServiceV2,
configurationRevisionService: ConfigurationRevisionService, configurationRevisionService: ConfigurationRevisionService,
clientInstanceService: ClientInstanceService,
): FrontendApiService => { ): FrontendApiService => {
const segmentReadModel = new SegmentReadModel(db); const segmentReadModel = new SegmentReadModel(db);
const settingStore = new SettingStore(db, config.getLogger); const settingStore = new SettingStore(db, config.getLogger);
@ -33,8 +30,6 @@ export const createFrontendApiService = (
config, config,
eventService, eventService,
); );
// TODO: remove this dependency after we migrate frontend API
const featureToggleService = createFeatureToggleService(db, config);
const clientFeatureToggleReadModel = new ClientFeatureToggleReadModel( const clientFeatureToggleReadModel = new ClientFeatureToggleReadModel(
db, db,
config.eventBus, config.eventBus,
@ -47,12 +42,10 @@ export const createFrontendApiService = (
); );
return new FrontendApiService( return new FrontendApiService(
config, config,
{ segmentReadModel },
{ {
featureToggleService,
clientMetricsServiceV2, clientMetricsServiceV2,
settingService, settingService,
configurationRevisionService, clientInstanceService,
}, },
globalFrontendApiCache, globalFrontendApiCache,
); );
@ -62,6 +55,7 @@ export const createFakeFrontendApiService = (
config: IUnleashConfig, config: IUnleashConfig,
clientMetricsServiceV2: ClientMetricsServiceV2, clientMetricsServiceV2: ClientMetricsServiceV2,
configurationRevisionService: ConfigurationRevisionService, configurationRevisionService: ConfigurationRevisionService,
clientInstanceService: ClientInstanceService,
): FrontendApiService => { ): FrontendApiService => {
const segmentReadModel = new FakeSegmentReadModel(); const segmentReadModel = new FakeSegmentReadModel();
const settingStore = new FakeSettingStore(); const settingStore = new FakeSettingStore();
@ -71,9 +65,6 @@ export const createFakeFrontendApiService = (
config, config,
eventService, eventService,
); );
// TODO: remove this dependency after we migrate frontend API
const featureToggleService =
createFakeFeatureToggleService(config).featureToggleService;
const clientFeatureToggleReadModel = new FakeClientFeatureToggleReadModel(); const clientFeatureToggleReadModel = new FakeClientFeatureToggleReadModel();
const globalFrontendApiCache = new GlobalFrontendApiCache( const globalFrontendApiCache = new GlobalFrontendApiCache(
config, config,
@ -83,12 +74,10 @@ export const createFakeFrontendApiService = (
); );
return new FrontendApiService( return new FrontendApiService(
config, config,
{ segmentReadModel },
{ {
featureToggleService,
clientMetricsServiceV2, clientMetricsServiceV2,
settingService, settingService,
configurationRevisionService, clientInstanceService,
}, },
globalFrontendApiCache, globalFrontendApiCache,
); );

View File

@ -4,11 +4,10 @@ import {
type IFlagResolver, type IFlagResolver,
type IUnleashConfig, type IUnleashConfig,
type IUnleashServices, type IUnleashServices,
type IUser,
NONE, NONE,
} from '../../types'; } from '../../types';
import type { Logger } from '../../logger'; import type { Logger } from '../../logger';
import ApiUser, { type IApiUser } from '../../types/api-user'; import type { IApiUser } from '../../types/api-user';
import { import {
type ClientMetricsSchema, type ClientMetricsSchema,
createRequestSchema, 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( private async registerFrontendApiMetrics(
req: ApiUserRequest<unknown, unknown, ClientMetricsSchema>, req: ApiUserRequest<unknown, unknown, ClientMetricsSchema>,
res: Response, res: Response,
@ -248,28 +240,12 @@ export default class FrontendAPIController extends Controller {
return; return;
} }
const environment = await this.services.frontendApiService.registerFrontendApiMetrics(
await this.services.frontendApiService.registerFrontendApiMetrics( req.user,
req.user, req.body,
req.body, req.ip,
req.ip, req.headers['unleash-sdk'],
); );
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); res.sendStatus(200);
} }

View File

@ -48,7 +48,6 @@ test('frontend api service fetching features from global cache', async () => {
const frontendApiService = new FrontendApiService( const frontendApiService = new FrontendApiService(
{ getLogger: noLogger, eventBus } as unknown as Config, { getLogger: noLogger, eventBus } as unknown as Config,
irrelevant, irrelevant,
irrelevant,
globalFrontendApiCache, globalFrontendApiCache,
); );

View File

@ -1,17 +1,17 @@
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import type { import type {
IAuditUser, IAuditUser,
IFlagResolver,
IUnleashConfig, IUnleashConfig,
IUnleashServices, IUnleashServices,
IUnleashStores, IUser,
} from '../../types'; } from '../../types';
import type { Logger } from '../../logger'; import type { Logger } from '../../logger';
import type { import type {
ClientMetricsSchema, ClientMetricsSchema,
FrontendApiFeatureSchema, FrontendApiFeatureSchema,
} from '../../openapi'; } from '../../openapi';
import type ApiUser from '../../types/api-user'; import ApiUser, { type IApiUser } from '../../types/api-user';
import type { IApiUser } from '../../types/api-user';
import { import {
type Context, type Context,
InMemStorageProvider, InMemStorageProvider,
@ -31,17 +31,16 @@ import type { GlobalFrontendApiCache } from './global-frontend-api-cache';
export type Config = Pick< export type Config = Pick<
IUnleashConfig, IUnleashConfig,
'getLogger' | 'frontendApi' | 'frontendApiOrigins' | 'eventBus' | 'getLogger'
| 'frontendApi'
| 'frontendApiOrigins'
| 'eventBus'
| 'flagResolver'
>; >;
export type Stores = Pick<IUnleashStores, 'segmentReadModel'>;
export type Services = Pick< export type Services = Pick<
IUnleashServices, IUnleashServices,
| 'featureToggleService' 'clientMetricsServiceV2' | 'settingService' | 'clientInstanceService'
| 'clientMetricsServiceV2'
| 'settingService'
| 'configurationRevisionService'
>; >;
export class FrontendApiService { export class FrontendApiService {
@ -49,10 +48,10 @@ export class FrontendApiService {
private readonly logger: Logger; private readonly logger: Logger;
private readonly stores: Stores;
private readonly services: Services; private readonly services: Services;
private flagResolver: IFlagResolver;
private readonly globalFrontendApiCache: GlobalFrontendApiCache; private readonly globalFrontendApiCache: GlobalFrontendApiCache;
/** /**
@ -67,14 +66,13 @@ export class FrontendApiService {
constructor( constructor(
config: Config, config: Config,
stores: Stores,
services: Services, services: Services,
globalFrontendApiCache: GlobalFrontendApiCache, globalFrontendApiCache: GlobalFrontendApiCache,
) { ) {
this.config = config; this.config = config;
this.logger = config.getLogger('services/frontend-api-service.ts'); this.logger = config.getLogger('services/frontend-api-service.ts');
this.stores = stores;
this.services = services; this.services = services;
this.flagResolver = config.flagResolver;
this.globalFrontendApiCache = globalFrontendApiCache; this.globalFrontendApiCache = globalFrontendApiCache;
} }
@ -107,11 +105,19 @@ export class FrontendApiService {
return resultDefinitions; return resultDefinitions;
} }
private resolveProject(user: IUser | IApiUser) {
if (user instanceof ApiUser) {
return user.projects;
}
return ['default'];
}
async registerFrontendApiMetrics( async registerFrontendApiMetrics(
token: IApiUser, token: IApiUser,
metrics: ClientMetricsSchema, metrics: ClientMetricsSchema,
ip: string, ip: string,
): Promise<string> { sdkVersion?: string | string[],
): Promise<void> {
FrontendApiService.assertExpectedTokenType(token); FrontendApiService.assertExpectedTokenType(token);
const environment = const environment =
@ -128,7 +134,21 @@ export class FrontendApiService {
ip, 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> { private async clientForFrontendApiToken(token: IApiUser): Promise<Unleash> {

View File

@ -9,8 +9,9 @@ import { type ISettingStore, TEST_AUDIT_USER } from '../../lib/types';
import { frontendSettingsKey } from '../../lib/types/settings/frontend-settings'; import { frontendSettingsKey } from '../../lib/types/settings/frontend-settings';
import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store'; import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store';
import { createFakeEventsService } from '../features'; 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 = ( const createSettingService = (
frontendApiOrigins: string[], frontendApiOrigins: string[],
): { frontendApiService: FrontendApiService; settingStore: ISettingStore } => { ): { frontendApiService: FrontendApiService; settingStore: ISettingStore } => {
@ -30,8 +31,11 @@ const createSettingService = (
}; };
return { return {
//@ts-ignore frontendApiService: new FrontendApiService(
frontendApiService: new FrontendApiService(config, stores, services), config,
services as Services,
{} as GlobalFrontendApiCache,
),
settingStore: stores.settingStore, settingStore: stores.settingStore,
}; };
}; };

View File

@ -358,11 +358,13 @@ export const createServices = (
config, config,
clientMetricsServiceV2, clientMetricsServiceV2,
configurationRevisionService, configurationRevisionService,
clientInstanceService,
) )
: createFakeFrontendApiService( : createFakeFrontendApiService(
config, config,
clientMetricsServiceV2, clientMetricsServiceV2,
configurationRevisionService, configurationRevisionService,
clientInstanceService,
); );
const edgeService = new EdgeService({ apiTokenService }, config); const edgeService = new EdgeService({ apiTokenService }, config);

View File

@ -12,6 +12,7 @@ import {
let app: IUnleashTest; let app: IUnleashTest;
let db: ITestDb; let db: ITestDb;
let defaultToken: IApiToken; let defaultToken: IApiToken;
let frontendToken: IApiToken;
const metrics = { const metrics = {
appName: 'appName', appName: 'appName',
@ -56,6 +57,7 @@ beforeAll(async () => {
experimental: { experimental: {
flags: { flags: {
strictSchemaValidation: true, strictSchemaValidation: true,
registerFrontendClient: true,
}, },
}, },
}, },
@ -69,6 +71,14 @@ beforeAll(async () => {
environment: 'default', environment: 'default',
tokenName: 'tester', tokenName: 'tester',
}); });
frontendToken =
await app.services.apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.FRONTEND,
projects: ['default'],
environment: 'default',
tokenName: 'tester',
});
}); });
afterEach(async () => { 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 () => { test('should show missing features and strategies', async () => {
await Promise.all([ await Promise.all([
app.createFeature('toggle-name-1'), app.createFeature('toggle-name-1'),