mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
chore: remove new frontend api feature flag (#6906)
The flag has been 100% for a bit now, we need to prepare for GA.
This commit is contained in:
parent
bf3366434c
commit
d578deab7f
@ -121,7 +121,6 @@ exports[`should create default config 1`] = `
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"filterInvalidClientMetrics": false,
|
"filterInvalidClientMetrics": false,
|
||||||
"globalFrontendApiCache": false,
|
|
||||||
"googleAuthEnabled": false,
|
"googleAuthEnabled": false,
|
||||||
"inMemoryScheduledChangeRequests": false,
|
"inMemoryScheduledChangeRequests": false,
|
||||||
"maintenanceMode": false,
|
"maintenanceMode": false,
|
||||||
@ -144,7 +143,6 @@ exports[`should create default config 1`] = `
|
|||||||
"queryMissingTokens": false,
|
"queryMissingTokens": false,
|
||||||
"responseTimeMetricsFix": false,
|
"responseTimeMetricsFix": false,
|
||||||
"responseTimeWithAppNameKillSwitch": false,
|
"responseTimeWithAppNameKillSwitch": false,
|
||||||
"returnGlobalFrontendApiCache": false,
|
|
||||||
"scimApi": false,
|
"scimApi": false,
|
||||||
"showInactiveUsers": false,
|
"showInactiveUsers": false,
|
||||||
"signals": false,
|
"signals": false,
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
emptyResponse,
|
emptyResponse,
|
||||||
getStandardResponses,
|
getStandardResponses,
|
||||||
type FrontendApiClientSchema,
|
type FrontendApiClientSchema,
|
||||||
type FrontendApiFeatureSchema,
|
|
||||||
frontendApiFeaturesSchema,
|
frontendApiFeaturesSchema,
|
||||||
type FrontendApiFeaturesSchema,
|
type FrontendApiFeaturesSchema,
|
||||||
} from '../../openapi';
|
} from '../../openapi';
|
||||||
@ -21,8 +20,6 @@ import NotImplementedError from '../../error/not-implemented-error';
|
|||||||
import NotFoundError from '../../error/notfound-error';
|
import NotFoundError from '../../error/notfound-error';
|
||||||
import rateLimit from 'express-rate-limit';
|
import rateLimit from 'express-rate-limit';
|
||||||
import { minutesToMilliseconds } from 'date-fns';
|
import { minutesToMilliseconds } from 'date-fns';
|
||||||
import isEqual from 'lodash.isequal';
|
|
||||||
import { diff } from 'json-diff';
|
|
||||||
import metricsHelper from '../../util/metrics-helper';
|
import metricsHelper from '../../util/metrics-helper';
|
||||||
import { FUNCTION_TIME } from '../../metric-events';
|
import { FUNCTION_TIME } from '../../metric-events';
|
||||||
|
|
||||||
@ -186,48 +183,11 @@ export default class FrontendAPIController extends Controller {
|
|||||||
if (!this.config.flagResolver.isEnabled('embedProxy')) {
|
if (!this.config.flagResolver.isEnabled('embedProxy')) {
|
||||||
throw new NotFoundError();
|
throw new NotFoundError();
|
||||||
}
|
}
|
||||||
let toggles: FrontendApiFeatureSchema[];
|
const toggles =
|
||||||
let newToggles: FrontendApiFeatureSchema[] = [];
|
await this.services.frontendApiService.getFrontendApiFeatures(
|
||||||
if (this.config.flagResolver.isEnabled('globalFrontendApiCache')) {
|
req.user,
|
||||||
const context = FrontendAPIController.createContext(req);
|
FrontendAPIController.createContext(req),
|
||||||
[toggles, newToggles] = await Promise.all([
|
|
||||||
this.getTimedFrontendApiFeatures(req, context),
|
|
||||||
this.getTimedNewFrontendApiFeatures(req, context),
|
|
||||||
]);
|
|
||||||
const sortedToggles = toggles.sort((a, b) =>
|
|
||||||
a.name.localeCompare(b.name),
|
|
||||||
);
|
);
|
||||||
const sortedNewToggles = newToggles.sort((a, b) =>
|
|
||||||
a.name.localeCompare(b.name),
|
|
||||||
);
|
|
||||||
if (!isEqual(sortedToggles, sortedNewToggles)) {
|
|
||||||
this.logger.warn(
|
|
||||||
`old features and new features are different. Old count ${
|
|
||||||
toggles.length
|
|
||||||
}, new count ${newToggles.length}, projects ${
|
|
||||||
req.user.projects
|
|
||||||
}, environment ${
|
|
||||||
req.user.environment
|
|
||||||
}, diff ${JSON.stringify(
|
|
||||||
diff(sortedToggles, sortedNewToggles),
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
this.config.flagResolver.isEnabled('returnGlobalFrontendApiCache')
|
|
||||||
) {
|
|
||||||
toggles =
|
|
||||||
await this.services.frontendApiService.getNewFrontendApiFeatures(
|
|
||||||
req.user,
|
|
||||||
FrontendAPIController.createContext(req),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
toggles =
|
|
||||||
await this.services.frontendApiService.getFrontendApiFeatures(
|
|
||||||
req.user,
|
|
||||||
FrontendAPIController.createContext(req),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.set('Cache-control', 'no-cache');
|
res.set('Cache-control', 'no-cache');
|
||||||
|
|
||||||
@ -239,28 +199,6 @@ export default class FrontendAPIController extends Controller {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getTimedFrontendApiFeatures(req, context) {
|
|
||||||
const stopTimer = this.timer('getFrontendApiFeatures');
|
|
||||||
const features =
|
|
||||||
await this.services.frontendApiService.getFrontendApiFeatures(
|
|
||||||
req.user,
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
stopTimer();
|
|
||||||
return features;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getTimedNewFrontendApiFeatures(req, context) {
|
|
||||||
const stopTimer = this.timer('getNewFrontendApiFeatures');
|
|
||||||
const features =
|
|
||||||
await this.services.frontendApiService.getNewFrontendApiFeatures(
|
|
||||||
req.user,
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
stopTimer();
|
|
||||||
return features;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async registerFrontendApiMetrics(
|
private async registerFrontendApiMetrics(
|
||||||
req: ApiUserRequest<unknown, unknown, ClientMetricsSchema>,
|
req: ApiUserRequest<unknown, unknown, ClientMetricsSchema>,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
@ -49,7 +49,6 @@ export class FrontendApiRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
getToggle(name: string): FeatureInterface {
|
getToggle(name: string): FeatureInterface {
|
||||||
//@ts-ignore (we must update the node SDK to allow undefined)
|
|
||||||
return this.globalFrontendApiCache.getToggle(name, this.token);
|
return this.globalFrontendApiCache.getToggle(name, this.token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,155 +0,0 @@
|
|||||||
import {
|
|
||||||
type IApiUser,
|
|
||||||
type IUnleashConfig,
|
|
||||||
type IUnleashStores,
|
|
||||||
TEST_AUDIT_USER,
|
|
||||||
} from '../../types';
|
|
||||||
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
|
|
||||||
import type { FrontendApiService } from './frontend-api-service';
|
|
||||||
import { createFrontendApiService } from './createFrontendApiService';
|
|
||||||
import { createLastSeenService } from '../metrics/last-seen/createLastSeenService';
|
|
||||||
import ClientMetricsServiceV2 from '../metrics/client-metrics/metrics-service-v2';
|
|
||||||
import ConfigurationRevisionService from '../feature-toggle/configuration-revision-service';
|
|
||||||
import getLogger from '../../../test/fixtures/no-logger';
|
|
||||||
import { createTestConfig } from '../../../test/config/test-config';
|
|
||||||
import { ApiTokenType } from '../../types/models/api-token';
|
|
||||||
import type FeatureToggleService from '../feature-toggle/feature-toggle-service';
|
|
||||||
import { createFeatureToggleService } from '../feature-toggle/createFeatureToggleService';
|
|
||||||
import {
|
|
||||||
FRONTEND_API_REPOSITORY_CREATED,
|
|
||||||
PROXY_REPOSITORY_CREATED,
|
|
||||||
} from '../../metric-events';
|
|
||||||
|
|
||||||
let stores: IUnleashStores;
|
|
||||||
let db: ITestDb;
|
|
||||||
let frontendApiService: FrontendApiService;
|
|
||||||
let featureToggleService: FeatureToggleService;
|
|
||||||
let configurationRevisionService: ConfigurationRevisionService;
|
|
||||||
let config: IUnleashConfig;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
db = await dbInit('frontend_api_service', getLogger);
|
|
||||||
stores = db.stores;
|
|
||||||
config = createTestConfig({
|
|
||||||
experimental: {
|
|
||||||
flags: {
|
|
||||||
globalFrontendApiCache: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const lastSeenService = createLastSeenService(db.rawDatabase, config);
|
|
||||||
const clientMetricsServiceV2 = new ClientMetricsServiceV2(
|
|
||||||
stores,
|
|
||||||
config,
|
|
||||||
lastSeenService,
|
|
||||||
);
|
|
||||||
configurationRevisionService = ConfigurationRevisionService.getInstance(
|
|
||||||
stores,
|
|
||||||
config,
|
|
||||||
);
|
|
||||||
frontendApiService = createFrontendApiService(
|
|
||||||
db.rawDatabase,
|
|
||||||
config,
|
|
||||||
clientMetricsServiceV2,
|
|
||||||
configurationRevisionService,
|
|
||||||
);
|
|
||||||
featureToggleService = createFeatureToggleService(db.rawDatabase, config);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await db.destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
const createProject = async (project: string) => {
|
|
||||||
await stores.projectStore.create({
|
|
||||||
name: project,
|
|
||||||
description: '',
|
|
||||||
id: project,
|
|
||||||
});
|
|
||||||
await stores.projectStore.addEnvironmentToProject(project, 'development');
|
|
||||||
await stores.projectStore.addEnvironmentToProject(project, 'production');
|
|
||||||
};
|
|
||||||
|
|
||||||
const createEnvironment = async (environment: string) => {
|
|
||||||
await stores.environmentStore.create({ name: environment, type: 'test' });
|
|
||||||
};
|
|
||||||
|
|
||||||
const createFeature = async (project: string, featureName: string) => {
|
|
||||||
await featureToggleService.createFeatureToggle(
|
|
||||||
project,
|
|
||||||
{ name: featureName, description: '' },
|
|
||||||
TEST_AUDIT_USER,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const enableFeature = async (
|
|
||||||
project: string,
|
|
||||||
featureName: string,
|
|
||||||
environment: string,
|
|
||||||
) => {
|
|
||||||
await featureToggleService.unprotectedUpdateEnabled(
|
|
||||||
project,
|
|
||||||
featureName,
|
|
||||||
environment,
|
|
||||||
true,
|
|
||||||
TEST_AUDIT_USER,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
test('Compare Frontend API implementations', async () => {
|
|
||||||
const projectA = 'projectA';
|
|
||||||
const projectB = 'projectB';
|
|
||||||
|
|
||||||
await createEnvironment('development');
|
|
||||||
await createEnvironment('production');
|
|
||||||
|
|
||||||
await createProject(projectA);
|
|
||||||
await createProject(projectB);
|
|
||||||
|
|
||||||
await createFeature(projectA, 'featureA'); // include
|
|
||||||
await createFeature(projectA, 'featureB'); // another env
|
|
||||||
await createFeature(projectA, 'featureC'); // not enabled
|
|
||||||
await createFeature(projectA, 'featureD'); // include
|
|
||||||
await enableFeature(projectA, 'featureA', 'development');
|
|
||||||
await enableFeature(projectA, 'featureD', 'development');
|
|
||||||
await enableFeature(projectA, 'featureB', 'production');
|
|
||||||
|
|
||||||
await createFeature(projectB, 'featureE'); // another project
|
|
||||||
await enableFeature(projectB, 'featureE', 'development');
|
|
||||||
|
|
||||||
await configurationRevisionService.updateMaxRevisionId();
|
|
||||||
|
|
||||||
let proxyRepositoriesCount = 0;
|
|
||||||
config.eventBus.on(PROXY_REPOSITORY_CREATED, () => {
|
|
||||||
proxyRepositoriesCount++;
|
|
||||||
});
|
|
||||||
|
|
||||||
let frontendRepositoriesCount = 0;
|
|
||||||
config.eventBus.on(FRONTEND_API_REPOSITORY_CREATED, () => {
|
|
||||||
frontendRepositoriesCount++;
|
|
||||||
});
|
|
||||||
|
|
||||||
const oldFeatures = await frontendApiService.getFrontendApiFeatures(
|
|
||||||
{
|
|
||||||
projects: [projectA],
|
|
||||||
environment: 'development',
|
|
||||||
type: ApiTokenType.FRONTEND,
|
|
||||||
} as IApiUser,
|
|
||||||
{ sessionId: '1234' },
|
|
||||||
);
|
|
||||||
|
|
||||||
const newFeatures = await frontendApiService.getNewFrontendApiFeatures(
|
|
||||||
{
|
|
||||||
projects: [projectA],
|
|
||||||
environment: 'development',
|
|
||||||
type: ApiTokenType.FRONTEND,
|
|
||||||
} as IApiUser,
|
|
||||||
{ sessionId: '1234' },
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(proxyRepositoriesCount).toBe(1);
|
|
||||||
expect(frontendRepositoriesCount).toBe(1);
|
|
||||||
expect(oldFeatures).toEqual(newFeatures);
|
|
||||||
expect(newFeatures.length).toBe(2);
|
|
||||||
});
|
|
@ -52,7 +52,7 @@ test('frontend api service fetching features from global cache', async () => {
|
|||||||
globalFrontendApiCache,
|
globalFrontendApiCache,
|
||||||
);
|
);
|
||||||
|
|
||||||
const features = await frontendApiService.getNewFrontendApiFeatures(
|
const features = await frontendApiService.getFrontendApiFeatures(
|
||||||
{
|
{
|
||||||
projects: ['irrelevant'],
|
projects: ['irrelevant'],
|
||||||
environment: 'irrelevant',
|
environment: 'irrelevant',
|
||||||
|
@ -25,13 +25,9 @@ import {
|
|||||||
} from '../../types/settings/frontend-settings';
|
} from '../../types/settings/frontend-settings';
|
||||||
import { validateOrigins } from '../../util';
|
import { validateOrigins } from '../../util';
|
||||||
import { BadDataError, InvalidTokenError } from '../../error';
|
import { BadDataError, InvalidTokenError } from '../../error';
|
||||||
import {
|
import { FRONTEND_API_REPOSITORY_CREATED } from '../../metric-events';
|
||||||
FRONTEND_API_REPOSITORY_CREATED,
|
|
||||||
PROXY_REPOSITORY_CREATED,
|
|
||||||
} from '../../metric-events';
|
|
||||||
import { FrontendApiRepository } from './frontend-api-repository';
|
import { FrontendApiRepository } from './frontend-api-repository';
|
||||||
import type { GlobalFrontendApiCache } from './global-frontend-api-cache';
|
import type { GlobalFrontendApiCache } from './global-frontend-api-cache';
|
||||||
import { ProxyRepository } from './proxy-repository';
|
|
||||||
|
|
||||||
export type Config = Pick<
|
export type Config = Pick<
|
||||||
IUnleashConfig,
|
IUnleashConfig,
|
||||||
@ -66,8 +62,6 @@ export class FrontendApiService {
|
|||||||
*/
|
*/
|
||||||
private readonly clients: Map<ApiUser['secret'], Promise<Unleash>> =
|
private readonly clients: Map<ApiUser['secret'], Promise<Unleash>> =
|
||||||
new Map();
|
new Map();
|
||||||
private readonly newClients: Map<ApiUser['secret'], Promise<Unleash>> =
|
|
||||||
new Map();
|
|
||||||
|
|
||||||
private cachedFrontendSettings?: FrontendSettings;
|
private cachedFrontendSettings?: FrontendSettings;
|
||||||
|
|
||||||
@ -93,34 +87,6 @@ export class FrontendApiService {
|
|||||||
const sessionId =
|
const sessionId =
|
||||||
context.sessionId || crypto.randomBytes(18).toString('hex');
|
context.sessionId || crypto.randomBytes(18).toString('hex');
|
||||||
|
|
||||||
const resultDefinitions = definitions
|
|
||||||
.filter((feature) =>
|
|
||||||
client.isEnabled(feature.name, {
|
|
||||||
...context,
|
|
||||||
sessionId,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.map((feature) => ({
|
|
||||||
name: feature.name,
|
|
||||||
enabled: Boolean(feature.enabled),
|
|
||||||
variant: client.getVariant(feature.name, {
|
|
||||||
...context,
|
|
||||||
sessionId,
|
|
||||||
}),
|
|
||||||
impressionData: Boolean(feature.impressionData),
|
|
||||||
}));
|
|
||||||
return resultDefinitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getNewFrontendApiFeatures(
|
|
||||||
token: IApiUser,
|
|
||||||
context: Context,
|
|
||||||
): Promise<FrontendApiFeatureSchema[]> {
|
|
||||||
const client = await this.newClientForFrontendApiToken(token);
|
|
||||||
const definitions = client.getFeatureToggleDefinitions() || [];
|
|
||||||
const sessionId =
|
|
||||||
context.sessionId || crypto.randomBytes(18).toString('hex');
|
|
||||||
|
|
||||||
const resultDefinitions = definitions
|
const resultDefinitions = definitions
|
||||||
.filter((feature) => {
|
.filter((feature) => {
|
||||||
const enabled = client.isEnabled(feature.name, {
|
const enabled = client.isEnabled(feature.name, {
|
||||||
@ -170,56 +136,13 @@ export class FrontendApiService {
|
|||||||
if (!client) {
|
if (!client) {
|
||||||
client = this.createClientForFrontendApiToken(token);
|
client = this.createClientForFrontendApiToken(token);
|
||||||
this.clients.set(token.secret, client);
|
this.clients.set(token.secret, client);
|
||||||
this.config.eventBus.emit(PROXY_REPOSITORY_CREATED);
|
|
||||||
}
|
|
||||||
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async newClientForFrontendApiToken(
|
|
||||||
token: IApiUser,
|
|
||||||
): Promise<Unleash> {
|
|
||||||
FrontendApiService.assertExpectedTokenType(token);
|
|
||||||
|
|
||||||
let newClient = this.newClients.get(token.secret);
|
|
||||||
if (!newClient) {
|
|
||||||
newClient = this.createNewClientForFrontendApiToken(token);
|
|
||||||
this.newClients.set(token.secret, newClient);
|
|
||||||
this.config.eventBus.emit(FRONTEND_API_REPOSITORY_CREATED);
|
this.config.eventBus.emit(FRONTEND_API_REPOSITORY_CREATED);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createClientForFrontendApiToken(
|
|
||||||
token: IApiUser,
|
|
||||||
): Promise<Unleash> {
|
|
||||||
const repository = new ProxyRepository(
|
|
||||||
this.config,
|
|
||||||
this.stores,
|
|
||||||
this.services,
|
|
||||||
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;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createNewClientForFrontendApiToken(
|
private async createClientForFrontendApiToken(
|
||||||
token: IApiUser,
|
token: IApiUser,
|
||||||
): Promise<Unleash> {
|
): Promise<Unleash> {
|
||||||
const repository = new FrontendApiRepository(
|
const repository = new FrontendApiRepository(
|
||||||
@ -259,6 +182,10 @@ export class FrontendApiService {
|
|||||||
this.clients.forEach((promise) => promise.then((c) => c.destroy()));
|
this.clients.forEach((promise) => promise.then((c) => c.destroy()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshData(): Promise<void> {
|
||||||
|
return this.globalFrontendApiCache.refreshData();
|
||||||
|
}
|
||||||
|
|
||||||
private static assertExpectedTokenType({ type }: IApiUser) {
|
private static assertExpectedTokenType({ type }: IApiUser) {
|
||||||
if (!(type === ApiTokenType.FRONTEND || type === ApiTokenType.ADMIN)) {
|
if (!(type === ApiTokenType.FRONTEND || type === ApiTokenType.ADMIN)) {
|
||||||
throw new InvalidTokenError();
|
throw new InvalidTokenError();
|
||||||
|
@ -12,18 +12,16 @@ import {
|
|||||||
} from '../../types/models/api-token';
|
} from '../../types/models/api-token';
|
||||||
import { startOfHour } from 'date-fns';
|
import { startOfHour } from 'date-fns';
|
||||||
import {
|
import {
|
||||||
FEATURE_UPDATED,
|
|
||||||
type IConstraint,
|
type IConstraint,
|
||||||
type IStrategyConfig,
|
type IStrategyConfig,
|
||||||
SYSTEM_USER_AUDIT,
|
SYSTEM_USER_AUDIT,
|
||||||
TEST_AUDIT_USER,
|
TEST_AUDIT_USER,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import { ProxyRepository } from './index';
|
import type { FrontendApiService } from './frontend-api-service';
|
||||||
import type { Logger } from '../../logger';
|
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
const TEST_USER_ID = -9999;
|
let frontendApiService: FrontendApiService;
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('frontend_api', getLogger);
|
db = await dbInit('frontend_api', getLogger);
|
||||||
app = await setupAppWithAuth(
|
app = await setupAppWithAuth(
|
||||||
@ -33,6 +31,7 @@ beforeAll(async () => {
|
|||||||
},
|
},
|
||||||
db.rawDatabase,
|
db.rawDatabase,
|
||||||
);
|
);
|
||||||
|
frontendApiService = app.services.frontendApiService;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -161,6 +160,7 @@ test('should allow requests with a token secret alias', async () => {
|
|||||||
alias: randomId(),
|
alias: randomId(),
|
||||||
environment: envB,
|
environment: envB,
|
||||||
});
|
});
|
||||||
|
await frontendApiService.refreshData();
|
||||||
await app.request
|
await app.request
|
||||||
.get('/api/frontend')
|
.get('/api/frontend')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
@ -226,6 +226,7 @@ test('should allow requests with an admin token', async () => {
|
|||||||
projects: ['*'],
|
projects: ['*'],
|
||||||
environment: '*',
|
environment: '*',
|
||||||
});
|
});
|
||||||
|
await frontendApiService.refreshData();
|
||||||
await app.request
|
await app.request
|
||||||
.get('/api/frontend')
|
.get('/api/frontend')
|
||||||
.set('Authorization', adminToken.secret)
|
.set('Authorization', adminToken.secret)
|
||||||
@ -264,6 +265,7 @@ test('should not allow requests with an invalid frontend token', async () => {
|
|||||||
|
|
||||||
test('should allow requests with a frontend token', async () => {
|
test('should allow requests with a frontend token', async () => {
|
||||||
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
|
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
|
||||||
|
await frontendApiService.refreshData();
|
||||||
await app.request
|
await app.request
|
||||||
.get('/api/frontend')
|
.get('/api/frontend')
|
||||||
.set('Authorization', frontendToken.secret)
|
.set('Authorization', frontendToken.secret)
|
||||||
@ -441,6 +443,7 @@ test('should filter features by enabled/disabled', async () => {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
strategies: [{ name: 'default', constraints: [], parameters: {} }],
|
strategies: [{ name: 'default', constraints: [], parameters: {} }],
|
||||||
});
|
});
|
||||||
|
await frontendApiService.refreshData();
|
||||||
await app.request
|
await app.request
|
||||||
.get('/api/frontend')
|
.get('/api/frontend')
|
||||||
.set('Authorization', frontendToken.secret)
|
.set('Authorization', frontendToken.secret)
|
||||||
@ -488,6 +491,7 @@ test('should filter features by strategies', async () => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
strategies: [{ name: 'default', constraints: [], parameters: {} }],
|
strategies: [{ name: 'default', constraints: [], parameters: {} }],
|
||||||
});
|
});
|
||||||
|
await frontendApiService.refreshData();
|
||||||
await app.request
|
await app.request
|
||||||
.get('/api/frontend')
|
.get('/api/frontend')
|
||||||
.set('Authorization', frontendToken.secret)
|
.set('Authorization', frontendToken.secret)
|
||||||
@ -544,6 +548,7 @@ test('should filter features by constraints', async () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
await frontendApiService.refreshData();
|
||||||
await app.request
|
await app.request
|
||||||
.get('/api/frontend?appName=a')
|
.get('/api/frontend?appName=a')
|
||||||
.set('Authorization', frontendToken.secret)
|
.set('Authorization', frontendToken.secret)
|
||||||
@ -585,6 +590,7 @@ test('should be able to set environment as a context variable', async () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await frontendApiService.refreshData();
|
||||||
await app.request
|
await app.request
|
||||||
.get('/api/frontend?environment=staging')
|
.get('/api/frontend?environment=staging')
|
||||||
.set('Authorization', frontendToken.secret)
|
.set('Authorization', frontendToken.secret)
|
||||||
@ -604,7 +610,6 @@ test('should be able to set environment as a context variable', async () => {
|
|||||||
expect(res.body.toggles).toHaveLength(0);
|
expect(res.body.toggles).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should filter features by project', async () => {
|
test('should filter features by project', async () => {
|
||||||
const projectA = 'projectA';
|
const projectA = 'projectA';
|
||||||
const projectB = 'projectB';
|
const projectB = 'projectB';
|
||||||
@ -636,6 +641,7 @@ test('should filter features by project', async () => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
strategies: [{ name: 'default', parameters: {} }],
|
strategies: [{ name: 'default', parameters: {} }],
|
||||||
});
|
});
|
||||||
|
await frontendApiService.refreshData();
|
||||||
await app.request
|
await app.request
|
||||||
.get('/api/frontend')
|
.get('/api/frontend')
|
||||||
.set('Authorization', frontendTokenDefault.secret)
|
.set('Authorization', frontendTokenDefault.secret)
|
||||||
@ -768,6 +774,7 @@ test('should filter features by environment', async () => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
strategies: [{ name: 'default', parameters: {} }],
|
strategies: [{ name: 'default', parameters: {} }],
|
||||||
});
|
});
|
||||||
|
await frontendApiService.refreshData();
|
||||||
await app.request
|
await app.request
|
||||||
.get('/api/frontend')
|
.get('/api/frontend')
|
||||||
.set('Authorization', frontendTokenEnvironmentDefault.secret)
|
.set('Authorization', frontendTokenEnvironmentDefault.secret)
|
||||||
@ -868,6 +875,7 @@ test('should filter features by segment', async () => {
|
|||||||
await app.services.segmentService.addToStrategy(segmentA.id, strategyA.id);
|
await app.services.segmentService.addToStrategy(segmentA.id, strategyA.id);
|
||||||
await app.services.segmentService.addToStrategy(segmentB.id, strategyB.id);
|
await app.services.segmentService.addToStrategy(segmentB.id, strategyB.id);
|
||||||
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
|
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
|
||||||
|
await frontendApiService.refreshData();
|
||||||
await app.request
|
await app.request
|
||||||
.get('/api/frontend')
|
.get('/api/frontend')
|
||||||
.set('Authorization', frontendToken.secret)
|
.set('Authorization', frontendToken.secret)
|
||||||
@ -900,102 +908,6 @@ test('should filter features by segment', async () => {
|
|||||||
.expect((res) => expect(res.body).toEqual({ toggles: [] }));
|
.expect((res) => expect(res.body).toEqual({ toggles: [] }));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should sync proxy for keys on an interval', async () => {
|
|
||||||
jest.useFakeTimers();
|
|
||||||
|
|
||||||
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
|
|
||||||
const user = await app.services.apiTokenService.getUserForToken(
|
|
||||||
frontendToken.secret,
|
|
||||||
);
|
|
||||||
|
|
||||||
const spy = jest.spyOn(
|
|
||||||
ProxyRepository.prototype as any,
|
|
||||||
'featuresForToken',
|
|
||||||
);
|
|
||||||
expect(user).not.toBeNull();
|
|
||||||
const proxyRepository = new ProxyRepository(
|
|
||||||
{
|
|
||||||
getLogger,
|
|
||||||
frontendApi: { refreshIntervalInMs: 5000 },
|
|
||||||
eventBus: <any>{ emit: jest.fn() },
|
|
||||||
},
|
|
||||||
db.stores,
|
|
||||||
app.services,
|
|
||||||
user!,
|
|
||||||
);
|
|
||||||
|
|
||||||
await proxyRepository.start();
|
|
||||||
|
|
||||||
jest.advanceTimersByTime(60000);
|
|
||||||
|
|
||||||
proxyRepository.stop();
|
|
||||||
expect(spy.mock.calls.length > 6).toBe(true);
|
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should change fetch interval', async () => {
|
|
||||||
jest.useFakeTimers();
|
|
||||||
|
|
||||||
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
|
|
||||||
const user = await app.services.apiTokenService.getUserForToken(
|
|
||||||
frontendToken.secret,
|
|
||||||
);
|
|
||||||
|
|
||||||
const spy = jest.spyOn(
|
|
||||||
ProxyRepository.prototype as any,
|
|
||||||
'featuresForToken',
|
|
||||||
);
|
|
||||||
const proxyRepository = new ProxyRepository(
|
|
||||||
{
|
|
||||||
getLogger,
|
|
||||||
frontendApi: { refreshIntervalInMs: 1000 },
|
|
||||||
eventBus: <any>{ emit: jest.fn() },
|
|
||||||
},
|
|
||||||
db.stores,
|
|
||||||
app.services,
|
|
||||||
user!,
|
|
||||||
);
|
|
||||||
|
|
||||||
await proxyRepository.start();
|
|
||||||
|
|
||||||
jest.advanceTimersByTime(60000);
|
|
||||||
|
|
||||||
proxyRepository.stop();
|
|
||||||
expect(spy.mock.calls.length > 30).toBe(true);
|
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should not recursively set off timers on events', async () => {
|
|
||||||
jest.useFakeTimers();
|
|
||||||
|
|
||||||
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
|
|
||||||
const user = await app.services.apiTokenService.getUserForToken(
|
|
||||||
frontendToken.secret,
|
|
||||||
);
|
|
||||||
|
|
||||||
const spy = jest.spyOn(ProxyRepository.prototype as any, 'dataPolling');
|
|
||||||
const proxyRepository = new ProxyRepository(
|
|
||||||
{
|
|
||||||
getLogger,
|
|
||||||
frontendApi: { refreshIntervalInMs: 5000 },
|
|
||||||
eventBus: <any>{ emit: jest.fn() },
|
|
||||||
},
|
|
||||||
db.stores,
|
|
||||||
app.services,
|
|
||||||
user!,
|
|
||||||
);
|
|
||||||
|
|
||||||
await proxyRepository.start();
|
|
||||||
|
|
||||||
db.stores.eventStore.emit(FEATURE_UPDATED);
|
|
||||||
|
|
||||||
jest.advanceTimersByTime(10000);
|
|
||||||
|
|
||||||
proxyRepository.stop();
|
|
||||||
expect(spy.mock.calls.length < 3).toBe(true);
|
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return maxAge header on options call', async () => {
|
test('should return maxAge header on options call', async () => {
|
||||||
await app.request
|
await app.request
|
||||||
.options('/api/frontend')
|
.options('/api/frontend')
|
||||||
@ -1006,51 +918,6 @@ test('should return maxAge header on options call', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should terminate data polling when stop is called', async () => {
|
|
||||||
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
|
|
||||||
const user = await app.services.apiTokenService.getUserForToken(
|
|
||||||
frontendToken.secret,
|
|
||||||
);
|
|
||||||
|
|
||||||
const logTrap: any[] = [];
|
|
||||||
const getDebugLogger = (): Logger => {
|
|
||||||
return {
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
debug: (message: any, ...args: any[]) => {
|
|
||||||
logTrap.push(message);
|
|
||||||
},
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
info: (...args: any[]) => {},
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
warn: (...args: any[]) => {},
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
error: (...args: any[]) => {},
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
fatal: (...args: any[]) => {},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
const proxyRepository = new ProxyRepository(
|
|
||||||
{
|
|
||||||
getLogger: getDebugLogger,
|
|
||||||
frontendApi: { refreshIntervalInMs: 1 },
|
|
||||||
eventBus: <any>{ emit: jest.fn() },
|
|
||||||
},
|
|
||||||
db.stores,
|
|
||||||
app.services,
|
|
||||||
user!,
|
|
||||||
);
|
|
||||||
|
|
||||||
await proxyRepository.start();
|
|
||||||
proxyRepository.stop();
|
|
||||||
// Polling here is an async recursive call, so we gotta give it a bit of time
|
|
||||||
await new Promise((r) => setTimeout(r, 10));
|
|
||||||
expect(logTrap).toContain(
|
|
||||||
'Shutting down data polling for proxy repository',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should evaluate strategies when returning toggles', async () => {
|
test('should evaluate strategies when returning toggles', async () => {
|
||||||
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
|
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
|
||||||
await createFeatureToggle({
|
await createFeatureToggle({
|
||||||
@ -1084,6 +951,7 @@ test('should evaluate strategies when returning toggles', async () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await frontendApiService.refreshData();
|
||||||
await app.request
|
await app.request
|
||||||
.get('/api/frontend')
|
.get('/api/frontend')
|
||||||
.set('Authorization', frontendToken.secret)
|
.set('Authorization', frontendToken.secret)
|
||||||
@ -1144,6 +1012,7 @@ test('should not return all features', async () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
await frontendApiService.refreshData();
|
||||||
await app.request
|
await app.request
|
||||||
.get('/api/frontend')
|
.get('/api/frontend')
|
||||||
.set('Authorization', frontendToken.secret)
|
.set('Authorization', frontendToken.secret)
|
||||||
@ -1256,6 +1125,7 @@ test('should NOT evaluate disabled strategies when returning toggles', async ()
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await frontendApiService.refreshData();
|
||||||
await app.request
|
await app.request
|
||||||
.get('/api/frontend')
|
.get('/api/frontend')
|
||||||
.set('Authorization', frontendToken.secret)
|
.set('Authorization', frontendToken.secret)
|
||||||
@ -1344,7 +1214,7 @@ test('should resolve variable rollout percentage consistently', async () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
await frontendApiService.refreshData();
|
||||||
for (let i = 0; i < 10; ++i) {
|
for (let i = 0; i < 10; ++i) {
|
||||||
const { body } = await app.request
|
const { body } = await app.request
|
||||||
.get('/api/frontend')
|
.get('/api/frontend')
|
||||||
|
@ -110,9 +110,8 @@ export class GlobalFrontendApiCache extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: fetch only relevant projects/environments based on tokens
|
// TODO: fetch only relevant projects/environments based on tokens
|
||||||
private async refreshData() {
|
public async refreshData() {
|
||||||
try {
|
try {
|
||||||
const stopTimer = this.timer('refreshData');
|
|
||||||
this.featuresByEnvironment = await this.getAllFeatures();
|
this.featuresByEnvironment = await this.getAllFeatures();
|
||||||
this.segments = await this.getAllSegments();
|
this.segments = await this.getAllSegments();
|
||||||
if (this.status === 'starting') {
|
if (this.status === 'starting') {
|
||||||
@ -122,7 +121,6 @@ export class GlobalFrontendApiCache extends EventEmitter {
|
|||||||
this.status = 'updated';
|
this.status = 'updated';
|
||||||
this.emit('updated');
|
this.emit('updated');
|
||||||
}
|
}
|
||||||
stopTimer();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error('Cannot load data for token', e);
|
this.logger.error('Cannot load data for token', e);
|
||||||
}
|
}
|
||||||
|
@ -51,8 +51,6 @@ export type IFlagKey =
|
|||||||
| 'responseTimeMetricsFix'
|
| 'responseTimeMetricsFix'
|
||||||
| 'scimApi'
|
| 'scimApi'
|
||||||
| 'displayEdgeBanner'
|
| 'displayEdgeBanner'
|
||||||
| 'globalFrontendApiCache'
|
|
||||||
| 'returnGlobalFrontendApiCache'
|
|
||||||
| 'projectOverviewRefactor'
|
| 'projectOverviewRefactor'
|
||||||
| 'variantDependencies'
|
| 'variantDependencies'
|
||||||
| 'disableShowContextFieldSelectionValues'
|
| 'disableShowContextFieldSelectionValues'
|
||||||
@ -255,14 +253,6 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_RESPONSE_TIME_METRICS_FIX,
|
process.env.UNLEASH_EXPERIMENTAL_RESPONSE_TIME_METRICS_FIX,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
globalFrontendApiCache: parseEnvVarBoolean(
|
|
||||||
process.env.UNLEASH_EXPERIMENTAL_GLOBAL_FRONTEND_API_CACHE,
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
returnGlobalFrontendApiCache: parseEnvVarBoolean(
|
|
||||||
process.env.UNLEASH_EXPERIMENTAL_RETURN_GLOBAL_FRONTEND_API_CACHE,
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
projectOverviewRefactor: parseEnvVarBoolean(
|
projectOverviewRefactor: parseEnvVarBoolean(
|
||||||
process.env.UNLEASH_EXPERIMENTAL_PROJECT_OVERVIEW_REFACTOR,
|
process.env.UNLEASH_EXPERIMENTAL_PROJECT_OVERVIEW_REFACTOR,
|
||||||
false,
|
false,
|
||||||
|
@ -47,8 +47,6 @@ process.nextTick(async () => {
|
|||||||
executiveDashboardUI: true,
|
executiveDashboardUI: true,
|
||||||
userAccessUIEnabled: true,
|
userAccessUIEnabled: true,
|
||||||
outdatedSdksBanner: true,
|
outdatedSdksBanner: true,
|
||||||
globalFrontendApiCache: true,
|
|
||||||
returnGlobalFrontendApiCache: false,
|
|
||||||
projectOverviewRefactor: true,
|
projectOverviewRefactor: true,
|
||||||
disableShowContextFieldSelectionValues: false,
|
disableShowContextFieldSelectionValues: false,
|
||||||
variantDependencies: true,
|
variantDependencies: true,
|
||||||
|
Loading…
Reference in New Issue
Block a user