Skip to content

Commit 390073b

Browse files
author
arthosofteq
authored
Merge pull request #2076 from RedisInsight/be/feature/RI-4489-beta_features
#RI-4489 initial implementation of features flags
2 parents 1359f38 + 6cc96f7 commit 390073b

File tree

72 files changed

+3092
-15
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+3092
-15
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
# Add reviewers for the most sensitive folders
22
33
4+
/redisinsight/api/config/features-config.json [email protected] [email protected] [email protected]

redisinsight/api/config/default.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,18 @@ export default {
194194
},
195195
],
196196
connections: {
197-
timeout: parseInt(process.env.CONNECTIONS_TIMEOUT_DEFAULT, 10) || 30 * 1_000 // 30 sec
197+
timeout: parseInt(process.env.CONNECTIONS_TIMEOUT_DEFAULT, 10) || 30 * 1_000, // 30 sec
198198
},
199199
redisStack: {
200200
id: process.env.BUILD_TYPE === 'REDIS_STACK' ? process.env.REDIS_STACK_DATABASE_ID || 'redis-stack' : undefined,
201201
name: process.env.REDIS_STACK_DATABASE_NAME,
202202
host: process.env.REDIS_STACK_DATABASE_HOST,
203203
port: process.env.REDIS_STACK_DATABASE_PORT,
204204
},
205+
features_config: {
206+
url: process.env.RI_FEATURES_CONFIG_URL
207+
// eslint-disable-next-line max-len
208+
|| 'https://raw.githubusercontent.com/RedisInsight/RedisInsight/main/redisinsight/api/config/features-config.json',
209+
syncInterval: parseInt(process.env.RI_FEATURES_CONFIG_SYNC_INTERVAL, 10) || 1_000 * 60 * 60 * 4, // 4h
210+
},
205211
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"version": 1,
3+
"features": {
4+
"insightsRecommendations": {
5+
"flag": true,
6+
"perc": [],
7+
"filters": [
8+
{
9+
"name": "agreements.analytics",
10+
"value": true,
11+
"cond": "eq"
12+
},
13+
{
14+
"name": "config.server.buildType",
15+
"value": "ELECTRON",
16+
"cond": "eq"
17+
}
18+
]
19+
}
20+
}
21+
}

redisinsight/api/config/ormconfig.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { DatabaseEntity } from 'src/modules/database/entities/database.entity';
1515
import { SshOptionsEntity } from 'src/modules/ssh/entities/ssh-options.entity';
1616
import { BrowserHistoryEntity } from 'src/modules/browser/entities/browser-history.entity';
1717
import { CustomTutorialEntity } from 'src/modules/custom-tutorial/entities/custom-tutorial.entity';
18+
import { FeatureEntity } from 'src/modules/feature/entities/feature.entity';
19+
import { FeaturesConfigEntity } from 'src/modules/feature/entities/features-config.entity';
1820
import migrations from '../migration';
1921
import * as config from '../src/utils/config';
2022

@@ -40,6 +42,8 @@ const ormConfig = {
4042
BrowserHistoryEntity,
4143
SshOptionsEntity,
4244
CustomTutorialEntity,
45+
FeatureEntity,
46+
FeaturesConfigEntity,
4347
],
4448
migrations,
4549
};

