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:
parent
fb7014a8ab
commit
6520aa1b0c
@ -206,6 +206,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
'features.project as project',
|
'features.project as project',
|
||||||
'features.stale as stale',
|
'features.stale as stale',
|
||||||
'features.variants as variants',
|
'features.variants as variants',
|
||||||
|
'features.impression_data as impression_data',
|
||||||
'features.created_at as created_at',
|
'features.created_at as created_at',
|
||||||
'features.last_seen_at as last_seen_at',
|
'features.last_seen_at as last_seen_at',
|
||||||
'feature_environments.enabled as enabled',
|
'feature_environments.enabled as enabled',
|
||||||
@ -249,6 +250,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
acc.environments = {};
|
acc.environments = {};
|
||||||
}
|
}
|
||||||
acc.name = r.name;
|
acc.name = r.name;
|
||||||
|
acc.impressionData = r.impression_data;
|
||||||
acc.description = r.description;
|
acc.description = r.description;
|
||||||
acc.project = r.project;
|
acc.project = r.project;
|
||||||
acc.stale = r.stale;
|
acc.stale = r.stale;
|
||||||
|
@ -74,6 +74,7 @@ export default class FeatureToggleClientStore
|
|||||||
'features.type as type',
|
'features.type as type',
|
||||||
'features.project as project',
|
'features.project as project',
|
||||||
'features.stale as stale',
|
'features.stale as stale',
|
||||||
|
'features.impression_data as impression_data',
|
||||||
'features.variants as variants',
|
'features.variants as variants',
|
||||||
'features.created_at as created_at',
|
'features.created_at as created_at',
|
||||||
'features.last_seen_at as last_seen_at',
|
'features.last_seen_at as last_seen_at',
|
||||||
@ -137,6 +138,7 @@ export default class FeatureToggleClientStore
|
|||||||
if (r.strategy_name) {
|
if (r.strategy_name) {
|
||||||
feature.strategies.push(this.getAdminStrategy(r, isAdmin));
|
feature.strategies.push(this.getAdminStrategy(r, isAdmin));
|
||||||
}
|
}
|
||||||
|
feature.impressionData = r.impression_data;
|
||||||
feature.enabled = !!r.enabled;
|
feature.enabled = !!r.enabled;
|
||||||
feature.name = r.name;
|
feature.name = r.name;
|
||||||
feature.description = r.description;
|
feature.description = r.description;
|
||||||
|
@ -15,6 +15,7 @@ const FEATURE_COLUMNS = [
|
|||||||
'stale',
|
'stale',
|
||||||
'variants',
|
'variants',
|
||||||
'created_at',
|
'created_at',
|
||||||
|
'impression_data',
|
||||||
'last_seen_at',
|
'last_seen_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ export interface FeaturesTable {
|
|||||||
project: string;
|
project: string;
|
||||||
last_seen_at?: Date;
|
last_seen_at?: Date;
|
||||||
created_at?: Date;
|
created_at?: Date;
|
||||||
|
impression_data: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TABLE = 'features';
|
const TABLE = 'features';
|
||||||
@ -166,6 +168,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
|||||||
variants: sortedVariants,
|
variants: sortedVariants,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
lastSeenAt: row.last_seen_at,
|
lastSeenAt: row.last_seen_at,
|
||||||
|
impressionData: row.impression_data,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,6 +191,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
|||||||
archived: data.archived || false,
|
archived: data.archived || false,
|
||||||
stale: data.stale,
|
stale: data.stale,
|
||||||
created_at: data.createdAt,
|
created_at: data.createdAt,
|
||||||
|
impression_data: data.impressionData,
|
||||||
};
|
};
|
||||||
if (!row.created_at) {
|
if (!row.created_at) {
|
||||||
delete row.created_at;
|
delete row.created_at;
|
||||||
|
@ -4,6 +4,7 @@ test('should require URL firendly name', () => {
|
|||||||
const toggle = {
|
const toggle = {
|
||||||
name: 'io`dasd',
|
name: 'io`dasd',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
impressionData: false,
|
||||||
strategies: [{ name: 'default' }],
|
strategies: [{ name: 'default' }],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ test('should be valid toggle name', () => {
|
|||||||
const toggle = {
|
const toggle = {
|
||||||
name: 'app.name',
|
name: 'app.name',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
impressionData: false,
|
||||||
strategies: [{ name: 'default' }],
|
strategies: [{ name: 'default' }],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -28,6 +30,7 @@ test('should strip extra variant fields', () => {
|
|||||||
type: 'release',
|
type: 'release',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
stale: false,
|
stale: false,
|
||||||
|
impressionData: false,
|
||||||
strategies: [{ name: 'default' }],
|
strategies: [{ name: 'default' }],
|
||||||
variants: [
|
variants: [
|
||||||
{
|
{
|
||||||
@ -49,6 +52,7 @@ test('should allow weightType=fix', () => {
|
|||||||
type: 'release',
|
type: 'release',
|
||||||
project: 'default',
|
project: 'default',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
impressionData: false,
|
||||||
stale: false,
|
stale: false,
|
||||||
archived: false,
|
archived: false,
|
||||||
strategies: [{ name: 'default' }],
|
strategies: [{ name: 'default' }],
|
||||||
@ -71,6 +75,7 @@ test('should disallow weightType=unknown', () => {
|
|||||||
name: 'app.name',
|
name: 'app.name',
|
||||||
type: 'release',
|
type: 'release',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
impressionData: false,
|
||||||
stale: false,
|
stale: false,
|
||||||
archived: false,
|
archived: false,
|
||||||
strategies: [{ name: 'default' }],
|
strategies: [{ name: 'default' }],
|
||||||
@ -95,6 +100,7 @@ test('should be possible to define variant overrides', () => {
|
|||||||
type: 'release',
|
type: 'release',
|
||||||
project: 'some',
|
project: 'some',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
impressionData: false,
|
||||||
stale: false,
|
stale: false,
|
||||||
archived: false,
|
archived: false,
|
||||||
strategies: [{ name: 'default' }],
|
strategies: [{ name: 'default' }],
|
||||||
@ -125,6 +131,7 @@ test('variant overrides must have corect shape', async () => {
|
|||||||
name: 'app.name',
|
name: 'app.name',
|
||||||
type: 'release',
|
type: 'release',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
impressionData: false,
|
||||||
stale: false,
|
stale: false,
|
||||||
strategies: [{ name: 'default' }],
|
strategies: [{ name: 'default' }],
|
||||||
variants: [
|
variants: [
|
||||||
@ -154,6 +161,7 @@ test('should keep constraints', () => {
|
|||||||
type: 'release',
|
type: 'release',
|
||||||
project: 'default',
|
project: 'default',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
impressionData: false,
|
||||||
stale: false,
|
stale: false,
|
||||||
archived: false,
|
archived: false,
|
||||||
strategies: [
|
strategies: [
|
||||||
@ -180,6 +188,7 @@ test('should not accept empty constraint values', () => {
|
|||||||
name: 'app.constraints.empty.value',
|
name: 'app.constraints.empty.value',
|
||||||
type: 'release',
|
type: 'release',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
impressionData: false,
|
||||||
stale: false,
|
stale: false,
|
||||||
strategies: [
|
strategies: [
|
||||||
{
|
{
|
||||||
@ -206,6 +215,7 @@ test('should not accept empty list of constraint values', () => {
|
|||||||
name: 'app.constraints.empty.value.list',
|
name: 'app.constraints.empty.value.list',
|
||||||
type: 'release',
|
type: 'release',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
impressionData: false,
|
||||||
stale: false,
|
stale: false,
|
||||||
strategies: [
|
strategies: [
|
||||||
{
|
{
|
||||||
|
@ -56,6 +56,12 @@ export const featureMetadataSchema = joi
|
|||||||
archived: joi.boolean().default(false),
|
archived: joi.boolean().default(false),
|
||||||
type: joi.string().default('release'),
|
type: joi.string().default('release'),
|
||||||
description: joi.string().allow('').allow(null).optional(),
|
description: joi.string().allow('').allow(null).optional(),
|
||||||
|
impressionData: joi
|
||||||
|
.boolean()
|
||||||
|
.allow(true)
|
||||||
|
.allow(false)
|
||||||
|
.default(false)
|
||||||
|
.optional(),
|
||||||
createdAt: joi.date().optional().allow(null),
|
createdAt: joi.date().optional().allow(null),
|
||||||
})
|
})
|
||||||
.options({ allowUnknown: false, stripUnknown: true, abortEarly: false });
|
.options({ allowUnknown: false, stripUnknown: true, abortEarly: false });
|
||||||
@ -70,6 +76,12 @@ export const featureSchema = joi
|
|||||||
type: joi.string().default('release'),
|
type: joi.string().default('release'),
|
||||||
project: joi.string().default('default'),
|
project: joi.string().default('default'),
|
||||||
description: joi.string().allow('').allow(null).optional(),
|
description: joi.string().allow('').allow(null).optional(),
|
||||||
|
impressionData: joi
|
||||||
|
.boolean()
|
||||||
|
.allow(true)
|
||||||
|
.allow(false)
|
||||||
|
.default(false)
|
||||||
|
.optional(),
|
||||||
strategies: joi
|
strategies: joi
|
||||||
.array()
|
.array()
|
||||||
.min(0)
|
.min(0)
|
||||||
|
@ -38,6 +38,7 @@ export interface FeatureToggleDTO {
|
|||||||
stale?: boolean;
|
stale?: boolean;
|
||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
|
impressionData?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FeatureToggle extends FeatureToggleDTO {
|
export interface FeatureToggle extends FeatureToggleDTO {
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
|
};
|
@ -512,13 +512,19 @@ test('Should patch feature toggle', async () => {
|
|||||||
const name = 'new.toggle.patch';
|
const name = 'new.toggle.patch';
|
||||||
await app.request
|
await app.request
|
||||||
.post(url)
|
.post(url)
|
||||||
.send({ name, description: 'some', type: 'release' })
|
.send({
|
||||||
|
name,
|
||||||
|
description: 'some',
|
||||||
|
type: 'release',
|
||||||
|
impressionData: true,
|
||||||
|
})
|
||||||
.expect(201);
|
.expect(201);
|
||||||
await app.request
|
await app.request
|
||||||
.patch(`${url}/${name}`)
|
.patch(`${url}/${name}`)
|
||||||
.send([
|
.send([
|
||||||
{ op: 'replace', path: '/description', value: 'New desc' },
|
{ op: 'replace', path: '/description', value: 'New desc' },
|
||||||
{ op: 'replace', path: '/type', value: 'kill-switch' },
|
{ op: 'replace', path: '/type', value: 'kill-switch' },
|
||||||
|
{ op: 'replace', path: '/impressionData', value: false },
|
||||||
])
|
])
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
@ -527,6 +533,7 @@ test('Should patch feature toggle', async () => {
|
|||||||
expect(toggle.name).toBe(name);
|
expect(toggle.name).toBe(name);
|
||||||
expect(toggle.description).toBe('New desc');
|
expect(toggle.description).toBe('New desc');
|
||||||
expect(toggle.type).toBe('kill-switch');
|
expect(toggle.type).toBe('kill-switch');
|
||||||
|
expect(toggle.impressionData).toBe(false);
|
||||||
expect(toggle.archived).toBeFalsy();
|
expect(toggle.archived).toBeFalsy();
|
||||||
const events = await db.stores.eventStore.getAll({
|
const events = await db.stores.eventStore.getAll({
|
||||||
type: FEATURE_METADATA_UPDATED,
|
type: FEATURE_METADATA_UPDATED,
|
||||||
@ -1983,3 +1990,79 @@ test('should not update project with PATCH', async () => {
|
|||||||
})
|
})
|
||||||
.expect(200);
|
.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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -14,6 +14,7 @@ beforeAll(async () => {
|
|||||||
{
|
{
|
||||||
name: 'featureX',
|
name: 'featureX',
|
||||||
description: 'the #1 feature',
|
description: 'the #1 feature',
|
||||||
|
impressionData: true,
|
||||||
},
|
},
|
||||||
'test',
|
'test',
|
||||||
);
|
);
|
||||||
@ -134,6 +135,24 @@ test('gets a feature by name', async () => {
|
|||||||
.expect(200);
|
.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 () => {
|
test('cant get feature that does not exist', async () => {
|
||||||
return app.request
|
return app.request
|
||||||
.get('/api/client/features/myfeature')
|
.get('/api/client/features/myfeature')
|
||||||
@ -255,3 +274,39 @@ test('Can use multiple filters', async () => {
|
|||||||
expect(res.body.features[0].name).toBe('test.feature');
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user