1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

Feat/impression data (#1310)

* feat: add impression data column

* fix: add default value to impressionData

* fix: allow client api to return impressionData

* fix: add tests for impressionData

* fix: reset server-dev

* fix: add test for adding a toggle with impression data on a different project

* fix: update tests
This commit is contained in:
Fredrik Strand Oseberg 2022-02-03 11:06:51 +01:00 committed by GitHub
parent fb7014a8ab
commit 6520aa1b0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 189 additions and 1 deletions

View File

@ -206,6 +206,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
'features.project as project',
'features.stale as stale',
'features.variants as variants',
'features.impression_data as impression_data',
'features.created_at as created_at',
'features.last_seen_at as last_seen_at',
'feature_environments.enabled as enabled',
@ -249,6 +250,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
acc.environments = {};
}
acc.name = r.name;
acc.impressionData = r.impression_data;
acc.description = r.description;
acc.project = r.project;
acc.stale = r.stale;

View File

@ -74,6 +74,7 @@ export default class FeatureToggleClientStore
'features.type as type',
'features.project as project',
'features.stale as stale',
'features.impression_data as impression_data',
'features.variants as variants',
'features.created_at as created_at',
'features.last_seen_at as last_seen_at',
@ -137,6 +138,7 @@ export default class FeatureToggleClientStore
if (r.strategy_name) {
feature.strategies.push(this.getAdminStrategy(r, isAdmin));
}
feature.impressionData = r.impression_data;
feature.enabled = !!r.enabled;
feature.name = r.name;
feature.description = r.description;

View File