redisinsight/api/config/test.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
export default {
22
server: {
33
env: 'test',
4-
requestTimeout: 1000,
4+
requestTimeout: parseInt(process.env.REQUEST_TIMEOUT, 10) || 1000,
5+
},
6+
db: {
7+
synchronize: process.env.DB_SYNC ? process.env.DB_SYNC === 'true' : true,
8+
migrationsRun: process.env.DB_MIGRATIONS ? process.env.DB_MIGRATIONS === 'true' : false,
59
},
610
profiler: {
711
logFileIdleThreshold: parseInt(process.env.PROFILER_LOG_FILE_IDLE_THRESHOLD, 10) || 1000 * 2, // 3sec
812
},
913
notifications: {
10-
updateUrl: 'https://s3.amazonaws.com/redisinsight.test/public/tests/notifications.json',
14+
updateUrl: process.env.NOTIFICATION_UPDATE_URL
15+
|| 'https://s3.amazonaws.com/redisinsight.test/public/tests/notifications.json',
16+
},
17+
features_config: {
18+
url: process.env.RI_FEATURES_CONFIG_URL
19+
|| 'http://localhost:5551/remote/features-config.json',
1120
},
1221
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class feature1684175820824 implements MigrationInterface {
4+
name = 'feature1684175820824'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`CREATE TABLE "features" ("name" varchar PRIMARY KEY NOT NULL, "flag" boolean NOT NULL)`);
8+
await queryRunner.query(`CREATE TABLE "features_config" ("id" varchar PRIMARY KEY NOT NULL, "controlNumber" integer, "data" varchar NOT NULL, "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`);
9+
}
10+
11+
public async down(queryRunner: QueryRunner): Promise<void> {
12+
await queryRunner.query(`DROP TABLE "features_config"`);
13+
await queryRunner.query(`DROP TABLE "features"`);
14+
}
15+
16+
}

redisinsight/api/migration/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { databaseCompressor1678182722874 } from './1678182722874-database-compre
3131
import { customTutorials1677135091633 } from './1677135091633-custom-tutorials';
3232
import { databaseRecommendations1681900503586 } from './1681900503586-database-recommendations';
3333
import { databaseRecommendationParams1683006064293 } from './1683006064293-database-recommendation-params';
34+
import { feature1684175820824 } from './1684175820824-feature';
3435

