mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
feat: Compare old results with new frontend api (#6476)
This commit is contained in:
parent
1949d0134f
commit
8f105f9d30
@ -115,6 +115,7 @@ exports[`should create default config 1`] = `
|
||||
},
|
||||
},
|
||||
"filterInvalidClientMetrics": false,
|
||||
"globalFrontendApiCache": false,
|
||||
"googleAuthEnabled": false,
|
||||
"inMemoryScheduledChangeRequests": false,
|
||||
"increaseUnleashWidth": false,
|
||||
|
@ -4,7 +4,7 @@ import { IClientFeatureToggleReadModel } from './client-feature-toggle-read-mode
|
||||
export default class FakeClientFeatureToggleReadModel
|
||||
implements IClientFeatureToggleReadModel
|
||||
{
|
||||
constructor(private value: Record<string, IFeatureToggleClient[]>) {}
|
||||
constructor(private value: Record<string, IFeatureToggleClient[]> = {}) {}
|
||||
|
||||
getClient(): Promise<Record<string, IFeatureToggleClient[]>> {
|
||||
return Promise.resolve(this.value);
|
||||
|
@ -23,20 +23,20 @@ export class FrontendApiRepository
|
||||
|
||||
private readonly token: IApiUser;
|
||||
|
||||
private globalFrontendApiRepository: GlobalFrontendApiCache;
|
||||
private globalFrontendApiCache: GlobalFrontendApiCache;
|
||||
|
||||
private running: boolean;
|
||||
|
||||
constructor(
|
||||
config: Config,
|
||||
globalFrontendApiRepository: GlobalFrontendApiCache,
|
||||
globalFrontendApiCache: GlobalFrontendApiCache,
|
||||
token: IApiUser,
|
||||
) {
|
||||
super();
|
||||
this.config = config;
|
||||
this.logger = config.getLogger('frontend-api-repository.ts');
|
||||
this.token = token;
|
||||
this.globalFrontendApiRepository = globalFrontendApiRepository;
|
||||
this.globalFrontendApiCache = globalFrontendApiCache;
|
||||
}
|
||||
|
||||
getTogglesWithSegmentData(): EnhancedFeatureInterface[] {
|
||||
@ -45,18 +45,18 @@ export class FrontendApiRepository
|
||||
}
|
||||
|
||||
getSegment(id: number): Segment | undefined {
|
||||
return this.globalFrontendApiRepository.getSegment(id);
|
||||
return this.globalFrontendApiCache.getSegment(id);
|
||||
}
|
||||
|
||||
getToggle(name: string): FeatureInterface {
|
||||
//@ts-ignore (we must update the node SDK to allow undefined)
|
||||
return this.globalFrontendApiRepository
|
||||
.getToggles(this.token)
|
||||
.find((feature) => feature.name);
|
||||
return this.getToggles(this.token).find(
|
||||
(feature) => feature.name === name,
|
||||
);
|
||||
}
|
||||
|
||||
getToggles(): FeatureInterface[] {
|
||||
return this.globalFrontendApiRepository.getToggles(this.token);
|
||||
return this.globalFrontendApiCache.getToggles(this.token);
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
|
@ -6,7 +6,12 @@ import noLogger from '../../test/fixtures/no-logger';
|
||||
import { FakeSegmentReadModel } from '../features/segment/fake-segment-read-model';
|
||||
import FakeClientFeatureToggleReadModel from './fake-client-feature-toggle-read-model';
|
||||
import EventEmitter from 'events';
|
||||
import { IApiUser, IFeatureToggleClient, ISegment } from '../types';
|
||||
import {
|
||||
IApiUser,
|
||||
IFeatureToggleClient,
|
||||
IFlagResolver,
|
||||
ISegment,
|
||||
} from '../types';
|
||||
import { UPDATE_REVISION } from '../features/feature-toggle/configuration-revision-service';
|
||||
|
||||
const state = async (
|
||||
@ -33,11 +38,17 @@ const defaultFeature: IFeatureToggleClient = {
|
||||
};
|
||||
const defaultSegment = { name: 'segment', id: 1 } as ISegment;
|
||||
|
||||
const alwaysOnFlagResolver = {
|
||||
isEnabled() {
|
||||
return true;
|
||||
},
|
||||
} as unknown as IFlagResolver;
|
||||
|
||||
const createCache = (
|
||||
segment: ISegment = defaultSegment,
|
||||
features: Record<string, IFeatureToggleClient[]> = {},
|
||||
) => {
|
||||
const config = { getLogger: noLogger };
|
||||
const config = { getLogger: noLogger, flagResolver: alwaysOnFlagResolver };
|
||||
const segmentReadModel = new FakeSegmentReadModel([segment as ISegment]);
|
||||
const clientFeatureToggleReadModel = new FakeClientFeatureToggleReadModel(
|
||||
features,
|
||||
|
@ -13,7 +13,7 @@ import { UPDATE_REVISION } from '../features/feature-toggle/configuration-revisi
|
||||
import { mapValues } from '../util';
|
||||
import { IClientFeatureToggleReadModel } from './client-feature-toggle-read-model-type';
|
||||
|
||||
type Config = Pick<IUnleashConfig, 'getLogger'>;
|
||||
type Config = Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>;
|
||||
|
||||
export type GlobalFrontendApiCacheState = 'starting' | 'ready' | 'updated';
|
||||
|
||||
@ -103,7 +103,9 @@ export class GlobalFrontendApiCache extends EventEmitter {
|
||||
}
|
||||
|
||||
private async onUpdateRevisionEvent() {
|
||||
await this.refreshData();
|
||||
if (this.config.flagResolver.isEnabled('globalFrontendApiCache')) {
|
||||
await this.refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
private environmentNameForToken(token: IApiUser): string {
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
emptyResponse,
|
||||
getStandardResponses,
|
||||
ProxyClientSchema,
|
||||
ProxyFeatureSchema,
|
||||
proxyFeaturesSchema,
|
||||
ProxyFeaturesSchema,
|
||||
} from '../../openapi';
|
||||
@ -20,6 +21,7 @@ import NotImplementedError from '../../error/not-implemented-error';
|
||||
import NotFoundError from '../../error/notfound-error';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import { minutesToMilliseconds } from 'date-fns';
|
||||
import isEqual from 'lodash.isequal';
|
||||
|
||||
interface ApiUserRequest<
|
||||
PARAM = any,
|
||||
@ -173,10 +175,32 @@ export default class FrontendAPIController extends Controller {
|
||||
if (!this.config.flagResolver.isEnabled('embedProxy')) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
const toggles = await this.services.proxyService.getProxyFeatures(
|
||||
req.user,
|
||||
FrontendAPIController.createContext(req),
|
||||
);
|
||||
let toggles: ProxyFeatureSchema[];
|
||||
let newToggles: ProxyFeatureSchema[];
|
||||
if (this.config.flagResolver.isEnabled('globalFrontendApiCache')) {
|
||||
[toggles, newToggles] = await Promise.all([
|
||||
this.services.proxyService.getProxyFeatures(
|
||||
req.user,
|
||||
FrontendAPIController.createContext(req),
|
||||
),
|
||||
this.services.proxyService.getNewProxyFeatures(
|
||||
req.user,
|
||||
FrontendAPIController.createContext(req),
|
||||
),
|
||||
]);
|
||||
if (!isEqual(toggles, newToggles)) {
|
||||
this.logger.warn(
|
||||
'old features and new feature are different',
|
||||
toggles.length,
|
||||
newToggles.length,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
toggles = await this.services.proxyService.getProxyFeatures(
|
||||
req.user,
|
||||
FrontendAPIController.createContext(req),
|
||||
);
|
||||
}
|
||||
|
||||
res.set('Cache-control', 'no-cache');
|
||||
|
||||
|
@ -111,6 +111,9 @@ import {
|
||||
import { InactiveUsersService } from '../users/inactive/inactive-users-service';
|
||||
import { SegmentReadModel } from '../features/segment/segment-read-model';
|
||||
import { FakeSegmentReadModel } from '../features/segment/fake-segment-read-model';
|
||||
import { GlobalFrontendApiCache } from '../proxy/global-frontend-api-cache';
|
||||
import ClientFeatureToggleReadModel from '../proxy/client-feature-toggle-read-model';
|
||||
import FakeClientFeatureToggleReadModel from '../proxy/fake-client-feature-toggle-read-model';
|
||||
|
||||
export const createServices = (
|
||||
stores: IUnleashStores,
|
||||
@ -293,12 +296,27 @@ export const createServices = (
|
||||
? createClientFeatureToggleService(db, config)
|
||||
: createFakeClientFeatureToggleService(config);
|
||||
|
||||
const proxyService = new ProxyService(config, stores, {
|
||||
featureToggleServiceV2,
|
||||
clientMetricsServiceV2,
|
||||
settingService,
|
||||
const clientFeatureToggleReadModel = db
|
||||
? new ClientFeatureToggleReadModel(db, config.eventBus)
|
||||
: new FakeClientFeatureToggleReadModel();
|
||||
const globalFrontendApiCache = new GlobalFrontendApiCache(
|
||||
config,
|
||||
segmentReadModel,
|
||||
clientFeatureToggleReadModel,
|
||||
configurationRevisionService,
|
||||
});
|
||||
);
|
||||
|
||||
const proxyService = new ProxyService(
|
||||
config,
|
||||
stores,
|
||||
{
|
||||
featureToggleServiceV2,
|
||||
clientMetricsServiceV2,
|
||||
settingService,
|
||||
configurationRevisionService,
|
||||
},
|
||||
globalFrontendApiCache,
|
||||
);
|
||||
|
||||
const edgeService = new EdgeService({ apiTokenService }, config);
|
||||
|
||||
|
54
src/lib/services/proxy-service.test.ts
Normal file
54
src/lib/services/proxy-service.test.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { ProxyService, Config } from './proxy-service';
|
||||
import { GlobalFrontendApiCache } from '../proxy/global-frontend-api-cache';
|
||||
import { IApiUser } from '../types';
|
||||
import { FeatureInterface } from 'unleash-client/lib/feature';
|
||||
import noLogger from '../../test/fixtures/no-logger';
|
||||
import { ApiTokenType } from '../types/models/api-token';
|
||||
|
||||
test('proxy service fetching features from global cache', async () => {
|
||||
const irrelevant = {} as any;
|
||||
const globalFrontendApiCache = {
|
||||
getToggles(_: IApiUser): FeatureInterface[] {
|
||||
return [
|
||||
{
|
||||
name: 'toggleA',
|
||||
enabled: true,
|
||||
project: 'projectA',
|
||||
type: 'release',
|
||||
variants: [],
|
||||
strategies: [
|
||||
{ name: 'default', parameters: [], constraints: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'toggleB',
|
||||
enabled: false,
|
||||
project: 'projectA',
|
||||
type: 'release',
|
||||
variants: [],
|
||||
strategies: [
|
||||
{ name: 'default', parameters: [], constraints: [] },
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
} as GlobalFrontendApiCache;
|
||||
const proxyService = new ProxyService(
|
||||
{ getLogger: noLogger } as unknown as Config,
|
||||
irrelevant,
|
||||
irrelevant,
|
||||
globalFrontendApiCache,
|
||||
);
|
||||
|
||||
const features = await proxyService.getNewProxyFeatures(
|
||||
{
|
||||
projects: ['irrelevant'],
|
||||
environment: 'irrelevant',
|
||||
type: ApiTokenType.FRONTEND,
|
||||
} as unknown as IApiUser,
|
||||
{},
|
||||
);
|
||||
|
||||
expect(features).toMatchObject([{ name: 'toggleA' }]);
|
||||
expect(features).toHaveLength(1);
|
||||
});
|
@ -17,18 +17,20 @@ import { validateOrigins } from '../util';
|
||||
import { BadDataError, InvalidTokenError } from '../error';
|
||||
import { PROXY_REPOSITORY_CREATED } from '../metric-events';
|
||||
import { ProxyRepository } from '../proxy';
|
||||
import { FrontendApiRepository } from '../proxy/frontend-api-repository';
|
||||
import { GlobalFrontendApiCache } from '../proxy/global-frontend-api-cache';
|
||||
|
||||
type Config = Pick<
|
||||
export type Config = Pick<
|
||||
IUnleashConfig,
|
||||
'getLogger' | 'frontendApi' | 'frontendApiOrigins' | 'eventBus'
|
||||
>;
|
||||
|
||||
type Stores = Pick<
|
||||
export type Stores = Pick<
|
||||
IUnleashStores,
|
||||
'projectStore' | 'eventStore' | 'segmentReadModel'
|
||||
>;
|
||||
|
||||
type Services = Pick<
|
||||
export type Services = Pick<
|
||||
IUnleashServices,
|
||||
| 'featureToggleServiceV2'
|
||||
| 'clientMetricsServiceV2'
|
||||
@ -45,21 +47,31 @@ export class ProxyService {
|
||||
|
||||
private readonly services: Services;
|
||||
|
||||
private readonly globalFrontendApiCache: GlobalFrontendApiCache;
|
||||
|
||||
/**
|
||||
* This is intentionally a Promise becasue we want to be able to await
|
||||
* This is intentionally a Promise because we want to be able to await
|
||||
* until the client (which might be being created by a different request) is ready
|
||||
* Check this test that fails if we don't use a Promise: src/test/e2e/api/proxy/proxy.concurrency.e2e.test.ts
|
||||
*/
|
||||
private readonly clients: Map<ApiUser['secret'], Promise<Unleash>> =
|
||||
new Map();
|
||||
private readonly newClients: Map<ApiUser['secret'], Promise<Unleash>> =
|
||||
new Map();
|
||||
|
||||
private cachedFrontendSettings?: FrontendSettings;
|
||||
|
||||
constructor(config: Config, stores: Stores, services: Services) {
|
||||
constructor(
|
||||
config: Config,
|
||||
stores: Stores,
|
||||
services: Services,
|
||||
globalFrontendApiCache: GlobalFrontendApiCache,
|
||||
) {
|
||||
this.config = config;
|
||||
this.logger = config.getLogger('services/proxy-service.ts');
|
||||
this.stores = stores;
|
||||
this.services = services;
|
||||
this.globalFrontendApiCache = globalFrontendApiCache;
|
||||
}
|
||||
|
||||
async getProxyFeatures(
|
||||
@ -85,6 +97,33 @@ export class ProxyService {
|
||||
}));
|
||||
}
|
||||
|
||||
async getNewProxyFeatures(
|
||||
token: IApiUser,
|
||||
context: Context,
|
||||
): Promise<ProxyFeatureSchema[]> {
|
||||
const client = await this.newClientForProxyToken(token);
|
||||
const definitions = client.getFeatureToggleDefinitions() || [];
|
||||
const sessionId = context.sessionId || String(Math.random());
|
||||
|
||||
return definitions
|
||||
.filter((feature) => {
|
||||
const enabled = client.isEnabled(feature.name, {
|
||||
...context,
|
||||
sessionId,
|
||||
});
|
||||
return enabled;
|
||||
})
|
||||
.map((feature) => ({
|
||||
name: feature.name,
|
||||
enabled: Boolean(feature.enabled),
|
||||
variant: client.getVariant(feature.name, {
|
||||
...context,
|
||||
sessionId,
|
||||
}),
|
||||
impressionData: Boolean(feature.impressionData),
|
||||
}));
|
||||
}
|
||||
|
||||
async registerProxyMetrics(
|
||||
token: IApiUser,
|
||||
metrics: ClientMetricsSchema,
|
||||
@ -117,6 +156,20 @@ export class ProxyService {
|
||||
return client;
|
||||
}
|
||||
|
||||
private async newClientForProxyToken(token: IApiUser): Promise<Unleash> {
|
||||
ProxyService.assertExpectedTokenType(token);
|
||||
|
||||
let newClient = this.newClients.get(token.secret);
|
||||
if (!newClient) {
|
||||
newClient = this.createNewClientForProxyToken(token);
|
||||
this.newClients.set(token.secret, newClient);
|
||||
// TODO: do we need this twice?
|
||||
// this.config.eventBus.emit(PROXY_REPOSITORY_CREATED);
|
||||
}
|
||||
|
||||
return newClient;
|
||||
}
|
||||
|
||||
private async createClientForProxyToken(token: IApiUser): Promise<Unleash> {
|
||||
const repository = new ProxyRepository(
|
||||
this.config,
|
||||
@ -143,6 +196,33 @@ export class ProxyService {
|
||||
return client;
|
||||
}
|
||||
|
||||
private async createNewClientForProxyToken(
|
||||
token: IApiUser,
|
||||
): Promise<Unleash> {
|
||||
const repository = new FrontendApiRepository(
|
||||
this.config,
|
||||
this.globalFrontendApiCache,
|
||||
token,
|
||||
);
|
||||
const client = new Unleash({
|
||||
appName: 'proxy',
|
||||
url: 'unused',
|
||||
storageProvider: new InMemStorageProvider(),
|
||||
disableMetrics: true,
|
||||
repository,
|
||||
disableAutoStart: true,
|
||||
skipInstanceCountWarning: true,
|
||||
});
|
||||
|
||||
client.on(UnleashEvents.Error, (error) => {
|
||||
this.logger.error('We found an event error', error);
|
||||
});
|
||||
|
||||
await client.start();
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
async deleteClientForProxyToken(secret: string): Promise<void> {
|
||||
const clientPromise = this.clients.get(secret);
|
||||
if (clientPromise) {
|
||||
|
@ -53,7 +53,8 @@ export type IFlagKey =
|
||||
| 'sdkReporting'
|
||||
| 'responseTimeMetricsFix'
|
||||
| 'scimApi'
|
||||
| 'displayEdgeBanner';
|
||||
| 'displayEdgeBanner'
|
||||
| 'globalFrontendApiCache';
|
||||
|
||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||
|
||||
@ -262,6 +263,10 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_RESPONSE_TIME_METRICS_FIX,
|
||||
false,
|
||||
),
|
||||
globalFrontendApiCache: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_GLOBAL_FRONTEND_API_CACHE,
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||
|
@ -50,6 +50,7 @@ process.nextTick(async () => {
|
||||
executiveDashboard: true,
|
||||
userAccessUIEnabled: true,
|
||||
sdkReporting: true,
|
||||
globalFrontendApiCache: true,
|
||||
},
|
||||
},
|
||||
authentication: {
|
||||
|
Loading…
Reference in New Issue
Block a user