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:
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 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,
|
||||||
);
|
);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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> {
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
|
@ -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'),
|
||||||
|
Loading…
Reference in New Issue
Block a user