3536
export default [
3637
initialMigration1614164490968,
@@ -66,4 +67,5 @@ export default [
6667
customTutorials1677135091633,
6768
databaseRecommendations1681900503586,
6869
databaseRecommendationParams1683006064293,
70+
feature1684175820824,
6971
];

redisinsight/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand -w 1",
3232
"test:e2e": "jest --config ./test/jest-e2e.json -w 1",
3333
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js -d ./config/ormconfig.ts",
34-
"test:api": "ts-mocha --paths -p test/api/api.tsconfig.json --config ./test/api/.mocharc.yml",
34+
"test:api": "cross-env NODE_ENV=test ts-mocha --paths -p test/api/api.tsconfig.json --config ./test/api/.mocharc.yml",
3535
"test:api:cov": "nyc --reporter=html --reporter=text --reporter=text-summary yarn run test:api",
3636
"test:api:ci:cov": "nyc -r text -r text-summary -r html yarn run test:api --reporter mocha-multi-reporters --reporter-options configFile=test/api/reporters.json && nyc merge .nyc_output ./coverage/test-run-coverage.json",
3737
"typeorm:migrate": "cross-env NODE_ENV=staging yarn typeorm migration:generate ./migration/migration",

redisinsight/api/src/__mocks__/common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export const mockRepository = jest.fn(() => ({
6161
save: jest.fn(),
6262
insert: jest.fn(),
6363
update: jest.fn(),
64+
upsert: jest.fn(),
6465
delete: jest.fn(),
6566
remove: jest.fn(),
6667
createQueryBuilder: mockCreateQueryBuilder,
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { FeaturesConfigEntity } from 'src/modules/feature/entities/features-config.entity';
2+
import {
3+
FeatureConfig,
4+
FeatureConfigFilter, FeatureConfigFilterAnd, FeatureConfigFilterOr,
5+
FeaturesConfig,
6+
FeaturesConfigData,
7+
} from 'src/modules/feature/model/features-config';
8+
import { classToClass } from 'src/utils';
9+
import { Feature } from 'src/modules/feature/model/feature';
10+
import { FeatureEntity } from 'src/modules/feature/entities/feature.entity';
11+
import { mockAppSettings } from 'src/__mocks__/app-settings';
12+
import config from 'src/utils/config';
13+
import { KnownFeatures } from 'src/modules/feature/constants';
14+
import * as defaultConfig from '../../config/features-config.json';
15+
16+
export const mockFeaturesConfigId = '1';
17+
export const mockFeaturesConfigVersion = defaultConfig.version + 0.111;
18+
export const mockControlNumber = 7.68;
19+
export const mockControlGroup = '7';
20+
21+
export const mockFeaturesConfigJson = {
22+
version: mockFeaturesConfigVersion,
23+
features: {
24+
[KnownFeatures.InsightsRecommendations]: {
25+
perc: [[1.25, 8.45]],
26+
flag: true,
27+
filters: [
28+
{
29+
name: 'agreements.analytics',
30+
value: true,
31+
cond: 'eq',
32+
},
33+
],
34+
},
35+
},
36+
};
37+
38+
export const mockFeaturesConfigJsonComplex = {
39+
...mockFeaturesConfigJson,
40+
features: {
41+
[KnownFeatures.InsightsRecommendations]: {
42+
...mockFeaturesConfigJson.features[KnownFeatures.InsightsRecommendations],
43+
filters: [
44+
{
45+
or: [
46+
{
47+
name: 'settings.testValue',
48+
value: 'test',
49+
cond: 'eq',
50+
},
51+
{
52+
and: [
53+
{
54+
name: 'agreements.analytics',
55+
value: true,
56+
cond: 'eq',
57+
},
58+
{
59+
or: [
60+
{
61+
name: 'settings.scanThreshold',
62+
value: mockAppSettings.scanThreshold,
63+
cond: 'eq',
64+
},
65+
{
66+
name: 'settings.batchSize',
67+
value: mockAppSettings.batchSize,
68+
cond: 'eq',
69+
},
70+
],
71+
},
72+
],
73+
},
74+
],
75+
},
76+
],
77+
},
78+
},
79+
};
80+
81+
export const mockFeaturesConfigData = Object.assign(new FeaturesConfigData(), {
82+
...mockFeaturesConfigJson,
83+
features: new Map(Object.entries({
84+
[KnownFeatures.InsightsRecommendations]: Object.assign(new FeatureConfig(), {
85+
...mockFeaturesConfigJson.features[KnownFeatures.InsightsRecommendations],
86+
filters: [
87+
Object.assign(new FeatureConfigFilter(), {
88+
...mockFeaturesConfigJson.features[KnownFeatures.InsightsRecommendations].filters[0],
89+
}),
90+
],
91+
}),
92+
})),
93+
});
94+
95+
export const mockFeaturesConfigDataComplex = Object.assign(new FeaturesConfigData(), {
96+
...mockFeaturesConfigJson,
97+
features: new Map(Object.entries({
98+
[KnownFeatures.InsightsRecommendations]: Object.assign(new FeatureConfig(), {
99+
...mockFeaturesConfigJson.features[KnownFeatures.InsightsRecommendations],
100+
filters: [
101+
Object.assign(new FeatureConfigFilterOr(), {
102+
or: [
103+
Object.assign(new FeatureConfigFilter(), {
104+
name: 'settings.testValue',
105+
value: 'test',
106+
cond: 'eq',
107+
}),
108+
Object.assign(new FeatureConfigFilterAnd(), {
109+
and: [
110+
Object.assign(new FeatureConfigFilter(), {
111+
name: 'agreements.analytics',
112+
value: true,
113+
cond: 'eq',
114+
}),
115+
Object.assign(new FeatureConfigFilterOr(), {
116+
or: [
117+
Object.assign(new FeatureConfigFilter(), {
118+
name: 'settings.scanThreshold',
119+
value: mockAppSettings.scanThreshold,
120+
cond: 'eq',
121+
}),
122+
Object.assign(new FeatureConfigFilter(), {
123+
name: 'settings.batchSize',
124+
value: mockAppSettings.batchSize,
125+
cond: 'eq',
126+
}),
127+
],
128+
}),
129+
],
130+
}),
131+
],
132+
}),
133+
],
134+
}),
135+
})),
136+
});
137+
138+
export const mockFeaturesConfig = Object.assign(new FeaturesConfig(), {
139+
controlNumber: mockControlNumber,
140+
data: mockFeaturesConfigData,
141+
});
142+
143+
export const mockFeaturesConfigComplex = Object.assign(new FeaturesConfig(), {
144+
controlNumber: mockControlNumber,
145+
data: mockFeaturesConfigDataComplex,
146+
});
147+
148+
export const mockFeaturesConfigEntity = Object.assign(new FeaturesConfigEntity(), {
149+
...classToClass(FeaturesConfigEntity, mockFeaturesConfig),
150+
id: mockFeaturesConfigId,
151+
});
152+
153+
export const mockFeaturesConfigEntityComplex = Object.assign(new FeaturesConfigEntity(), {
154+
...classToClass(FeaturesConfigEntity, mockFeaturesConfigComplex),
155+
id: mockFeaturesConfigId,
156+
});
157+
158+
export const mockFeature = Object.assign(new Feature(), {
159+
name: KnownFeatures.InsightsRecommendations,
160+
flag: true,
161+
});
162+
163+
export const mockUnknownFeature = Object.assign(new Feature(), {
164+
name: 'unknown',
165+
flag: true,
166+
});
167+
168+
export const mockFeatureEntity = Object.assign(new FeatureEntity(), {
169+
id: 'lr-1',
170+
name: KnownFeatures.InsightsRecommendations,
171+
flag: true,
172+
});
173+
174+
export const mockServerState = {
175+
settings: mockAppSettings,
176+
agreements: mockAppSettings.agreements,
177+
config: config.get(),
178+
env: process.env,
179+
};
180+
181+
export const mockFeaturesConfigRepository = jest.fn(() => ({
182+
getOrCreate: jest.fn().mockResolvedValue(mockFeaturesConfig),
183+
update: jest.fn().mockResolvedValue(mockFeaturesConfig),
184+
}));
185+
186+
export const mockFeatureRepository = jest.fn(() => ({
187+
get: jest.fn().mockResolvedValue(mockFeature),
188+
upsert: jest.fn().mockResolvedValue({ updated: 1 }),
189+
list: jest.fn().mockResolvedValue([mockFeature]),
190+
delete: jest.fn().mockResolvedValue({ deleted: 1 }),
191+
}));
192+
193+
export const mockFeaturesConfigService = jest.fn(() => ({
194+
sync: jest.fn(),
195+
getControlInfo: jest.fn().mockResolvedValue({
196+
controlNumber: mockControlNumber,
197+
controlGroup: mockControlGroup,
198+
}),
199+
}));
200+
201+
export const mockFeatureService = jest.fn(() => ({
202+
isFeatureEnabled: jest.fn().mockResolvedValue(true),
203+
}));
204+
205+
export const mockFeatureAnalytics = jest.fn(() => ({
206+
sendFeatureFlagConfigUpdated: jest.fn(),
207+
sendFeatureFlagConfigUpdateError: jest.fn(),
208+
sendFeatureFlagInvalidRemoteConfig: jest.fn(),
209+
sendFeatureFlagRecalculated: jest.fn(),
210+
}));
211+
212+
export const mockInsightsRecommendationsFlagStrategy = {
213+
calculate: jest.fn().mockResolvedValue(true),
214+
};
215+
216+
export const mockFeatureFlagProvider = jest.fn(() => ({
217+
getStrategy: jest.fn().mockResolvedValue(mockInsightsRecommendationsFlagStrategy),
218+
calculate: jest.fn().mockResolvedValue(true),
219+
}));

0 commit comments

Comments
 (0)