1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-18 13:48:58 +02:00

refactor: drop tags in events

This commit is contained in:
Nuno Góis 2023-09-14 16:59:52 +01:00
parent 6bbb382d1c
commit 687a7158d8
No known key found for this signature in database
GPG Key ID: 71ECC689F1091765
19 changed files with 214 additions and 257 deletions

View File

@ -1,14 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Should call datadog webhook for archived toggle 1`] = `"{"text":"%%% \\n some@user.com just archived feature toggle *[some-toggle](http://some-url.com/archive)* \\n %%% ","title":"Unleash notification update"}"`; exports[`Should call datadog webhook for archived toggle 1`] = `"{"text":"%%% \\n some@user.com just archived feature toggle *[some-toggle](http://some-url.com/archive)* \\n %%% ","title":"Unleash notification update","tags":[]}"`;
exports[`Should call datadog webhook for archived toggle with project info 1`] = `"{"text":"%%% \\n some@user.com just archived feature toggle *[some-toggle](http://some-url.com/projects/some-project/archive)* \\n %%% ","title":"Unleash notification update"}"`; exports[`Should call datadog webhook for archived toggle with project info 1`] = `"{"text":"%%% \\n some@user.com just archived feature toggle *[some-toggle](http://some-url.com/projects/some-project/archive)* \\n %%% ","title":"Unleash notification update","tags":[]}"`;
exports[`Should call datadog webhook 1`] = `"{"text":"%%% \\n some@user.com created feature toggle [some-toggle](http://some-url.com/projects//features/some-toggle) in project *undefined* \\n %%% ","title":"Unleash notification update"}"`; exports[`Should call datadog webhook 1`] = `"{"text":"%%% \\n some@user.com created feature toggle [some-toggle](http://some-url.com/projects//features/some-toggle) in project *undefined* \\n %%% ","title":"Unleash notification update","tags":[]}"`;
exports[`Should call datadog webhook for toggled environment 1`] = `"{"text":"%%% \\n some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/features/some-toggle) in *development* environment in project *default* \\n %%% ","title":"Unleash notification update"}"`; exports[`Should call datadog webhook for toggled environment 1`] = `"{"text":"%%% \\n some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/features/some-toggle) in *development* environment in project *default* \\n %%% ","title":"Unleash notification update","tags":[]}"`;
exports[`Should include customHeaders in headers when calling service 1`] = `"{"text":"%%% \\n some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/features/some-toggle) in *development* environment in project *default* \\n %%% ","title":"Unleash notification update"}"`; exports[`Should include customHeaders in headers when calling service 1`] = `"{"text":"%%% \\n some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/features/some-toggle) in *development* environment in project *default* \\n %%% ","title":"Unleash notification update","tags":[]}"`;
exports[`Should include customHeaders in headers when calling service 2`] = ` exports[`Should include customHeaders in headers when calling service 2`] = `
{ {
@ -18,7 +18,7 @@ exports[`Should include customHeaders in headers when calling service 2`] = `
} }
`; `;
exports[`Should not include source_type_name when included in the config 1`] = `"{"text":"%%% \\n some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/features/some-toggle) in *development* environment in project *default* \\n %%% ","title":"Unleash notification update","source_type_name":"my-custom-source-type"}"`; exports[`Should not include source_type_name when included in the config 1`] = `"{"text":"%%% \\n some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/features/some-toggle) in *development* environment in project *default* \\n %%% ","title":"Unleash notification update","tags":[],"source_type_name":"my-custom-source-type"}"`;
exports[`Should not include source_type_name when included in the config 2`] = ` exports[`Should not include source_type_name when included in the config 2`] = `
{ {

View File

@ -2,6 +2,7 @@ import nock from 'nock';
import noLogger from '../../test/fixtures/no-logger'; import noLogger from '../../test/fixtures/no-logger';
import SlackAddon from './slack'; import SlackAddon from './slack';
import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store';
beforeEach(() => { beforeEach(() => {
nock.disableNetConnect(); nock.disableNetConnect();
@ -12,6 +13,7 @@ test('Does not retry if request succeeds', async () => {
const addon = new SlackAddon({ const addon = new SlackAddon({
getLogger: noLogger, getLogger: noLogger,
unleashUrl: url, unleashUrl: url,
featureTagStore: new FakeFeatureTagStore(),
}); });
nock(url).get('/').reply(201); nock(url).get('/').reply(201);
const res = await addon.fetchRetry(url); const res = await addon.fetchRetry(url);
@ -23,6 +25,7 @@ test('Retries once, and succeeds', async () => {
const addon = new SlackAddon({ const addon = new SlackAddon({
getLogger: noLogger, getLogger: noLogger,
unleashUrl: url, unleashUrl: url,
featureTagStore: new FakeFeatureTagStore(),
}); });
nock(url).get('/').replyWithError('testing retry'); nock(url).get('/').replyWithError('testing retry');
nock(url).get('/').reply(200); nock(url).get('/').reply(200);
@ -36,6 +39,7 @@ test('Does not throw if response is error', async () => {
const addon = new SlackAddon({ const addon = new SlackAddon({
getLogger: noLogger, getLogger: noLogger,
unleashUrl: url, unleashUrl: url,
featureTagStore: new FakeFeatureTagStore(),
}); });
nock(url).get('/').twice().replyWithError('testing retry'); nock(url).get('/').twice().replyWithError('testing retry');
const res = await addon.fetchRetry(url); const res = await addon.fetchRetry(url);
@ -47,6 +51,7 @@ test('Supports custom number of retries', async () => {
const addon = new SlackAddon({ const addon = new SlackAddon({
getLogger: noLogger, getLogger: noLogger,
unleashUrl: url, unleashUrl: url,
featureTagStore: new FakeFeatureTagStore(),
}); });
let retries = 0; let retries = 0;
nock(url).get('/').twice().replyWithError('testing retry'); nock(url).get('/').twice().replyWithError('testing retry');

View File

@ -9,6 +9,7 @@ import { Logger } from '../logger';
import DatadogAddon from './datadog'; import DatadogAddon from './datadog';
import noLogger from '../../test/fixtures/no-logger'; import noLogger from '../../test/fixtures/no-logger';
import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store';
let fetchRetryCalls: any[] = []; let fetchRetryCalls: any[] = [];
@ -39,6 +40,7 @@ test('Should call datadog webhook', async () => {
const addon = new DatadogAddon({ const addon = new DatadogAddon({
getLogger: noLogger, getLogger: noLogger,
unleashUrl: 'http://some-url.com', unleashUrl: 'http://some-url.com',
featureTagStore: new FakeFeatureTagStore(),
}); });
const event: IEvent = { const event: IEvent = {
id: 1, id: 1,
@ -68,6 +70,7 @@ test('Should call datadog webhook for archived toggle', async () => {
const addon = new DatadogAddon({ const addon = new DatadogAddon({
getLogger: noLogger, getLogger: noLogger,
unleashUrl: 'http://some-url.com', unleashUrl: 'http://some-url.com',
featureTagStore: new FakeFeatureTagStore(),
}); });
const event: IEvent = { const event: IEvent = {
id: 2, id: 2,
@ -95,6 +98,7 @@ test('Should call datadog webhook for archived toggle with project info', async
const addon = new DatadogAddon({ const addon = new DatadogAddon({
getLogger: noLogger, getLogger: noLogger,
unleashUrl: 'http://some-url.com', unleashUrl: 'http://some-url.com',
featureTagStore: new FakeFeatureTagStore(),
}); });
const event: IEvent = { const event: IEvent = {
id: 2, id: 2,
@ -123,6 +127,7 @@ test(`Should call datadog webhook for toggled environment`, async () => {
const addon = new DatadogAddon({ const addon = new DatadogAddon({
getLogger: noLogger, getLogger: noLogger,
unleashUrl: 'http://some-url.com', unleashUrl: 'http://some-url.com',
featureTagStore: new FakeFeatureTagStore(),
}); });
const event: IEvent = { const event: IEvent = {
id: 2, id: 2,
@ -153,6 +158,7 @@ test(`Should include customHeaders in headers when calling service`, async () =>
const addon = new DatadogAddon({ const addon = new DatadogAddon({
getLogger: noLogger, getLogger: noLogger,
unleashUrl: 'http://some-url.com', unleashUrl: 'http://some-url.com',
featureTagStore: new FakeFeatureTagStore(),
}); });
const event: IEvent = { const event: IEvent = {
id: 2, id: 2,
@ -184,6 +190,7 @@ test(`Should not include source_type_name when included in the config`, async ()
const addon = new DatadogAddon({ const addon = new DatadogAddon({
getLogger: noLogger, getLogger: noLogger,
unleashUrl: 'http://some-url.com', unleashUrl: 'http://some-url.com',
featureTagStore: new FakeFeatureTagStore(),
}); });
const event: IEvent = { const event: IEvent = {
id: 2, id: 2,

View File

@ -8,6 +8,7 @@ import {
LinkStyle, LinkStyle,
} from './feature-event-formatter-md'; } from './feature-event-formatter-md';
import { IEvent } from '../types/events'; import { IEvent } from '../types/events';
import { IFeatureTagStore, ITag } from '../types';
interface IDatadogParameters { interface IDatadogParameters {
url: string; url: string;
@ -23,15 +24,22 @@ interface DDRequestBody {
source_type_name?: string; source_type_name?: string;
} }
interface IDatadogAddonConfig extends IAddonConfig {
featureTagStore: IFeatureTagStore;
}
export default class DatadogAddon extends Addon { export default class DatadogAddon extends Addon {
private msgFormatter: FeatureEventFormatter; private msgFormatter: FeatureEventFormatter;
constructor(config: IAddonConfig) { private featureTagStore: IFeatureTagStore;
constructor(config: IDatadogAddonConfig) {
super(definition, config); super(definition, config);
this.msgFormatter = new FeatureEventFormatterMd( this.msgFormatter = new FeatureEventFormatterMd(
config.unleashUrl, config.unleashUrl,
LinkStyle.MD, LinkStyle.MD,
); );
this.featureTagStore = config.featureTagStore;
} }
async handleEvent( async handleEvent(
@ -47,7 +55,8 @@ export default class DatadogAddon extends Addon {
const text = this.msgFormatter.format(event); const text = this.msgFormatter.format(event);
const { tags: eventTags } = event; const eventTags = await this.getFeatureTags(event);
const tags = const tags =
eventTags && eventTags.map((tag) => `${tag.type}:${tag.value}`); eventTags && eventTags.map((tag) => `${tag.type}:${tag.value}`);
const body: DDRequestBody = { const body: DDRequestBody = {
@ -82,4 +91,13 @@ export default class DatadogAddon extends Addon {
`Handled event ${event.type}. Status codes=${res.status}`, `Handled event ${event.type}. Status codes=${res.status}`,
); );
} }
async getFeatureTags({
featureName,
}: Pick<IEvent, 'featureName'>): Promise<ITag[]> {
if (featureName) {
return this.featureTagStore.getAllTagsForFeature(featureName);
}
return [];
}
} }

View File

@ -52,7 +52,6 @@ const testCases: [string, IEvent, string][] = [
}, },
constraints: [], constraints: [],
}, },
tags: [],
featureName: 'new-feature', featureName: 'new-feature',
project: 'my-other-project', project: 'my-other-project',
environment: 'production', environment: 'production',
@ -86,7 +85,6 @@ const testCases: [string, IEvent, string][] = [
}, },
constraints: [], constraints: [],
}, },
tags: [],
featureName: 'new-feature', featureName: 'new-feature',
project: 'my-other-project', project: 'my-other-project',
environment: 'production', environment: 'production',
@ -120,7 +118,6 @@ const testCases: [string, IEvent, string][] = [
}, },
constraints: [], constraints: [],
}, },
tags: [],
featureName: 'new-feature', featureName: 'new-feature',
project: 'my-other-project', project: 'my-other-project',
environment: 'production', environment: 'production',
@ -162,7 +159,6 @@ const testCases: [string, IEvent, string][] = [
}, },
constraints: [], constraints: [],
}, },
tags: [],
featureName: 'new-feature', featureName: 'new-feature',
project: 'my-other-project', project: 'my-other-project',
environment: 'production', environment: 'production',
@ -196,7 +192,6 @@ const testCases: [string, IEvent, string][] = [
}, },
constraints: [], constraints: [],
}, },
tags: [],
featureName: 'new-feature', featureName: 'new-feature',
project: 'my-other-project', project: 'my-other-project',
environment: 'production', environment: 'production',
@ -221,7 +216,6 @@ const testCases: [string, IEvent, string][] = [
}, },
}, },
preData: null, preData: null,
tags: [],
featureName: 'new-feature', featureName: 'new-feature',
project: 'my-other-project', project: 'my-other-project',
environment: 'production', environment: 'production',
@ -242,7 +236,6 @@ const testCases: [string, IEvent, string][] = [
parameters: {}, parameters: {},
constraints: [], constraints: [],
}, },
tags: [],
featureName: 'new-feature', featureName: 'new-feature',
project: 'my-other-project', project: 'my-other-project',
environment: 'production', environment: 'production',
@ -293,7 +286,6 @@ const testCases: [string, IEvent, string][] = [
parameters: {}, parameters: {},
constraints: [], constraints: [],
}, },
tags: [],
featureName: 'aaa', featureName: 'aaa',
project: 'default', project: 'default',
environment: 'production', environment: 'production',
@ -345,7 +337,6 @@ const testCases: [string, IEvent, string][] = [
}, },
], ],
}, },
tags: [],
featureName: 'aaa', featureName: 'aaa',
project: 'default', project: 'default',
environment: 'production', environment: 'production',
@ -386,7 +377,6 @@ const testCases: [string, IEvent, string][] = [
sortOrder: 9999, sortOrder: 9999,
id: '9a995d94-5944-4897-a82f-0f7e65c2fb3f', id: '9a995d94-5944-4897-a82f-0f7e65c2fb3f',
}, },
tags: [],
featureName: 'new-feature', featureName: 'new-feature',
project: 'my-other-project', project: 'my-other-project',
environment: 'production', environment: 'production',
@ -422,7 +412,6 @@ const testCases: [string, IEvent, string][] = [
IPs: '', IPs: '',
}, },
}, },
tags: [],
featureName: 'new-feature', featureName: 'new-feature',
project: 'my-other-project', project: 'my-other-project',
environment: 'production', environment: 'production',
@ -458,7 +447,6 @@ const testCases: [string, IEvent, string][] = [
hostNames: '', hostNames: '',
}, },
}, },
tags: [],
featureName: 'new-feature', featureName: 'new-feature',
project: 'my-other-project', project: 'my-other-project',
environment: 'production', environment: 'production',
@ -494,7 +482,6 @@ const testCases: [string, IEvent, string][] = [
IPs: '', IPs: '',
}, },
}, },
tags: [],
featureName: 'new-feature', featureName: 'new-feature',
project: 'my-other-project', project: 'my-other-project',
environment: 'production', environment: 'production',

View File

@ -5,7 +5,7 @@ import DatadogAddon from './datadog';
import Addon from './addon'; import Addon from './addon';
import { LogProvider } from '../logger'; import { LogProvider } from '../logger';
import SlackAppAddon from './slack-app'; import SlackAppAddon from './slack-app';
import { IFlagResolver } from '../types'; import { IFeatureTagStore, IFlagResolver } from '../types';
export interface IAddonProviders { export interface IAddonProviders {
[key: string]: Addon; [key: string]: Addon;
@ -15,10 +15,20 @@ export const getAddons: (args: {
getLogger: LogProvider; getLogger: LogProvider;
unleashUrl: string; unleashUrl: string;
flagResolver: IFlagResolver; flagResolver: IFlagResolver;
}) => IAddonProviders = ({ getLogger, unleashUrl, flagResolver }) => { featureTagStore: IFeatureTagStore;
}) => IAddonProviders = ({
getLogger,
unleashUrl,
flagResolver,
featureTagStore,
}) => {
const slackAppAddonEnabled = flagResolver.isEnabled('slackAppAddon'); const slackAppAddonEnabled = flagResolver.isEnabled('slackAppAddon');
const slackAddon = new SlackAddon({ getLogger, unleashUrl }); const slackAddon = new SlackAddon({
getLogger,
unleashUrl,
featureTagStore,
});
if (slackAppAddonEnabled) { if (slackAppAddonEnabled) {
slackAddon.definition.deprecated = slackAddon.definition.deprecated =
@ -29,11 +39,13 @@ export const getAddons: (args: {
new Webhook({ getLogger }), new Webhook({ getLogger }),
slackAddon, slackAddon,
new TeamsAddon({ getLogger, unleashUrl }), new TeamsAddon({ getLogger, unleashUrl }),
new DatadogAddon({ getLogger, unleashUrl }), new DatadogAddon({ getLogger, unleashUrl, featureTagStore }),
]; ];
if (slackAppAddonEnabled) { if (slackAppAddonEnabled) {
addons.push(new SlackAppAddon({ getLogger, unleashUrl })); addons.push(
new SlackAppAddon({ getLogger, unleashUrl, featureTagStore }),
);
} }
return addons.reduce((map, addon) => { return addons.reduce((map, addon) => {

View File

@ -1,3 +1,4 @@
import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store';
import { IEvent, FEATURE_ENVIRONMENT_ENABLED } from '../types/events'; import { IEvent, FEATURE_ENVIRONMENT_ENABLED } from '../types/events';
import SlackAppAddon from './slack-app'; import SlackAppAddon from './slack-app';
import { ChatPostMessageArguments, ErrorCode } from '@slack/web-api'; import { ChatPostMessageArguments, ErrorCode } from '@slack/web-api';
@ -35,11 +36,45 @@ describe('SlackAppAddon', () => {
fatal: jest.fn(), fatal: jest.fn(),
}; };
const getLogger = jest.fn(() => loggerMock); const getLogger = jest.fn(() => loggerMock);
const featureTagStore = new FakeFeatureTagStore();
const mockError = { const mockError = {
code: ErrorCode.PlatformError, code: ErrorCode.PlatformError,
data: 'Platform error message', data: 'Platform error message',
}; };
featureTagStore.tagFeatures([
{
featureName: 'some-toggle',
tagType: 'slack',
tagValue: 'general',
},
{
featureName: 'toggle-2tags',
tagType: 'slack',
tagValue: 'general',
},
{
featureName: 'toggle-2tags',
tagType: 'slack',
tagValue: 'another-channel-1',
},
{
featureName: 'toggle-3tags',
tagType: 'slack',
tagValue: 'general',
},
{
featureName: 'toggle-3tags',
tagType: 'slack',
tagValue: 'another-channel-1',
},
{
featureName: 'toggle-3tags',
tagType: 'slack',
tagValue: 'another-channel-2',
},
]);
const event: IEvent = { const event: IEvent = {
id: 1, id: 1,
createdAt: new Date(), createdAt: new Date(),
@ -54,7 +89,6 @@ describe('SlackAppAddon', () => {
type: 'release', type: 'release',
strategies: [{ name: 'default' }], strategies: [{ name: 'default' }],
}, },
tags: [{ type: 'slack', value: 'general' }],
}; };
beforeEach(() => { beforeEach(() => {
@ -64,6 +98,7 @@ describe('SlackAppAddon', () => {
addon = new SlackAppAddon({ addon = new SlackAppAddon({
getLogger, getLogger,
unleashUrl: 'http://some-url.com', unleashUrl: 'http://some-url.com',
featureTagStore: featureTagStore,
}); });
}); });
@ -81,10 +116,7 @@ describe('SlackAppAddon', () => {
it('should post to all channels in tags', async () => { it('should post to all channels in tags', async () => {
const eventWith2Tags: IEvent = { const eventWith2Tags: IEvent = {
...event, ...event,
tags: [ featureName: 'toggle-2tags',
{ type: 'slack', value: 'general' },
{ type: 'slack', value: 'another-channel-1' },
],
}; };
await addon.handleEvent(eventWith2Tags, { accessToken }); await addon.handleEvent(eventWith2Tags, { accessToken });
@ -97,7 +129,7 @@ describe('SlackAppAddon', () => {
it('should not post a message if there are no tagged channels and no defaultChannels', async () => { it('should not post a message if there are no tagged channels and no defaultChannels', async () => {
const eventWithoutTags: IEvent = { const eventWithoutTags: IEvent = {
...event, ...event,
tags: [], featureName: 'toggle-no-tags',
}; };
await addon.handleEvent(eventWithoutTags, { await addon.handleEvent(eventWithoutTags, {
@ -110,7 +142,7 @@ describe('SlackAppAddon', () => {
it('should use defaultChannels if no tagged channels are found', async () => { it('should use defaultChannels if no tagged channels are found', async () => {
const eventWithoutTags: IEvent = { const eventWithoutTags: IEvent = {
...event, ...event,
tags: [], featureName: 'toggle-no-tags',
}; };
await addon.handleEvent(eventWithoutTags, { await addon.handleEvent(eventWithoutTags, {
@ -137,11 +169,7 @@ describe('SlackAppAddon', () => {
it('should handle rejections in chat.postMessage', async () => { it('should handle rejections in chat.postMessage', async () => {
const eventWith3Tags: IEvent = { const eventWith3Tags: IEvent = {
...event, ...event,
tags: [ featureName: 'toggle-3tags',
{ type: 'slack', value: 'general' },
{ type: 'slack', value: 'another-channel-1' },
{ type: 'slack', value: 'another-channel-2' },
],
}; };
postMessage = jest postMessage = jest

View File

@ -11,13 +11,14 @@ import {
import Addon from './addon'; import Addon from './addon';
import slackAppDefinition from './slack-app-definition'; import slackAppDefinition from './slack-app-definition';
import { IAddonConfig } from '../types/model'; import { IAddonConfig, ITag } from '../types/model';
import { import {
FeatureEventFormatter, FeatureEventFormatter,
FeatureEventFormatterMd, FeatureEventFormatterMd,
LinkStyle, LinkStyle,
} from './feature-event-formatter-md'; } from './feature-event-formatter-md';
import { IEvent } from '../types/events'; import { IEvent } from '../types/events';
import { IFeatureTagStore } from '../types';
interface ISlackAppAddonParameters { interface ISlackAppAddonParameters {
accessToken: string; accessToken: string;
@ -25,6 +26,10 @@ interface ISlackAppAddonParameters {
alwaysPostToDefault: string; alwaysPostToDefault: string;
} }
interface ISlackAppAddonConfig extends IAddonConfig {
featureTagStore: IFeatureTagStore;
}
export default class SlackAppAddon extends Addon { export default class SlackAppAddon extends Addon {
private msgFormatter: FeatureEventFormatter; private msgFormatter: FeatureEventFormatter;
@ -32,12 +37,15 @@ export default class SlackAppAddon extends Addon {
private slackClient?: WebClient; private slackClient?: WebClient;
constructor(args: IAddonConfig) { private featureTagStore: IFeatureTagStore;
constructor(args: ISlackAppAddonConfig) {
super(slackAppDefinition, args); super(slackAppDefinition, args);
this.msgFormatter = new FeatureEventFormatterMd( this.msgFormatter = new FeatureEventFormatterMd(
args.unleashUrl, args.unleashUrl,
LinkStyle.SLACK, LinkStyle.SLACK,
); );
this.featureTagStore = args.featureTagStore;
} }
async handleEvent( async handleEvent(
@ -56,7 +64,8 @@ export default class SlackAppAddon extends Addon {
alwaysPostToDefault === 'true' || alwaysPostToDefault === 'yes'; alwaysPostToDefault === 'true' || alwaysPostToDefault === 'yes';
this.logger.debug(`Post to default was set to ${postToDefault}`); this.logger.debug(`Post to default was set to ${postToDefault}`);
const taggedChannels = this.findTaggedChannels(event); const taggedChannels = await this.findTaggedSlackChannels(event);
let eventChannels: string[]; let eventChannels: string[];
if (postToDefault) { if (postToDefault) {
eventChannels = taggedChannels.concat( eventChannels = taggedChannels.concat(
@ -139,8 +148,18 @@ export default class SlackAppAddon extends Addon {
} }
} }
findTaggedChannels({ tags }: Pick<IEvent, 'tags'>): string[] { async getFeatureTags(featureName?: string): Promise<ITag[]> {
if (tags) { if (featureName) {
return this.featureTagStore.getAllTagsForFeature(featureName);
}
return [];
}
async findTaggedSlackChannels({
featureName,
}: Pick<IEvent, 'featureName'>): Promise<string[]> {
const tags = await this.getFeatureTags(featureName);
if (tags.length) {
return tags return tags
.filter((tag) => tag.type === 'slack') .filter((tag) => tag.type === 'slack')
.map((t) => t.value); .map((t) => t.value);

View File

@ -9,6 +9,9 @@ import { Logger } from '../logger';
import SlackAddon from './slack'; import SlackAddon from './slack';
import noLogger from '../../test/fixtures/no-logger'; import noLogger from '../../test/fixtures/no-logger';
import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store';
const featureTagStore = new FakeFeatureTagStore();
let fetchRetryCalls: any[] = []; let fetchRetryCalls: any[] = [];
@ -35,10 +38,15 @@ jest.mock(
}, },
); );
beforeEach(() => {
featureTagStore.deleteAll();
});
test('Should call slack webhook', async () => { test('Should call slack webhook', async () => {
const addon = new SlackAddon({ const addon = new SlackAddon({
getLogger: noLogger, getLogger: noLogger,
unleashUrl: 'http://some-url.com', unleashUrl: 'http://some-url.com',
featureTagStore,
}); });
const event: IEvent = { const event: IEvent = {
id: 1, id: 1,
@ -70,6 +78,7 @@ test('Should call slack webhook for archived toggle', async () => {
const addon = new SlackAddon({ const addon = new SlackAddon({
getLogger: noLogger, getLogger: noLogger,
unleashUrl: 'http://some-url.com', unleashUrl: 'http://some-url.com',
featureTagStore,
}); });
const event: IEvent = { const event: IEvent = {
id: 2, id: 2,
@ -97,6 +106,7 @@ test('Should call slack webhook for archived toggle with project info', async ()
const addon = new SlackAddon({ const addon = new SlackAddon({
getLogger: noLogger, getLogger: noLogger,
unleashUrl: 'http://some-url.com', unleashUrl: 'http://some-url.com',
featureTagStore,
}); });
const event: IEvent = { const event: IEvent = {
id: 2, id: 2,
@ -125,6 +135,7 @@ test(`Should call webhook for toggled environment`, async () => {
const addon = new SlackAddon({ const addon = new SlackAddon({
getLogger: noLogger, getLogger: noLogger,
unleashUrl: 'http://some-url.com', unleashUrl: 'http://some-url.com',
featureTagStore,
}); });
const event: IEvent = { const event: IEvent = {
id: 2, id: 2,
@ -155,6 +166,7 @@ test('Should use default channel', async () => {
const addon = new SlackAddon({ const addon = new SlackAddon({
getLogger: noLogger, getLogger: noLogger,
unleashUrl: 'http://some-url.com', unleashUrl: 'http://some-url.com',
featureTagStore,
}); });
const event: IEvent = { const event: IEvent = {
id: 3, id: 3,
@ -185,6 +197,7 @@ test('Should override default channel with data from tag', async () => {
const addon = new SlackAddon({ const addon = new SlackAddon({
getLogger: noLogger, getLogger: noLogger,
unleashUrl: 'http://some-url.com', unleashUrl: 'http://some-url.com',
featureTagStore,
}); });
const event: IEvent = { const event: IEvent = {
id: 4, id: 4,
@ -197,13 +210,12 @@ test('Should override default channel with data from tag', async () => {
enabled: false, enabled: false,
strategies: [{ name: 'default' }], strategies: [{ name: 'default' }],
}, },
tags: [ };
{
featureTagStore.tagFeature('some-toggle', {
type: 'slack', type: 'slack',
value: 'another-channel', value: 'another-channel',
}, });
],
};
const parameters = { const parameters = {
url: 'http://hooks.slack.com', url: 'http://hooks.slack.com',
@ -221,6 +233,7 @@ test('Should post to all channels in tags', async () => {
const addon = new SlackAddon({ const addon = new SlackAddon({
getLogger: noLogger, getLogger: noLogger,
unleashUrl: 'http://some-url.com', unleashUrl: 'http://some-url.com',
featureTagStore,
}); });
const event: IEvent = { const event: IEvent = {
id: 5, id: 5,
@ -233,18 +246,21 @@ test('Should post to all channels in tags', async () => {
enabled: false, enabled: false,
strategies: [{ name: 'default' }], strategies: [{ name: 'default' }],
}, },
tags: [
{
type: 'slack',
value: 'another-channel-1',
},
{
type: 'slack',
value: 'another-channel-2',
},
],
}; };
featureTagStore.tagFeatures([
{
featureName: 'some-toggle',
tagType: 'slack',
tagValue: 'another-channel-1',
},
{
featureName: 'some-toggle',
tagType: 'slack',
tagValue: 'another-channel-2',
},
]);
const parameters = { const parameters = {
url: 'http://hooks.slack.com', url: 'http://hooks.slack.com',
defaultChannel: 'some-channel', defaultChannel: 'some-channel',
@ -264,6 +280,7 @@ test('Should include custom headers from parameters in call to service', async (
const addon = new SlackAddon({ const addon = new SlackAddon({
getLogger: noLogger, getLogger: noLogger,
unleashUrl: 'http://some-url.com', unleashUrl: 'http://some-url.com',
featureTagStore,
}); });
const event: IEvent = { const event: IEvent = {
id: 2, id: 2,

View File

@ -1,7 +1,7 @@
import Addon from './addon'; import Addon from './addon';
import slackDefinition from './slack-definition'; import slackDefinition from './slack-definition';
import { IAddonConfig } from '../types/model'; import { IAddonConfig, ITag } from '../types/model';
import { import {
FeatureEventFormatter, FeatureEventFormatter,
@ -9,6 +9,7 @@ import {
LinkStyle, LinkStyle,
} from './feature-event-formatter-md'; } from './feature-event-formatter-md';
import { IEvent } from '../types/events'; import { IEvent } from '../types/events';
import { IFeatureTagStore } from '../types';
interface ISlackAddonParameters { interface ISlackAddonParameters {
url: string; url: string;
@ -17,15 +18,23 @@ interface ISlackAddonParameters {
emojiIcon?: string; emojiIcon?: string;
customHeaders?: string; customHeaders?: string;
} }
interface ISlackAddonConfig extends IAddonConfig {
featureTagStore: IFeatureTagStore;
}
export default class SlackAddon extends Addon { export default class SlackAddon extends Addon {
private msgFormatter: FeatureEventFormatter; private msgFormatter: FeatureEventFormatter;
constructor(args: IAddonConfig) { private featureTagStore: IFeatureTagStore;
constructor(args: ISlackAddonConfig) {
super(slackDefinition, args); super(slackDefinition, args);
this.msgFormatter = new FeatureEventFormatterMd( this.msgFormatter = new FeatureEventFormatterMd(
args.unleashUrl, args.unleashUrl,
LinkStyle.SLACK, LinkStyle.SLACK,
); );
this.featureTagStore = args.featureTagStore;
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@ -41,7 +50,7 @@ export default class SlackAddon extends Addon {
customHeaders, customHeaders,
} = parameters; } = parameters;
const slackChannels = this.findSlackChannels(event); const slackChannels = await this.findTaggedSlackChannels(event);
if (slackChannels.length === 0) { if (slackChannels.length === 0) {
slackChannels.push(defaultChannel); slackChannels.push(defaultChannel);
@ -98,8 +107,18 @@ export default class SlackAddon extends Addon {
this.logger.info(`Handled event ${event.type}. Status codes=${codes}`); this.logger.info(`Handled event ${event.type}. Status codes=${codes}`);
} }
findSlackChannels({ tags }: Pick<IEvent, 'tags'>): string[] { async getFeatureTags(featureName?: string): Promise<ITag[]> {
if (tags) { if (featureName) {
return this.featureTagStore.getAllTagsForFeature(featureName);
}
return [];
}
async findTaggedSlackChannels({
featureName,
}: Pick<IEvent, 'featureName'>): Promise<string[]> {
const tags = await this.getFeatureTags(featureName);
if (tags.length) {
return tags return tags
.filter((tag) => tag.type === 'slack') .filter((tag) => tag.type === 'slack')
.map((t) => t.value); .map((t) => t.value);

View File

@ -6,7 +6,6 @@ import {
} from '../types/events'; } from '../types/events';
import { LogProvider, Logger } from '../logger'; import { LogProvider, Logger } from '../logger';
import { IEventStore } from '../types/stores/event-store'; import { IEventStore } from '../types/stores/event-store';
import { ITag } from '../types/model';
import { SearchEventsSchema } from '../openapi/spec/search-events-schema'; import { SearchEventsSchema } from '../openapi/spec/search-events-schema';
import { sharedEventEmitter } from '../util/anyEventEmitter'; import { sharedEventEmitter } from '../util/anyEventEmitter';
import { Db } from './db'; import { Db } from './db';
@ -20,7 +19,6 @@ const EVENT_COLUMNS = [
'created_at', 'created_at',
'data', 'data',
'pre_data', 'pre_data',
'tags',
'feature_name', 'feature_name',
'project', 'project',
'environment', 'environment',
@ -77,7 +75,6 @@ export interface IEventTable {
feature_name?: string; feature_name?: string;
project?: string; project?: string;
environment?: string; environment?: string;
tags: ITag[];
} }
const TABLE = 'events'; const TABLE = 'events';
@ -342,7 +339,6 @@ class EventStore implements IEventStore {
.orWhereRaw('type::text ILIKE ?', `%${search.query}%`) .orWhereRaw('type::text ILIKE ?', `%${search.query}%`)
.orWhereRaw('created_by::text ILIKE ?', `%${search.query}%`) .orWhereRaw('created_by::text ILIKE ?', `%${search.query}%`)
.orWhereRaw('data::text ILIKE ?', `%${search.query}%`) .orWhereRaw('data::text ILIKE ?', `%${search.query}%`)
.orWhereRaw('tags::text ILIKE ?', `%${search.query}%`)
.orWhereRaw('pre_data::text ILIKE ?', `%${search.query}%`), .orWhereRaw('pre_data::text ILIKE ?', `%${search.query}%`),
); );
} }
@ -362,7 +358,6 @@ class EventStore implements IEventStore {
createdAt: row.created_at, createdAt: row.created_at,
data: row.data, data: row.data,
preData: row.pre_data, preData: row.pre_data,
tags: row.tags || [],
featureName: row.feature_name, featureName: row.feature_name,
project: row.project, project: row.project,
environment: row.environment, environment: row.environment,
@ -377,8 +372,6 @@ class EventStore implements IEventStore {
pre_data: Array.isArray(e.preData) pre_data: Array.isArray(e.preData)
? JSON.stringify(e.preData) ? JSON.stringify(e.preData)
: e.preData, : e.preData,
// @ts-expect-error workaround for json-array
tags: JSON.stringify(e.tags),
feature_name: e.featureName, feature_name: e.featureName,
project: e.project, project: e.project,
environment: e.environment, environment: e.environment,

View File

@ -7,7 +7,6 @@ import FeatureStrategiesStore from '../../db/feature-strategy-store';
import FeatureToggleStore from '../../db/feature-toggle-store'; import FeatureToggleStore from '../../db/feature-toggle-store';
import FeatureToggleClientStore from '../../db/feature-toggle-client-store'; import FeatureToggleClientStore from '../../db/feature-toggle-client-store';
import ProjectStore from '../../db/project-store'; import ProjectStore from '../../db/project-store';
import FeatureTagStore from '../../db/feature-tag-store';
import { FeatureEnvironmentStore } from '../../db/feature-environment-store'; import { FeatureEnvironmentStore } from '../../db/feature-environment-store';
import ContextFieldStore from '../../db/context-field-store'; import ContextFieldStore from '../../db/context-field-store';
import GroupStore from '../../db/group-store'; import GroupStore from '../../db/group-store';
@ -22,7 +21,6 @@ import FakeFeatureStrategiesStore from '../../../test/fixtures/fake-feature-stra
import FakeFeatureToggleStore from '../../../test/fixtures/fake-feature-toggle-store'; import FakeFeatureToggleStore from '../../../test/fixtures/fake-feature-toggle-store';
import FakeFeatureToggleClientStore from '../../../test/fixtures/fake-feature-toggle-client-store'; import FakeFeatureToggleClientStore from '../../../test/fixtures/fake-feature-toggle-client-store';
import FakeProjectStore from '../../../test/fixtures/fake-project-store'; import FakeProjectStore from '../../../test/fixtures/fake-project-store';
import FakeFeatureTagStore from '../../../test/fixtures/fake-feature-tag-store';
import FakeFeatureEnvironmentStore from '../../../test/fixtures/fake-feature-environment-store'; import FakeFeatureEnvironmentStore from '../../../test/fixtures/fake-feature-environment-store';
import FakeContextFieldStore from '../../../test/fixtures/fake-context-field-store'; import FakeContextFieldStore from '../../../test/fixtures/fake-context-field-store';
import FakeGroupStore from '../../../test/fixtures/fake-group-store'; import FakeGroupStore from '../../../test/fixtures/fake-group-store';
@ -66,7 +64,6 @@ export const createFeatureToggleService = (
getLogger, getLogger,
flagResolver, flagResolver,
); );
const featureTagStore = new FeatureTagStore(db, eventBus, getLogger);
const featureEnvironmentStore = new FeatureEnvironmentStore( const featureEnvironmentStore = new FeatureEnvironmentStore(
db, db,
eventBus, eventBus,
@ -105,7 +102,6 @@ export const createFeatureToggleService = (
featureToggleClientStore, featureToggleClientStore,
projectStore, projectStore,
eventStore, eventStore,
featureTagStore,
featureEnvironmentStore, featureEnvironmentStore,
contextFieldStore, contextFieldStore,
strategyStore, strategyStore,
@ -128,7 +124,6 @@ export const createFakeFeatureToggleService = (
const featureToggleStore = new FakeFeatureToggleStore(); const featureToggleStore = new FakeFeatureToggleStore();
const featureToggleClientStore = new FakeFeatureToggleClientStore(); const featureToggleClientStore = new FakeFeatureToggleClientStore();
const projectStore = new FakeProjectStore(); const projectStore = new FakeProjectStore();
const featureTagStore = new FakeFeatureTagStore();
const featureEnvironmentStore = new FakeFeatureEnvironmentStore(); const featureEnvironmentStore = new FakeFeatureEnvironmentStore();
const contextFieldStore = new FakeContextFieldStore(); const contextFieldStore = new FakeContextFieldStore();
const groupStore = new FakeGroupStore(); const groupStore = new FakeGroupStore();
@ -154,7 +149,6 @@ export const createFakeFeatureToggleService = (
featureToggleClientStore, featureToggleClientStore,
projectStore, projectStore,
eventStore, eventStore,
featureTagStore,
featureEnvironmentStore, featureEnvironmentStore,
contextFieldStore, contextFieldStore,
strategyStore, strategyStore,

View File

@ -45,7 +45,6 @@ test('should get events list via admin', async () => {
data: { name: 'test', project: 'default' }, data: { name: 'test', project: 'default' },
featureName: 'test', featureName: 'test',
project: 'default', project: 'default',
tags: [],
}), }),
); );
const { body } = await request const { body } = await request
@ -65,7 +64,6 @@ test('should anonymise events list via admin', async () => {
data: { name: 'test', project: 'default' }, data: { name: 'test', project: 'default' },
featureName: 'test', featureName: 'test',
project: 'default', project: 'default',
tags: [],
}), }),
); );
const { body } = await request const { body } = await request

View File

@ -45,9 +45,13 @@ export default class AddonService {
addonStore, addonStore,
eventStore, eventStore,
featureToggleStore, featureToggleStore,
featureTagStore,
}: Pick< }: Pick<
IUnleashStores, IUnleashStores,
'addonStore' | 'eventStore' | 'featureToggleStore' | 'addonStore'
| 'eventStore'
| 'featureToggleStore'
| 'featureTagStore'
>, >,
{ {
getLogger, getLogger,
@ -69,6 +73,7 @@ export default class AddonService {
getLogger, getLogger,
unleashUrl: server.unleashUrl, unleashUrl: server.unleashUrl,
flagResolver, flagResolver,
featureTagStore,
}); });
this.sensitiveParams = this.loadSensitiveParams(this.addonProviders); this.sensitiveParams = this.loadSensitiveParams(this.addonProviders);
if (addonStore) { if (addonStore) {

View File

@ -25,7 +25,6 @@ import {
IFeatureEnvironmentStore, IFeatureEnvironmentStore,
IFeatureOverview, IFeatureOverview,
IFeatureStrategy, IFeatureStrategy,
IFeatureTagStore,
IFeatureToggleClientStore, IFeatureToggleClientStore,
IFeatureToggleQuery, IFeatureToggleQuery,
IFeatureToggleStore, IFeatureToggleStore,
@ -136,8 +135,6 @@ class FeatureToggleService {
private featureToggleClientStore: IFeatureToggleClientStore; private featureToggleClientStore: IFeatureToggleClientStore;
private tagStore: IFeatureTagStore;
private featureEnvironmentStore: IFeatureEnvironmentStore; private featureEnvironmentStore: IFeatureEnvironmentStore;
private projectStore: IProjectStore; private projectStore: IProjectStore;
@ -161,7 +158,6 @@ class FeatureToggleService {
featureToggleClientStore, featureToggleClientStore,
projectStore, projectStore,
eventStore, eventStore,
featureTagStore,
featureEnvironmentStore, featureEnvironmentStore,
contextFieldStore, contextFieldStore,
strategyStore, strategyStore,
@ -172,7 +168,6 @@ class FeatureToggleService {
| 'featureToggleClientStore' | 'featureToggleClientStore'
| 'projectStore' | 'projectStore'
| 'eventStore' | 'eventStore'
| 'featureTagStore'
| 'featureEnvironmentStore' | 'featureEnvironmentStore'
| 'contextFieldStore' | 'contextFieldStore'
| 'strategyStore' | 'strategyStore'
@ -190,7 +185,6 @@ class FeatureToggleService {
this.strategyStore = strategyStore; this.strategyStore = strategyStore;
this.featureToggleStore = featureToggleStore; this.featureToggleStore = featureToggleStore;
this.featureToggleClientStore = featureToggleClientStore; this.featureToggleClientStore = featureToggleClientStore;
this.tagStore = featureTagStore;
this.projectStore = projectStore; this.projectStore = projectStore;
this.eventStore = eventStore; this.eventStore = eventStore;
this.featureEnvironmentStore = featureEnvironmentStore; this.featureEnvironmentStore = featureEnvironmentStore;
@ -382,15 +376,12 @@ class FeatureToggleService {
); );
if (featureToggle.stale !== newDocument.stale) { if (featureToggle.stale !== newDocument.stale) {
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store( await this.eventStore.store(
new FeatureStaleEvent({ new FeatureStaleEvent({
stale: newDocument.stale, stale: newDocument.stale,
project, project,
featureName, featureName,
createdBy, createdBy,
tags,
}), }),
); );
} }
@ -489,8 +480,6 @@ class FeatureToggleService {
.map((strategy) => strategy.id); .map((strategy) => strategy.id);
const eventData: StrategyIds = { strategyIds: newOrder }; const eventData: StrategyIds = { strategyIds: newOrder };
const tags = await this.tagStore.getAllTagsForFeature(featureName);
const event = new StrategiesOrderChangedEvent({ const event = new StrategiesOrderChangedEvent({
featureName, featureName,
environment, environment,
@ -498,7 +487,6 @@ class FeatureToggleService {
createdBy, createdBy,
preData: eventPreData, preData: eventPreData,
data: eventData, data: eventData,
tags: tags,
}); });
await this.eventStore.store(event); await this.eventStore.store(event);
} }
@ -594,8 +582,6 @@ class FeatureToggleService {
segments, segments,
); );
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store( await this.eventStore.store(
new FeatureStrategyAddEvent({ new FeatureStrategyAddEvent({
project: projectId, project: projectId,
@ -603,7 +589,6 @@ class FeatureToggleService {
createdBy, createdBy,
environment, environment,
data: strategy, data: strategy,
tags,
}), }),
); );
return strategy; return strategy;
@ -712,7 +697,6 @@ class FeatureToggleService {
); );
// Store event! // Store event!
const tags = await this.tagStore.getAllTagsForFeature(featureName);
const data = this.featureStrategyToPublic(strategy, segments); const data = this.featureStrategyToPublic(strategy, segments);
const preData = this.featureStrategyToPublic( const preData = this.featureStrategyToPublic(
existingStrategy, existingStrategy,
@ -726,7 +710,6 @@ class FeatureToggleService {
createdBy: userName, createdBy: userName,
data, data,
preData, preData,
tags,
}), }),
); );
await this.optionallyDisableFeature( await this.optionallyDisableFeature(
@ -758,7 +741,6 @@ class FeatureToggleService {
id, id,
existingStrategy, existingStrategy,
); );
const tags = await this.tagStore.getAllTagsForFeature(featureName);
const segments = await this.segmentService.getByStrategy( const segments = await this.segmentService.getByStrategy(
strategy.id, strategy.id,
); );
@ -775,7 +757,6 @@ class FeatureToggleService {
createdBy: userName, createdBy: userName,
data, data,
preData, preData,
tags,
}), }),
); );
return data; return data;
@ -840,7 +821,6 @@ class FeatureToggleService {
); );
} }
const tags = await this.tagStore.getAllTagsForFeature(featureName);
const preData = this.featureStrategyToPublic(existingStrategy); const preData = this.featureStrategyToPublic(existingStrategy);
await this.eventStore.store( await this.eventStore.store(
@ -850,7 +830,6 @@ class FeatureToggleService {
environment, environment,
createdBy, createdBy,
preData, preData,
tags,
}), }),
); );
@ -1085,15 +1064,12 @@ class FeatureToggleService {
); );
} }
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store( await this.eventStore.store(
new FeatureCreatedEvent({ new FeatureCreatedEvent({
featureName, featureName,
createdBy, createdBy,
project: projectId, project: projectId,
data: createdToggle, data: createdToggle,
tags,
}), }),
); );
@ -1243,8 +1219,6 @@ class FeatureToggleService {
name: featureName, name: featureName,
}); });
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store( await this.eventStore.store(
new FeatureMetadataUpdateEvent({ new FeatureMetadataUpdateEvent({
createdBy: userName, createdBy: userName,
@ -1252,7 +1226,6 @@ class FeatureToggleService {
preData, preData,
featureName, featureName,
project: projectId, project: projectId,
tags,
}), }),
); );
return featureToggle; return featureToggle;
@ -1379,7 +1352,6 @@ class FeatureToggleService {
const { project } = feature; const { project } = feature;
feature.stale = isStale; feature.stale = isStale;
await this.featureToggleStore.update(project, feature); await this.featureToggleStore.update(project, feature);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store( await this.eventStore.store(
new FeatureStaleEvent({ new FeatureStaleEvent({
@ -1387,7 +1359,6 @@ class FeatureToggleService {
project, project,
featureName, featureName,
createdBy, createdBy,
tags,
}), }),
); );
@ -1409,13 +1380,12 @@ class FeatureToggleService {
} }
await this.featureToggleStore.archive(featureName); await this.featureToggleStore.archive(featureName);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store( await this.eventStore.store(
new FeatureArchivedEvent({ new FeatureArchivedEvent({
featureName, featureName,
createdBy, createdBy,
project: feature.project, project: feature.project,
tags,
}), }),
); );
} }
@ -1430,8 +1400,9 @@ class FeatureToggleService {
const features = await this.featureToggleStore.getAllByNames( const features = await this.featureToggleStore.getAllByNames(
featureNames, featureNames,
); );
await this.featureToggleStore.batchArchive(featureNames); await this.featureToggleStore.batchArchive(featureNames);
const tags = await this.tagStore.getAllByFeatures(featureNames);
await this.eventStore.batchStore( await this.eventStore.batchStore(
features.map( features.map(
(feature) => (feature) =>
@ -1439,12 +1410,6 @@ class FeatureToggleService {
featureName: feature.name, featureName: feature.name,
createdBy, createdBy,
project: feature.project, project: feature.project,
tags: tags
.filter((tag) => tag.featureName === feature.name)
.map((tag) => ({
value: tag.tagValue,
type: tag.tagType,
})),
}), }),
), ),
); );
@ -1467,8 +1432,9 @@ class FeatureToggleService {
const relevantFeatureNames = relevantFeatures.map( const relevantFeatureNames = relevantFeatures.map(
(feature) => feature.name, (feature) => feature.name,
); );
await this.featureToggleStore.batchStale(relevantFeatureNames, stale); await this.featureToggleStore.batchStale(relevantFeatureNames, stale);
const tags = await this.tagStore.getAllByFeatures(relevantFeatureNames);
await this.eventStore.batchStore( await this.eventStore.batchStore(
relevantFeatures.map( relevantFeatures.map(
(feature) => (feature) =>
@ -1477,12 +1443,6 @@ class FeatureToggleService {
project: projectId, project: projectId,
featureName: feature.name, featureName: feature.name,
createdBy, createdBy,
tags: tags
.filter((tag) => tag.featureName === feature.name)
.map((tag) => ({
value: tag.tagValue,
type: tag.tagType,
})),
}), }),
), ),
); );
@ -1627,7 +1587,6 @@ class FeatureToggleService {
const feature = await this.featureToggleStore.get(featureName); const feature = await this.featureToggleStore.get(featureName);
if (updatedEnvironmentStatus > 0) { if (updatedEnvironmentStatus > 0) {
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store( await this.eventStore.store(
new FeatureEnvironmentEvent({ new FeatureEnvironmentEvent({
enabled, enabled,
@ -1635,7 +1594,6 @@ class FeatureToggleService {
featureName, featureName,
environment, environment,
createdBy, createdBy,
tags,
}), }),
); );
} }
@ -1647,7 +1605,6 @@ class FeatureToggleService {
featureName: string, featureName: string,
createdBy: string, createdBy: string,
): Promise<FeatureToggleLegacy> { ): Promise<FeatureToggleLegacy> {
const tags = await this.tagStore.getAllTagsForFeature(featureName);
const feature = await this.getFeatureToggleLegacy(featureName); const feature = await this.getFeatureToggleLegacy(featureName);
// Legacy event. Will not be used from v4.3. // Legacy event. Will not be used from v4.3.
@ -1657,7 +1614,6 @@ class FeatureToggleService {
createdBy, createdBy,
featureName, featureName,
data: feature, data: feature,
tags,
project: feature.project, project: feature.project,
}); });
return feature; return feature;
@ -1719,14 +1675,12 @@ class FeatureToggleService {
feature.project = newProject; feature.project = newProject;
await this.featureToggleStore.update(newProject, feature); await this.featureToggleStore.update(newProject, feature);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store( await this.eventStore.store(
new FeatureChangeProjectEvent({ new FeatureChangeProjectEvent({
createdBy, createdBy,
oldProject, oldProject,
newProject, newProject,
featureName, featureName,
tags,
}), }),
); );
} }
@ -1738,15 +1692,15 @@ class FeatureToggleService {
// TODO: add project id. // TODO: add project id.
async deleteFeature(featureName: string, createdBy: string): Promise<void> { async deleteFeature(featureName: string, createdBy: string): Promise<void> {
const toggle = await this.featureToggleStore.get(featureName); const toggle = await this.featureToggleStore.get(featureName);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.featureToggleStore.delete(featureName); await this.featureToggleStore.delete(featureName);
await this.eventStore.store( await this.eventStore.store(
new FeatureDeletedEvent({ new FeatureDeletedEvent({
featureName, featureName,
project: toggle.project, project: toggle.project,
createdBy, createdBy,
preData: toggle, preData: toggle,
tags,
}), }),
); );
} }
@ -1767,8 +1721,9 @@ class FeatureToggleService {
const eligibleFeatureNames = eligibleFeatures.map( const eligibleFeatureNames = eligibleFeatures.map(
(toggle) => toggle.name, (toggle) => toggle.name,
); );
const tags = await this.tagStore.getAllByFeatures(eligibleFeatureNames);
await this.featureToggleStore.batchDelete(eligibleFeatureNames); await this.featureToggleStore.batchDelete(eligibleFeatureNames);
await this.eventStore.batchStore( await this.eventStore.batchStore(
eligibleFeatures.map( eligibleFeatures.map(
(feature) => (feature) =>
@ -1777,12 +1732,6 @@ class FeatureToggleService {
createdBy, createdBy,
project: feature.project, project: feature.project,
preData: feature, preData: feature,
tags: tags
.filter((tag) => tag.featureName === feature.name)
.map((tag) => ({
value: tag.tagValue,
type: tag.tagType,
})),
}), }),
), ),
); );
@ -1804,8 +1753,9 @@ class FeatureToggleService {
const eligibleFeatureNames = eligibleFeatures.map( const eligibleFeatureNames = eligibleFeatures.map(
(toggle) => toggle.name, (toggle) => toggle.name,
); );
const tags = await this.tagStore.getAllByFeatures(eligibleFeatureNames);
await this.featureToggleStore.batchRevive(eligibleFeatureNames); await this.featureToggleStore.batchRevive(eligibleFeatureNames);
await this.eventStore.batchStore( await this.eventStore.batchStore(
eligibleFeatures.map( eligibleFeatures.map(
(feature) => (feature) =>
@ -1813,12 +1763,6 @@ class FeatureToggleService {
featureName: feature.name, featureName: feature.name,
createdBy, createdBy,
project: feature.project, project: feature.project,
tags: tags
.filter((tag) => tag.featureName === feature.name)
.map((tag) => ({
value: tag.tagValue,
type: tag.tagType,
})),
}), }),
), ),
); );
@ -1827,13 +1771,12 @@ class FeatureToggleService {
// TODO: add project id. // TODO: add project id.
async reviveFeature(featureName: string, createdBy: string): Promise<void> { async reviveFeature(featureName: string, createdBy: string): Promise<void> {
const toggle = await this.featureToggleStore.revive(featureName); const toggle = await this.featureToggleStore.revive(featureName);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store( await this.eventStore.store(
new FeatureRevivedEvent({ new FeatureRevivedEvent({
createdBy, createdBy,
featureName, featureName,
project: toggle.project, project: toggle.project,
tags,
}), }),
); );
} }
@ -1930,13 +1873,12 @@ class FeatureToggleService {
featureName, featureName,
fixedVariants, fixedVariants,
); );
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store( await this.eventStore.store(
new FeatureVariantEvent({ new FeatureVariantEvent({
project, project,
featureName, featureName,
createdBy, createdBy,
tags,
oldVariants, oldVariants,
newVariants: featureToggle.variants as IVariant[], newVariants: featureToggle.variants as IVariant[],
}), }),
@ -1964,8 +1906,6 @@ class FeatureToggleService {
).variants || ).variants ||
[]; [];
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store( await this.eventStore.store(
new EnvironmentVariantEvent({ new EnvironmentVariantEvent({
featureName, featureName,
@ -1974,7 +1914,6 @@ class FeatureToggleService {
createdBy: user, createdBy: user,
oldVariants: theOldVariants, oldVariants: theOldVariants,
newVariants: fixedVariants, newVariants: fixedVariants,
tags,
}), }),
); );
await this.featureEnvironmentStore.setVariantsToFeatureEnvironments( await this.featureEnvironmentStore.setVariantsToFeatureEnvironments(
@ -2041,8 +1980,6 @@ class FeatureToggleService {
oldVariants[env] = featureEnv.variants || []; oldVariants[env] = featureEnv.variants || [];
} }
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.batchStore( await this.eventStore.batchStore(
environments.map( environments.map(
(environment) => (environment) =>
@ -2053,7 +1990,6 @@ class FeatureToggleService {
createdBy: user, createdBy: user,
oldVariants: oldVariants[environment], oldVariants: oldVariants[environment],
newVariants: fixedVariants, newVariants: fixedVariants,
tags,
}), }),
), ),
); );
@ -2169,9 +2105,6 @@ class FeatureToggleService {
new PotentiallyStaleOnEvent({ new PotentiallyStaleOnEvent({
featureName: name, featureName: name,
project, project,
tags: await this.tagStore.getAllTagsForFeature(
name,
),
}), }),
), ),
), ),

View File

@ -1,5 +1,5 @@
import { extractUsernameFromUser } from '../util'; import { extractUsernameFromUser } from '../util';
import { FeatureToggle, IStrategyConfig, ITag, IVariant } from './model'; import { FeatureToggle, IStrategyConfig, IVariant } from './model';
import { IApiToken } from './models/api-token'; import { IApiToken } from './models/api-token';
import { IUser } from './user'; import { IUser } from './user';
@ -258,7 +258,6 @@ export interface IBaseEvent {
featureName?: string; featureName?: string;
data?: any; data?: any;
preData?: any; preData?: any;
tags?: ITag[];
} }
export interface IEvent extends IBaseEvent { export interface IEvent extends IBaseEvent {
@ -276,22 +275,15 @@ class BaseEvent implements IBaseEvent {
readonly createdBy: string; readonly createdBy: string;
readonly tags: ITag[];
/** /**
* @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization
*/ */
constructor( constructor(type: IEventType, createdBy: string | IUser) {
type: IEventType,
createdBy: string | IUser,
tags: ITag[] = [],
) {
this.type = type; this.type = type;
this.createdBy = this.createdBy =
typeof createdBy === 'string' typeof createdBy === 'string'
? createdBy ? createdBy
: extractUsernameFromUser(createdBy); : extractUsernameFromUser(createdBy);
this.tags = tags;
} }
} }
@ -308,13 +300,8 @@ export class FeatureStaleEvent extends BaseEvent {
project: string; project: string;
featureName: string; featureName: string;
createdBy: string | IUser; createdBy: string | IUser;
tags: ITag[];
}) { }) {
super( super(p.stale ? FEATURE_STALE_ON : FEATURE_STALE_OFF, p.createdBy);
p.stale ? FEATURE_STALE_ON : FEATURE_STALE_OFF,
p.createdBy,
p.tags,
);
this.project = p.project; this.project = p.project;
this.featureName = p.featureName; this.featureName = p.featureName;
} }
@ -336,14 +323,12 @@ export class FeatureEnvironmentEvent extends BaseEvent {
featureName: string; featureName: string;
environment: string; environment: string;
createdBy: string | IUser; createdBy: string | IUser;
tags: ITag[];
}) { }) {
super( super(
p.enabled p.enabled
? FEATURE_ENVIRONMENT_ENABLED ? FEATURE_ENVIRONMENT_ENABLED
: FEATURE_ENVIRONMENT_DISABLED, : FEATURE_ENVIRONMENT_DISABLED,
p.createdBy, p.createdBy,
p.tags,
); );
this.project = p.project; this.project = p.project;
this.featureName = p.featureName; this.featureName = p.featureName;
@ -372,9 +357,8 @@ export class StrategiesOrderChangedEvent extends BaseEvent {
createdBy: string | IUser; createdBy: string | IUser;
data: StrategyIds; data: StrategyIds;
preData: StrategyIds; preData: StrategyIds;
tags: ITag[];
}) { }) {
super(STRATEGY_ORDER_CHANGED, p.createdBy, p.tags); super(STRATEGY_ORDER_CHANGED, p.createdBy);
const { project, featureName, environment, data, preData } = p; const { project, featureName, environment, data, preData } = p;
this.project = project; this.project = project;
this.featureName = featureName; this.featureName = featureName;
@ -400,11 +384,10 @@ export class FeatureVariantEvent extends BaseEvent {
project: string; project: string;
featureName: string; featureName: string;
createdBy: string | IUser; createdBy: string | IUser;
tags: ITag[];
newVariants: IVariant[]; newVariants: IVariant[];
oldVariants: IVariant[]; oldVariants: IVariant[];
}) { }) {
super(FEATURE_VARIANTS_UPDATED, p.createdBy, p.tags); super(FEATURE_VARIANTS_UPDATED, p.createdBy);
this.project = p.project; this.project = p.project;
this.featureName = p.featureName; this.featureName = p.featureName;
this.data = { variants: p.newVariants }; this.data = { variants: p.newVariants };
@ -431,11 +414,10 @@ export class EnvironmentVariantEvent extends BaseEvent {
environment: string; environment: string;
project: string; project: string;
createdBy: string | IUser; createdBy: string | IUser;
tags: ITag[];
newVariants: IVariant[]; newVariants: IVariant[];
oldVariants: IVariant[]; oldVariants: IVariant[];
}) { }) {
super(FEATURE_ENVIRONMENT_VARIANTS_UPDATED, p.createdBy, p.tags); super(FEATURE_ENVIRONMENT_VARIANTS_UPDATED, p.createdBy);
this.featureName = p.featureName; this.featureName = p.featureName;
this.environment = p.environment; this.environment = p.environment;
this.project = p.project; this.project = p.project;
@ -462,9 +444,8 @@ export class FeatureChangeProjectEvent extends BaseEvent {
newProject: string; newProject: string;
featureName: string; featureName: string;
createdBy: string | IUser; createdBy: string | IUser;
tags: ITag[];
}) { }) {
super(FEATURE_PROJECT_CHANGE, p.createdBy, p.tags); super(FEATURE_PROJECT_CHANGE, p.createdBy);
const { newProject, oldProject, featureName } = p; const { newProject, oldProject, featureName } = p;
this.project = newProject; this.project = newProject;
this.featureName = featureName; this.featureName = featureName;
@ -487,9 +468,8 @@ export class FeatureCreatedEvent extends BaseEvent {
featureName: string; featureName: string;
createdBy: string | IUser; createdBy: string | IUser;
data: FeatureToggle; data: FeatureToggle;
tags: ITag[];
}) { }) {
super(FEATURE_CREATED, p.createdBy, p.tags); super(FEATURE_CREATED, p.createdBy);
const { project, featureName, data } = p; const { project, featureName, data } = p;
this.project = project; this.project = project;
this.featureName = featureName; this.featureName = featureName;
@ -509,9 +489,8 @@ export class FeatureArchivedEvent extends BaseEvent {
project: string; project: string;
featureName: string; featureName: string;
createdBy: string | IUser; createdBy: string | IUser;
tags: ITag[];
}) { }) {
super(FEATURE_ARCHIVED, p.createdBy, p.tags); super(FEATURE_ARCHIVED, p.createdBy);
const { project, featureName } = p; const { project, featureName } = p;
this.project = project; this.project = project;
this.featureName = featureName; this.featureName = featureName;
@ -530,9 +509,8 @@ export class FeatureRevivedEvent extends BaseEvent {
project: string; project: string;
featureName: string; featureName: string;
createdBy: string | IUser; createdBy: string | IUser;
tags: ITag[];
}) { }) {
super(FEATURE_REVIVED, p.createdBy, p.tags); super(FEATURE_REVIVED, p.createdBy);
const { project, featureName } = p; const { project, featureName } = p;
this.project = project; this.project = project;
this.featureName = featureName; this.featureName = featureName;
@ -554,9 +532,8 @@ export class FeatureDeletedEvent extends BaseEvent {
featureName: string; featureName: string;
preData: FeatureToggle; preData: FeatureToggle;
createdBy: string | IUser; createdBy: string | IUser;
tags: ITag[];
}) { }) {
super(FEATURE_DELETED, p.createdBy, p.tags); super(FEATURE_DELETED, p.createdBy);
const { project, featureName, preData } = p; const { project, featureName, preData } = p;
this.project = project; this.project = project;
this.featureName = featureName; this.featureName = featureName;
@ -582,9 +559,8 @@ export class FeatureMetadataUpdateEvent extends BaseEvent {
project: string; project: string;
data: FeatureToggle; data: FeatureToggle;
preData: FeatureToggle; preData: FeatureToggle;
tags: ITag[];
}) { }) {
super(FEATURE_METADATA_UPDATED, p.createdBy, p.tags); super(FEATURE_METADATA_UPDATED, p.createdBy);
const { project, featureName, data, preData } = p; const { project, featureName, data, preData } = p;
this.project = project; this.project = project;
this.featureName = featureName; this.featureName = featureName;
@ -611,9 +587,8 @@ export class FeatureStrategyAddEvent extends BaseEvent {
environment: string; environment: string;
createdBy: string | IUser; createdBy: string | IUser;
data: IStrategyConfig; data: IStrategyConfig;
tags: ITag[];
}) { }) {
super(FEATURE_STRATEGY_ADD, p.createdBy, p.tags); super(FEATURE_STRATEGY_ADD, p.createdBy);
const { project, featureName, environment, data } = p; const { project, featureName, environment, data } = p;
this.project = project; this.project = project;
this.featureName = featureName; this.featureName = featureName;
@ -643,9 +618,8 @@ export class FeatureStrategyUpdateEvent extends BaseEvent {
createdBy: string | IUser; createdBy: string | IUser;
data: IStrategyConfig; data: IStrategyConfig;
preData: IStrategyConfig; preData: IStrategyConfig;
tags: ITag[];
}) { }) {
super(FEATURE_STRATEGY_UPDATE, p.createdBy, p.tags); super(FEATURE_STRATEGY_UPDATE, p.createdBy);
const { project, featureName, environment, data, preData } = p; const { project, featureName, environment, data, preData } = p;
this.project = project; this.project = project;
this.featureName = featureName; this.featureName = featureName;
@ -673,9 +647,8 @@ export class FeatureStrategyRemoveEvent extends BaseEvent {
environment: string; environment: string;
createdBy: string | IUser; createdBy: string | IUser;
preData: IStrategyConfig; preData: IStrategyConfig;
tags: ITag[];
}) { }) {
super(FEATURE_STRATEGY_REMOVE, p.createdBy, p.tags); super(FEATURE_STRATEGY_REMOVE, p.createdBy);
const { project, featureName, environment, preData } = p; const { project, featureName, environment, preData } = p;
this.project = project; this.project = project;
this.featureName = featureName; this.featureName = featureName;
@ -1073,12 +1046,8 @@ export class PotentiallyStaleOnEvent extends BaseEvent {
readonly project: string; readonly project: string;
constructor(eventData: { constructor(eventData: { featureName: string; project: string }) {
featureName: string; super(FEATURE_POTENTIALLY_STALE_ON, 'unleash-system');
project: string;
tags: ITag[];
}) {
super(FEATURE_POTENTIALLY_STALE_ON, 'unleash-system', eventData.tags);
this.featureName = eventData.featureName; this.featureName = eventData.featureName;
this.project = eventData.project; this.project = eventData.project;
} }

View File

@ -54,7 +54,6 @@ test('Can filter by project', async () => {
type: FEATURE_CREATED, type: FEATURE_CREATED,
project: 'something-else', project: 'something-else',
data: { id: 'some-other-feature' }, data: { id: 'some-other-feature' },
tags: [],
createdBy: 'test-user', createdBy: 'test-user',
environment: 'test', environment: 'test',
}); });
@ -62,7 +61,6 @@ test('Can filter by project', async () => {
type: FEATURE_CREATED, type: FEATURE_CREATED,
project: 'default', project: 'default',
data: { id: 'feature' }, data: { id: 'feature' },
tags: [],
createdBy: 'test-user', createdBy: 'test-user',
environment: 'test', environment: 'test',
}); });
@ -81,7 +79,6 @@ test('can search for events', async () => {
type: FEATURE_CREATED, type: FEATURE_CREATED,
project: randomId(), project: randomId(),
data: { id: randomId() }, data: { id: randomId() },
tags: [],
createdBy: randomId(), createdBy: randomId(),
}, },
{ {
@ -89,7 +86,6 @@ test('can search for events', async () => {
project: randomId(), project: randomId(),
data: { id: randomId() }, data: { id: randomId() },
preData: { id: randomId() }, preData: { id: randomId() },
tags: [{ type: 'simple', value: randomId() }],
createdBy: randomId(), createdBy: randomId(),
}, },
]; ];
@ -130,12 +126,4 @@ test('can search for events', async () => {
expect(res.body.events).toHaveLength(1); expect(res.body.events).toHaveLength(1);
expect(res.body.events[0].preData.id).toEqual(events[1].preData.id); expect(res.body.events[0].preData.id).toEqual(events[1].preData.id);
}); });
await app.request
.post('/api/admin/events/search')
.send({ query: events[1].tags![0].value })
.expect(200)
.expect((res) => {
expect(res.body.events).toHaveLength(1);
expect(res.body.events[0].data.id).toEqual(events[1].data.id);
});
}); });

View File

@ -1323,7 +1323,6 @@ test('should calculate average time to production', async () => {
featureName: toggle.name, featureName: toggle.name,
environment: 'default', environment: 'default',
createdBy: 'Fredrik', createdBy: 'Fredrik',
tags: [],
}), }),
); );
}), }),
@ -1599,7 +1598,6 @@ test('should return average time to production per toggle', async () => {
featureName: toggle.name, featureName: toggle.name,
environment: 'default', environment: 'default',
createdBy: 'Fredrik', createdBy: 'Fredrik',
tags: [],
}), }),
); );
}), }),
@ -1678,7 +1676,6 @@ test('should return average time to production per toggle for a specific project
featureName: toggle.name, featureName: toggle.name,
environment: 'default', environment: 'default',
createdBy: 'Fredrik', createdBy: 'Fredrik',
tags: [],
}), }),
); );
}), }),
@ -1693,7 +1690,6 @@ test('should return average time to production per toggle for a specific project
featureName: toggle.name, featureName: toggle.name,
environment: 'default', environment: 'default',
createdBy: 'Fredrik', createdBy: 'Fredrik',
tags: [],
}), }),
); );
}), }),
@ -1757,7 +1753,6 @@ test('should return average time to production per toggle and include archived t
featureName: toggle.name, featureName: toggle.name,
environment: 'default', environment: 'default',
createdBy: 'Fredrik', createdBy: 'Fredrik',
tags: [],
}), }),
); );
}), }),

View File

@ -52,33 +52,6 @@ test('Should include id and createdAt when saving', async () => {
jest.useRealTimers(); jest.useRealTimers();
}); });
test('Should include empty tags array for new event', async () => {
expect.assertions(2);
const event = {
type: FEATURE_CREATED,
createdBy: 'me@mail.com',
data: {
name: 'someName',
enabled: true,
strategies: [{ name: 'default' }],
},
};
const promise = new Promise<void>((resolve) => {
eventStore.on(FEATURE_CREATED, (storedEvent: IEvent) => {
expect(storedEvent.data.name).toBe(event.data.name);
expect(Array.isArray(storedEvent.tags)).toBe(true);
resolve();
});
});
// Trigger
await eventStore.store(event);
await eventStore.publishUnannouncedEvents();
return promise;
});
test('Should be able to store multiple events at once', async () => { test('Should be able to store multiple events at once', async () => {
jest.useFakeTimers(); jest.useFakeTimers();
const event1 = { const event1 = {
@ -104,10 +77,9 @@ test('Should be able to store multiple events at once', async () => {
clientIp: '127.0.0.1', clientIp: '127.0.0.1',
appName: 'test3', appName: 'test3',
}, },
tags: [{ type: 'simple', value: 'mytest' }],
}; };
const seen = []; const seen: IEvent[] = [];
eventStore.on(APPLICATION_CREATED, (e) => seen.push(e)); eventStore.on(APPLICATION_CREATED, (e: IEvent) => seen.push(e));
await eventStore.batchStore([event1, event2, event3]); await eventStore.batchStore([event1, event2, event3]);
await eventStore.publishUnannouncedEvents(); await eventStore.publishUnannouncedEvents();
expect(seen.length).toBe(3); expect(seen.length).toBe(3);
@ -198,14 +170,12 @@ test('Should get all events of type', async () => {
featureName: data.name, featureName: data.name,
createdBy: 'test-user', createdBy: 'test-user',
data, data,
tags: [],
}) })
: new FeatureDeletedEvent({ : new FeatureDeletedEvent({
project: data.project, project: data.project,
preData: data, preData: data,
featureName: data.name, featureName: data.name,
createdBy: 'test-user', createdBy: 'test-user',
tags: [],
}); });
return eventStore.store(event); return eventStore.store(event);
}), }),