@ -15,6 +15,7 @@ const FEATURE_COLUMNS = [
'stale',
'variants',
'created_at',
'impression_data',
'last_seen_at',
];
@ -27,6 +28,7 @@ export interface FeaturesTable {
project: string;
last_seen_at?: Date;
created_at?: Date;
impression_data: boolean;
}
const TABLE = 'features';
@ -166,6 +168,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
variants: sortedVariants,
createdAt: row.created_at,
lastSeenAt: row.last_seen_at,
impressionData: row.impression_data,
};
}
@ -188,6 +191,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
archived: data.archived || false,
stale: data.stale,
created_at: data.createdAt,
impression_data: data.impressionData,
};
if (!row.created_at) {
delete row.created_at;

View File

@ -4,6 +4,7 @@ test('should require URL firendly name', () => {
const toggle = {
name: 'io`dasd',
enabled: false,
impressionData: false,
strategies: [{ name: 'default' }],
};
@ -15,6 +16,7 @@ test('should be valid toggle name', () => {
const toggle = {
name: 'app.name',
enabled: false,
impressionData: false,
strategies: [{ name: 'default' }],
};
@ -28,6 +30,7 @@ test('should strip extra variant fields', () => {
type: 'release',
enabled: false,
stale: false,
impressionData: false,
strategies: [{ name: 'default' }],
variants: [
{
@ -49,6 +52,7 @@ test('should allow weightType=fix', () => {
type: 'release',
project: 'default',
enabled: false,
impressionData: false,
stale: false,
archived: false,
strategies: [{ name: 'default' }],
@ -71,6 +75,7 @@ test('should disallow weightType=unknown', () => {
name: 'app.name',
type: 'release',
enabled: false,
impressionData: false,
stale: false,
archived: false,
strategies: [{ name: 'default' }],
@ -95,6 +100,7 @@ test('should be possible to define variant overrides', () => {
type: 'release',
project: 'some',
enabled: false,
impressionData: false,
stale: false,
archived: false,
strategies: [{ name: 'default' }],
@ -125,6 +131,7 @@ test('variant overrides must have corect shape', async () => {
name: 'app.name',
type: 'release',
enabled: false,
impressionData: false,
stale: false,
strategies: [{ name: 'default' }],
variants: [
@ -154,6 +161,7 @@ test('should keep constraints', () => {
type: 'release',
project: 'default',
enabled: false,
impressionData: false,
stale: false,
archived: false,
strategies: [
@ -180,6 +188,7 @@ test('should not accept empty constraint values', () => {
name: 'app.constraints.empty.value',
type: 'release',
enabled: false,
impressionData: false,
stale: false,
strategies: [
{
@ -206,6 +215,7 @@ test('should not accept empty list of constraint values', () => {
name: 'app.constraints.empty.value.list',
type: 'release',
enabled: false,
impressionData: false,
stale: false,
strategies: [
{

View File

@ -56,6 +56,12 @@ export const featureMetadataSchema = joi
archived: joi.boolean().default(false),
type: joi.string().default('release'),
description: joi.string().allow('').allow(null).optional(),
impressionData: joi
.boolean()
.allow(true)
.allow(false)
.default(false)
.optional(),
createdAt: joi.date().optional().allow(null),
})
.options({ allowUnknown: false, stripUnknown: true, abortEarly: false });
@ -70,6 +76,12 @@ export const featureSchema = joi
type: joi.string().default('release'),
project: joi.string().default('default'),
description: joi.string().allow('').allow(null).optional(),
impressionData: joi
.boolean()
.allow(true)
.allow(false)
.default(false)
.optional(),
strategies: joi
.array()
.min(0)

View File

@ -38,6 +38,7 @@ export interface FeatureToggleDTO {
stale?: boolean;
archived?: boolean;
createdAt?: Date;
impressionData?: boolean;
}
export interface FeatureToggle extends FeatureToggleDTO {

View File

@ -0,0 +1,19 @@
'use strict';
exports.up = function (db, cb) {
db.runSql(
`
ALTER TABLE features ADD COLUMN "impression_data" BOOLEAN DEFAULT FALSE;
`,
cb,
);
};
exports.down = function (db, cb) {
db.runSql(
`
ALTER TABLE features DROP COLUMN "impression_data";
`,
cb,
);
};

View File

@ -512,13 +512,19 @@ test('Should patch feature toggle', async () => {
const name = 'new.toggle.patch';
await app.request
.post(url)
.send({ name, description: 'some', type: 'release' })
.send({
name,
description: 'some',
type: 'release',
impressionData: true,
})
.expect(201);
await app.request
.patch(`${url}/${name}`)
.send([
{ op: 'replace', path: '/description', value: 'New desc' },
{ op: 'replace', path: '/type', value: 'kill-switch' },
{ op: 'replace', path: '/impressionData', value: false },
])
.expect(200);
@ -527,6 +533,7 @@ test('Should patch feature toggle', async () => {
expect(toggle.name).toBe(name);
expect(toggle.description).toBe('New desc');
expect(toggle.type).toBe('kill-switch');
expect(toggle.impressionData).toBe(false);
expect(toggle.archived).toBeFalsy();
const events = await db.stores.eventStore.getAll({
type: FEATURE_METADATA_UPDATED,
@ -1983,3 +1990,79 @@ test('should not update project with PATCH', async () => {
})
.expect(200);
});
test('Can create a feature with impression data', async () => {
await app.request
.post('/api/admin/projects/default/features')
.send({
name: 'new.toggle.with.impressionData',
impressionData: true,
})
.expect(201)
.expect((res) => {
expect(res.body.impressionData).toBe(true);
});
});
test('Can create a feature without impression data', async () => {
await app.request
.post('/api/admin/projects/default/features')
.send({
name: 'new.toggle.without.impressionData',
})
.expect(201)
.expect((res) => {
expect(res.body.impressionData).toBe(false);
});
});
test('Can update impression data with PUT', async () => {
const toggle = {
name: 'update.toggle.with.impressionData',
impressionData: true,
};
await app.request
.post('/api/admin/projects/default/features')
.send(toggle)
.expect(201)
.expect((res) => {
expect(res.body.impressionData).toBe(true);
});
await app.request
.put(`/api/admin/projects/default/features/${toggle.name}`)
.send({ ...toggle, impressionData: false })
.expect(200)
.expect((res) => {
expect(res.body.impressionData).toBe(false);
});
});
test('Can create toggle with impression data on different project', async () => {
db.stores.projectStore.create({
id: 'impression-data',
name: 'ImpressionData',
description: '',
});
const toggle = {
name: 'project.impression.data',
impressionData: true,
};
await app.request
.post('/api/admin/projects/impression-data/features')
.send(toggle)
.expect(201)
.expect((res) => {
expect(res.body.impressionData).toBe(true);
});
await app.request
.put(`/api/admin/projects/impression-data/features/${toggle.name}`)
.send({ ...toggle, impressionData: false })
.expect(200)
.expect((res) => {
expect(res.body.impressionData).toBe(false);
});
});

View File

@ -14,6 +14,7 @@ beforeAll(async () => {
{
name: 'featureX',
description: 'the #1 feature',
impressionData: true,
},
'test',
);
@ -134,6 +135,24 @@ test('gets a feature by name', async () => {
.expect(200);
});
test('returns a feature toggles impression data', async () => {
return app.request
.get('/api/client/features/featureX')
.expect('Content-Type', /json/)
.expect((res) => {
expect(res.body.impressionData).toBe(true);
});
});
test('returns a false for impression data when not specified', async () => {
return app.request
.get('/api/client/features/featureZ')
.expect('Content-Type', /json/)
.expect((res) => {
expect(res.body.impressionData).toBe(false);
});
});
test('cant get feature that does not exist', async () => {
return app.request
.get('/api/client/features/myfeature')
@ -255,3 +274,39 @@ test('Can use multiple filters', async () => {
expect(res.body.features[0].name).toBe('test.feature');
});
});
test('returns a feature toggles impression data for a different project', async () => {
const project = {
id: 'impression-data-client',
name: 'ImpressionData',
description: '',
};
db.stores.projectStore.create(project);
const toggle = {
name: 'project-client.impression.data',
impressionData: true,
};
await app.request
.post('/api/admin/projects/impression-data-client/features')
.send(toggle)
.expect(201)
.expect((res) => {
expect(res.body.impressionData).toBe(true);
});
return app.request
.get('/api/client/features')
.expect('Content-Type', /json/)
.expect((res) => {
const projectToggle = res.body.features.find(
(resToggle) => resToggle.project === project.id,
);
expect(projectToggle.name).toBe(toggle.name);
expect(projectToggle.project).toBe(project.id);
expect(projectToggle.impressionData).toBe(true);
});
});