From b4b222f4c934a6e017c51be14ae544a1183138bc Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Thu, 7 Oct 2021 10:22:20 +0200 Subject: [PATCH] feat: add new more specific feature/environment events to addons (#994) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add new more specific feature/environment events to addons * Updated strategy change text * Update all three addon messages for strategy * Link to new features view for strategy change text Co-authored-by: Ivar Conradi Ă˜sthus --- .../addons/__snapshots__/datadog.test.ts.snap | 4 +- .../addons/__snapshots__/slack.test.ts.snap | 2 + .../addons/__snapshots__/teams.test.ts.snap | 2 + src/lib/addons/datadog-definition.ts | 14 ++++ src/lib/addons/datadog.test.ts | 36 +++++++++- src/lib/addons/datadog.ts | 64 +++++++++++++++++ src/lib/addons/slack-definition.ts | 14 ++++ src/lib/addons/slack.test.ts | 34 ++++++++- src/lib/addons/slack.ts | 64 +++++++++++++++++ src/lib/addons/teams-definition.ts | 14 ++++ src/lib/addons/teams.test.ts | 34 ++++++++- src/lib/addons/teams.ts | 70 ++++++++++++++++++- src/lib/addons/webhook-definition.ts | 15 ++++ src/lib/db/feature-toggle-client-store.ts | 11 ++- src/lib/services/feature-toggle-service-v2.ts | 27 ++++--- .../project/feature.strategy.e2e.test.ts | 58 +++++++++++++++ .../feature-toggle-service-v2.e2e.test.ts | 2 +- 17 files changed, 439 insertions(+), 26 deletions(-) diff --git a/src/lib/addons/__snapshots__/datadog.test.ts.snap b/src/lib/addons/__snapshots__/datadog.test.ts.snap index d03229c41e..264081f26a 100644 --- a/src/lib/addons/__snapshots__/datadog.test.ts.snap +++ b/src/lib/addons/__snapshots__/datadog.test.ts.snap @@ -1,5 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Should call datadog webhook for archived toggle 1`] = `"{\\"text\\":\\"%%% \\\\n The feature toggle *[some-toggle](http://some-url.com/archive/strategies/some-toggle)* was *archived* by some@user.com. \\\\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/features/strategies/some-toggle)\\\\n**Enabled**: no | **Type**: undefined | **Project**: undefined\\\\n**Activation strategies**: \`\`\`- name: default\\\\n\`\`\` \\\\n %%% \\",\\"title\\":\\"Unleash notification update\\"}"`; -exports[`Should call datadog webhook for archived toggle 1`] = `"{\\"text\\":\\"%%% \\\\n The feature toggle *[some-toggle](http://some-url.com/archive/strategies/some-toggle)* was *archived* by some@user.com. \\\\n %%% \\",\\"title\\":\\"Unleash notification update\\"}"`; +exports[`Should call datadog webhook for toggled environment 1`] = `"{\\"text\\":\\"%%% \\\\n The feature toggle ** in the default project was disabled in environment *development* \\\\n %%% \\",\\"title\\":\\"Unleash notification update\\"}"`; diff --git a/src/lib/addons/__snapshots__/slack.test.ts.snap b/src/lib/addons/__snapshots__/slack.test.ts.snap index 166753b183..a6e0702667 100644 --- a/src/lib/addons/__snapshots__/slack.test.ts.snap +++ b/src/lib/addons/__snapshots__/slack.test.ts.snap @@ -3,3 +3,5 @@ exports[`Should call slack webhook 1`] = `"{\\"username\\":\\"Unleash\\",\\"icon_emoji\\":\\":unleash:\\",\\"text\\":\\"some@user.com created feature toggle \\\\n*Enabled*: no | *Type*: undefined | *Project*: undefined\\\\n*Activation strategies*: \`\`\`- name: default\\\\n\`\`\`\\",\\"channel\\":\\"#undefined\\",\\"attachments\\":[{\\"actions\\":[{\\"name\\":\\"featureToggle\\",\\"text\\":\\"Open in Unleash\\",\\"type\\":\\"button\\",\\"value\\":\\"featureToggle\\",\\"style\\":\\"primary\\",\\"url\\":\\"http://some-url.com/features/strategies/some-toggle\\"}]}]}"`; exports[`Should call slack webhook for archived toggle 1`] = `"{\\"username\\":\\"Unleash\\",\\"icon_emoji\\":\\":unleash:\\",\\"text\\":\\"The feature toggle ** was *archived* by some@user.com.\\",\\"channel\\":\\"#undefined\\",\\"attachments\\":[{\\"actions\\":[{\\"name\\":\\"featureToggle\\",\\"text\\":\\"Open in Unleash\\",\\"type\\":\\"button\\",\\"value\\":\\"featureToggle\\",\\"style\\":\\"primary\\",\\"url\\":\\"http://some-url.com/archive/strategies/some-toggle\\"}]}]}"`; + +exports[`Should call webhook for toggled environment 1`] = `"{\\"username\\":\\"Unleash\\",\\"icon_emoji\\":\\":unleash:\\",\\"text\\":\\"The feature toggle ** in the default project was disabled in environment *development*\\",\\"channel\\":\\"#undefined\\",\\"attachments\\":[{\\"actions\\":[{\\"name\\":\\"featureToggle\\",\\"text\\":\\"Open in Unleash\\",\\"type\\":\\"button\\",\\"value\\":\\"featureToggle\\",\\"style\\":\\"primary\\",\\"url\\":\\"http://some-url.com/features/strategies/some-toggle\\"}]}]}"`; diff --git a/src/lib/addons/__snapshots__/teams.test.ts.snap b/src/lib/addons/__snapshots__/teams.test.ts.snap index cabbdd8082..029d7a1cbb 100644 --- a/src/lib/addons/__snapshots__/teams.test.ts.snap +++ b/src/lib/addons/__snapshots__/teams.test.ts.snap @@ -3,3 +3,5 @@ exports[`Should call teams webhook 1`] = `"{\\"themeColor\\":\\"0076D7\\",\\"summary\\":\\"Message\\",\\"sections\\":[{\\"activityTitle\\":\\"Feature toggle some-toggle | *Type*: undefined | *Project*: undefined
*Activation strategies*: \\\\n- name: default\\\\n\\",\\"activitySubtitle\\":\\"Unleash notification update\\",\\"facts\\":[{\\"name\\":\\"User\\",\\"value\\":\\"some@user.com\\"},{\\"name\\":\\"Action\\",\\"value\\":\\"Create\\"},{\\"name\\":\\"Enabled\\",\\"value\\":\\"*no*\\"}]}],\\"potentialAction\\":[{\\"@type\\":\\"OpenUri\\",\\"name\\":\\"Go to feature\\",\\"targets\\":[{\\"os\\":\\"default\\",\\"uri\\":\\"http://some-url.com/features/strategies/some-toggle\\"}]}]}"`; exports[`Should call teams webhook for archived toggle 1`] = `"{\\"themeColor\\":\\"0076D7\\",\\"summary\\":\\"Message\\",\\"sections\\":[{\\"activityTitle\\":\\"The feature toggle *some-toggle* was *archived*\\",\\"activitySubtitle\\":\\"Unleash notification update\\",\\"facts\\":[{\\"name\\":\\"User\\",\\"value\\":\\"some@user.com\\"},{\\"name\\":\\"Action\\",\\"value\\":\\"feature-archived\\"},{\\"name\\":\\"Enabled\\",\\"value\\":\\"*no*\\"}]}],\\"potentialAction\\":[{\\"@type\\":\\"OpenUri\\",\\"name\\":\\"Go to feature\\",\\"targets\\":[{\\"os\\":\\"default\\",\\"uri\\":\\"http://some-url.com/archive/strategies/some-toggle\\"}]}]}"`; + +exports[`Should call teams webhook for toggled environment 1`] = `"{\\"themeColor\\":\\"0076D7\\",\\"summary\\":\\"Message\\",\\"sections\\":[{\\"activityTitle\\":\\"The feature toggle ** in the default project was disabled in environment *development*\\",\\"activitySubtitle\\":\\"Unleash notification update\\",\\"facts\\":[{\\"name\\":\\"User\\",\\"value\\":\\"some@user.com\\"},{\\"name\\":\\"Action\\",\\"value\\":\\"feature-environment-disabled\\"},{\\"name\\":\\"Enabled\\",\\"value\\":\\"*no*\\"}]}],\\"potentialAction\\":[{\\"@type\\":\\"OpenUri\\",\\"name\\":\\"Go to feature\\",\\"targets\\":[{\\"os\\":\\"default\\",\\"uri\\":\\"http://some-url.com/features/strategies/some-toggle\\"}]}]}"`; diff --git a/src/lib/addons/datadog-definition.ts b/src/lib/addons/datadog-definition.ts index 1f27b38d6d..d2e0de5e0b 100644 --- a/src/lib/addons/datadog-definition.ts +++ b/src/lib/addons/datadog-definition.ts @@ -5,6 +5,13 @@ import { FEATURE_REVIVED, FEATURE_STALE_ON, FEATURE_STALE_OFF, + FEATURE_ENVIRONMENT_ENABLED, + FEATURE_ENVIRONMENT_DISABLED, + FEATURE_STRATEGY_REMOVE, + FEATURE_STRATEGY_UPDATE, + FEATURE_STRATEGY_ADD, + FEATURE_METADATA_UPDATED, + FEATURE_PROJECT_CHANGE, } from '../types/events'; import { IAddonDefinition } from '../types/model'; @@ -40,6 +47,13 @@ const dataDogDefinition: IAddonDefinition = { FEATURE_REVIVED, FEATURE_STALE_ON, FEATURE_STALE_OFF, + FEATURE_ENVIRONMENT_ENABLED, + FEATURE_ENVIRONMENT_DISABLED, + FEATURE_STRATEGY_REMOVE, + FEATURE_STRATEGY_UPDATE, + FEATURE_STRATEGY_ADD, + FEATURE_METADATA_UPDATED, + FEATURE_PROJECT_CHANGE, ], tagTypes: [ { diff --git a/src/lib/addons/datadog.test.ts b/src/lib/addons/datadog.test.ts index f77b4c05d5..3b221728d1 100644 --- a/src/lib/addons/datadog.test.ts +++ b/src/lib/addons/datadog.test.ts @@ -1,4 +1,8 @@ -import { FEATURE_CREATED, FEATURE_ARCHIVED } from '../types/events'; +import { + FEATURE_ARCHIVED, + FEATURE_CREATED, + FEATURE_ENVIRONMENT_DISABLED, +} from '../types/events'; import { Logger } from '../logger'; import DatadogAddon from './datadog'; @@ -58,7 +62,7 @@ test('Should call datadog webhook', async () => { expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); }); -test('Should call datadog webhook for archived toggle', async () => { +test('Should call datadog webhook for archived toggle', async () => { const addon = new DatadogAddon({ getLogger: noLogger, unleashUrl: 'http://some-url.com', @@ -82,3 +86,31 @@ test('Should call datadog webhook for archived toggle', async () => { expect(fetchRetryCalls[0].url).toBe(parameters.url); expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); }); + +test(`Should call datadog webhook for toggled environment`, async () => { + const addon = new DatadogAddon({ + getLogger: noLogger, + unleashUrl: 'http://some-url.com', + }); + const event: IEvent = { + id: 2, + createdAt: new Date(), + type: FEATURE_ENVIRONMENT_DISABLED, + createdBy: 'some@user.com', + environment: 'development', + project: 'default', + data: { + name: 'some-toggle', + }, + }; + + const parameters = { + url: 'http://hooks.slack.com', + }; + + await addon.handleEvent(event, parameters); + expect(fetchRetryCalls).toHaveLength(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatch(/disabled/); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); +}); diff --git a/src/lib/addons/datadog.ts b/src/lib/addons/datadog.ts index 96f688106e..8ff0f5d6f3 100644 --- a/src/lib/addons/datadog.ts +++ b/src/lib/addons/datadog.ts @@ -7,6 +7,13 @@ import { FEATURE_REVIVED, FEATURE_STALE_ON, FEATURE_STALE_OFF, + FEATURE_ENVIRONMENT_ENABLED, + FEATURE_STRATEGY_UPDATE, + FEATURE_STRATEGY_ADD, + FEATURE_ENVIRONMENT_DISABLED, + FEATURE_STRATEGY_REMOVE, + FEATURE_METADATA_UPDATED, + FEATURE_PROJECT_CHANGE, } from '../types/events'; import definition from './datadog-definition'; @@ -31,6 +38,25 @@ export default class DatadogAddon extends Addon { text = this.generateArchivedText(event); } else if ([FEATURE_STALE_ON, FEATURE_STALE_OFF].includes(event.type)) { text = this.generateStaleText(event); + } else if ( + [ + FEATURE_ENVIRONMENT_DISABLED, + FEATURE_ENVIRONMENT_ENABLED, + ].includes(event.type) + ) { + text = this.generateEnvironmentToggleText(event); + } else if ( + [ + FEATURE_STRATEGY_ADD, + FEATURE_STRATEGY_REMOVE, + FEATURE_STRATEGY_UPDATE, + ].includes(event.type) + ) { + text = this.generateStrategyChangeText(event); + } else if (FEATURE_METADATA_UPDATED === event.type) { + text = this.generateMetadataText(event); + } else if (FEATURE_PROJECT_CHANGE === event.type) { + text = this.generateProjectChangeText(event); } else { text = this.generateText(event); } @@ -58,6 +84,44 @@ export default class DatadogAddon extends Addon { ); } + generateEnvironmentToggleText(event: IEvent): string { + const { environment, project, data, type } = event; + const toggleStatus = + type === FEATURE_ENVIRONMENT_ENABLED ? 'enabled' : 'disabled'; + const feature = `<${this.featureLink(event)}|${data.name}>`; + return `The feature toggle *${feature}* in the ${project} project was ${toggleStatus} in environment *${environment}*`; + } + + generateStrategyChangeText(event: IEvent): string { + const { environment, project, data, type } = event; + const feature = `<${this.strategiesLink(event)}|${data.featureName}>`; + let action; + if (FEATURE_STRATEGY_UPDATE === type) { + action = 'updated in'; + } else if (FEATURE_STRATEGY_ADD) { + action = 'added to'; + } else { + action = 'removed from'; + } + const strategyText = `a ${data.name} strategy ${action} the *${environment}* environment`; + return `The feature toggle *${feature}* in project: ${project} had ${strategyText}`; + } + + generateMetadataText(event: IEvent): string { + const { createdBy, project, data } = event; + const feature = `<${this.featureLink(event)}|${data.name}>`; + return `${createdBy} updated the metadata for ${feature} in project ${project}`; + } + + generateProjectChangeText(event: IEvent): string { + const { createdBy, project, data } = event; + return `${createdBy} moved ${data.name} to ${project}`; + } + + strategiesLink(event: IEvent): string { + return `${this.unleashUrl}/projects/${event.project}/features2/${event.data.featureName}/strategies?environment=${event.environment}`; + } + featureLink(event: IEvent): string { const path = event.type === FEATURE_ARCHIVED ? 'archive' : 'features'; return `${this.unleashUrl}/${path}/strategies/${event.data.name}`; diff --git a/src/lib/addons/slack-definition.ts b/src/lib/addons/slack-definition.ts index 39a03d43ec..198a466d40 100644 --- a/src/lib/addons/slack-definition.ts +++ b/src/lib/addons/slack-definition.ts @@ -5,6 +5,13 @@ import { FEATURE_REVIVED, FEATURE_STALE_ON, FEATURE_STALE_OFF, + FEATURE_ENVIRONMENT_ENABLED, + FEATURE_ENVIRONMENT_DISABLED, + FEATURE_STRATEGY_REMOVE, + FEATURE_STRATEGY_UPDATE, + FEATURE_STRATEGY_ADD, + FEATURE_METADATA_UPDATED, + FEATURE_PROJECT_CHANGE, } from '../types/events'; import { IAddonDefinition } from '../types/model'; @@ -58,6 +65,13 @@ const slackDefinition: IAddonDefinition = { FEATURE_REVIVED, FEATURE_STALE_ON, FEATURE_STALE_OFF, + FEATURE_ENVIRONMENT_ENABLED, + FEATURE_ENVIRONMENT_DISABLED, + FEATURE_STRATEGY_REMOVE, + FEATURE_STRATEGY_UPDATE, + FEATURE_STRATEGY_ADD, + FEATURE_METADATA_UPDATED, + FEATURE_PROJECT_CHANGE, ], tagTypes: [ { diff --git a/src/lib/addons/slack.test.ts b/src/lib/addons/slack.test.ts index 2f57274d05..f5418971aa 100644 --- a/src/lib/addons/slack.test.ts +++ b/src/lib/addons/slack.test.ts @@ -1,4 +1,8 @@ -import { FEATURE_CREATED, FEATURE_ARCHIVED } from '../types/events'; +import { + FEATURE_CREATED, + FEATURE_ARCHIVED, + FEATURE_ENVIRONMENT_DISABLED, +} from '../types/events'; import { Logger } from '../logger'; import SlackAddon from './slack'; @@ -83,6 +87,34 @@ test('Should call slack webhook for archived toggle', async () => { expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); }); +test(`Should call webhook for toggled environment`, async () => { + const addon = new SlackAddon({ + getLogger: noLogger, + unleashUrl: 'http://some-url.com', + }); + const event: IEvent = { + id: 2, + createdAt: new Date(), + type: FEATURE_ENVIRONMENT_DISABLED, + createdBy: 'some@user.com', + environment: 'development', + project: 'default', + data: { + name: 'some-toggle', + }, + }; + + const parameters = { + url: 'http://hooks.slack.com', + }; + + await addon.handleEvent(event, parameters); + expect(fetchRetryCalls).toHaveLength(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatch(/disabled/); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); +}); + test('Should use default channel', async () => { const addon = new SlackAddon({ getLogger: noLogger, diff --git a/src/lib/addons/slack.ts b/src/lib/addons/slack.ts index 33007294ed..75744038aa 100644 --- a/src/lib/addons/slack.ts +++ b/src/lib/addons/slack.ts @@ -11,6 +11,13 @@ import { FEATURE_REVIVED, FEATURE_STALE_ON, FEATURE_STALE_OFF, + FEATURE_STRATEGY_UPDATE, + FEATURE_STRATEGY_REMOVE, + FEATURE_STRATEGY_ADD, + FEATURE_ENVIRONMENT_DISABLED, + FEATURE_ENVIRONMENT_ENABLED, + FEATURE_METADATA_UPDATED, + FEATURE_PROJECT_CHANGE, } from '../types/events'; export default class SlackAddon extends Addon { @@ -42,6 +49,25 @@ export default class SlackAddon extends Addon { text = this.generateArchivedText(event); } else if ([FEATURE_STALE_ON, FEATURE_STALE_OFF].includes(event.type)) { text = this.generateStaleText(event); + } else if ( + [ + FEATURE_ENVIRONMENT_DISABLED, + FEATURE_ENVIRONMENT_ENABLED, + ].includes(event.type) + ) { + text = this.generateEnvironmentToggleText(event); + } else if ( + [ + FEATURE_STRATEGY_ADD, + FEATURE_STRATEGY_REMOVE, + FEATURE_STRATEGY_UPDATE, + ].includes(event.type) + ) { + text = this.generateStrategyChangeText(event); + } else if (FEATURE_METADATA_UPDATED === event.type) { + text = this.generateMetadataText(event); + } else if (FEATURE_PROJECT_CHANGE === event.type) { + text = this.generateProjectChangeText(event); } else { text = this.generateText(event); } @@ -82,6 +108,44 @@ export default class SlackAddon extends Addon { this.logger.info(`Handled event ${event.type}. Status codes=${codes}`); } + generateEnvironmentToggleText(event: IEvent): string { + const { environment, project, data, type } = event; + const toggleStatus = + type === FEATURE_ENVIRONMENT_ENABLED ? 'enabled' : 'disabled'; + const feature = `<${this.featureLink(event)}|${data.name}>`; + return `The feature toggle *${feature}* in the ${project} project was ${toggleStatus} in environment *${environment}*`; + } + + generateStrategyChangeText(event: IEvent): string { + const { environment, project, data, type } = event; + const feature = `<${this.strategiesLink(event)}|${data.featureName}>`; + let action; + if (FEATURE_STRATEGY_UPDATE === type) { + action = 'updated in'; + } else if (FEATURE_STRATEGY_ADD) { + action = 'added to'; + } else { + action = 'removed from'; + } + const strategyText = `a ${data.name} strategy ${action} the *${environment}* environment`; + return `The feature toggle *${feature}* in project: ${project} had ${strategyText}`; + } + + generateMetadataText(event: IEvent): string { + const { createdBy, project, data } = event; + const feature = `<${this.featureLink(event)}|${data.name}>`; + return `${createdBy} updated the metadata for ${feature} in project ${project}`; + } + + generateProjectChangeText(event: IEvent): string { + const { createdBy, project, data } = event; + return `${createdBy} moved ${data.name} to ${project}`; + } + + strategiesLink(event: IEvent): string { + return `${this.unleashUrl}/projects/${event.project}/features2/${event.data.featureName}/strategies?environment=${event.environment}`; + } + featureLink(event: IEvent): string { const path = event.type === FEATURE_ARCHIVED ? 'archive' : 'features'; return `${this.unleashUrl}/${path}/strategies/${event.data.name}`; diff --git a/src/lib/addons/teams-definition.ts b/src/lib/addons/teams-definition.ts index b46239438b..388bd25b18 100644 --- a/src/lib/addons/teams-definition.ts +++ b/src/lib/addons/teams-definition.ts @@ -5,6 +5,13 @@ import { FEATURE_REVIVED, FEATURE_STALE_ON, FEATURE_STALE_OFF, + FEATURE_ENVIRONMENT_ENABLED, + FEATURE_ENVIRONMENT_DISABLED, + FEATURE_STRATEGY_REMOVE, + FEATURE_STRATEGY_UPDATE, + FEATURE_STRATEGY_ADD, + FEATURE_METADATA_UPDATED, + FEATURE_PROJECT_CHANGE, } from '../types/events'; import { IAddonDefinition } from '../types/model'; @@ -29,6 +36,13 @@ const teamsDefinition: IAddonDefinition = { FEATURE_REVIVED, FEATURE_STALE_ON, FEATURE_STALE_OFF, + FEATURE_ENVIRONMENT_ENABLED, + FEATURE_ENVIRONMENT_DISABLED, + FEATURE_STRATEGY_REMOVE, + FEATURE_STRATEGY_UPDATE, + FEATURE_STRATEGY_ADD, + FEATURE_METADATA_UPDATED, + FEATURE_PROJECT_CHANGE, ], }; diff --git a/src/lib/addons/teams.test.ts b/src/lib/addons/teams.test.ts index 8945454951..ec7355b0c7 100644 --- a/src/lib/addons/teams.test.ts +++ b/src/lib/addons/teams.test.ts @@ -1,6 +1,10 @@ import { Logger } from '../logger'; -import { FEATURE_CREATED, FEATURE_ARCHIVED } from '../types/events'; +import { + FEATURE_ARCHIVED, + FEATURE_CREATED, + FEATURE_ENVIRONMENT_DISABLED, +} from '../types/events'; import TeamsAddon from './teams'; @@ -83,3 +87,31 @@ test('Should call teams webhook for archived toggle', async () => { expect(fetchRetryCalls[0].url).toBe(parameters.url); expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); }); + +test(`Should call teams webhook for toggled environment`, async () => { + const addon = new TeamsAddon({ + getLogger: noLogger, + unleashUrl: 'http://some-url.com', + }); + const event: IEvent = { + id: 2, + createdAt: new Date(), + type: FEATURE_ENVIRONMENT_DISABLED, + createdBy: 'some@user.com', + environment: 'development', + project: 'default', + data: { + name: 'some-toggle', + }, + }; + + const parameters = { + url: 'http://hooks.slack.com', + }; + + await addon.handleEvent(event, parameters); + expect(fetchRetryCalls).toHaveLength(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatch(/disabled/); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); +}); diff --git a/src/lib/addons/teams.ts b/src/lib/addons/teams.ts index 1c9c6c146e..16a415a0ee 100644 --- a/src/lib/addons/teams.ts +++ b/src/lib/addons/teams.ts @@ -2,12 +2,19 @@ import YAML from 'js-yaml'; import Addon from './addon'; import { - FEATURE_CREATED, - FEATURE_UPDATED, FEATURE_ARCHIVED, + FEATURE_CREATED, + FEATURE_ENVIRONMENT_DISABLED, + FEATURE_ENVIRONMENT_ENABLED, + FEATURE_METADATA_UPDATED, + FEATURE_PROJECT_CHANGE, FEATURE_REVIVED, - FEATURE_STALE_ON, FEATURE_STALE_OFF, + FEATURE_STALE_ON, + FEATURE_STRATEGY_ADD, + FEATURE_STRATEGY_REMOVE, + FEATURE_STRATEGY_UPDATE, + FEATURE_UPDATED, } from '../types/events'; import { LogProvider } from '../logger'; @@ -31,6 +38,25 @@ export default class TeamsAddon extends Addon { text = this.generateArchivedText(event); } else if ([FEATURE_STALE_ON, FEATURE_STALE_OFF].includes(event.type)) { text = this.generateStaleText(event); + } else if ( + [ + FEATURE_ENVIRONMENT_DISABLED, + FEATURE_ENVIRONMENT_ENABLED, + ].includes(event.type) + ) { + text = this.generateEnvironmentToggleText(event); + } else if ( + [ + FEATURE_STRATEGY_ADD, + FEATURE_STRATEGY_REMOVE, + FEATURE_STRATEGY_UPDATE, + ].includes(event.type) + ) { + text = this.generateStrategyChangeText(event); + } else if (FEATURE_METADATA_UPDATED === event.type) { + text = this.generateMetadataText(event); + } else if (FEATURE_PROJECT_CHANGE === event.type) { + text = this.generateProjectChangeText(event); } else { text = this.generateText(event); } @@ -85,6 +111,44 @@ export default class TeamsAddon extends Addon { ); } + generateEnvironmentToggleText(event: IEvent): string { + const { environment, project, data, type } = event; + const toggleStatus = + type === FEATURE_ENVIRONMENT_ENABLED ? 'enabled' : 'disabled'; + const feature = `<${this.featureLink(event)}|${data.name}>`; + return `The feature toggle *${feature}* in the ${project} project was ${toggleStatus} in environment *${environment}*`; + } + + generateStrategyChangeText(event: IEvent): string { + const { environment, project, data, type } = event; + const feature = `<${this.strategiesLink(event)}|${data.featureName}>`; + let action; + if (FEATURE_STRATEGY_UPDATE === type) { + action = 'updated in'; + } else if (FEATURE_STRATEGY_ADD) { + action = 'added to'; + } else { + action = 'removed from'; + } + const strategyText = `a ${data.name} strategy ${action} the *${environment}* environment`; + return `The feature toggle *${feature}* in project: ${project} had ${strategyText}`; + } + + generateMetadataText(event: IEvent): string { + const { createdBy, project, data } = event; + const feature = `<${this.featureLink(event)}|${data.name}>`; + return `${createdBy} updated the metadata for ${feature} in project ${project}`; + } + + generateProjectChangeText(event: IEvent): string { + const { createdBy, project, data } = event; + return `${createdBy} moved ${data.name} to ${project}`; + } + + strategiesLink(event: IEvent): string { + return `${this.unleashUrl}/projects/${event.project}/features2/${event.data.featureName}/strategies?environment=${event.environment}`; + } + featureLink(event: IEvent): string { const path = event.type === FEATURE_ARCHIVED ? 'archive' : 'features'; return `${this.unleashUrl}/${path}/strategies/${event.data.name}`; diff --git a/src/lib/addons/webhook-definition.ts b/src/lib/addons/webhook-definition.ts index 563d147257..82d11bee9a 100644 --- a/src/lib/addons/webhook-definition.ts +++ b/src/lib/addons/webhook-definition.ts @@ -1,9 +1,16 @@ import { FEATURE_ARCHIVED, FEATURE_CREATED, + FEATURE_ENVIRONMENT_DISABLED, + FEATURE_ENVIRONMENT_ENABLED, + FEATURE_METADATA_UPDATED, + FEATURE_PROJECT_CHANGE, FEATURE_REVIVED, FEATURE_STALE_OFF, FEATURE_STALE_ON, + FEATURE_STRATEGY_ADD, + FEATURE_STRATEGY_REMOVE, + FEATURE_STRATEGY_UPDATE, FEATURE_UPDATED, } from '../types/events'; import { IAddonDefinition } from '../types/model'; @@ -54,9 +61,17 @@ const webhookDefinition: IAddonDefinition = { FEATURE_CREATED, FEATURE_UPDATED, FEATURE_ARCHIVED, + FEATURE_METADATA_UPDATED, FEATURE_REVIVED, FEATURE_STALE_ON, FEATURE_STALE_OFF, + FEATURE_ENVIRONMENT_ENABLED, + FEATURE_ENVIRONMENT_DISABLED, + FEATURE_STRATEGY_REMOVE, + FEATURE_STRATEGY_UPDATE, + FEATURE_STRATEGY_ADD, + FEATURE_METADATA_UPDATED, + FEATURE_PROJECT_CHANGE, ], }; diff --git a/src/lib/db/feature-toggle-client-store.ts b/src/lib/db/feature-toggle-client-store.ts index b853f45250..4435c574a3 100644 --- a/src/lib/db/feature-toggle-client-store.ts +++ b/src/lib/db/feature-toggle-client-store.ts @@ -52,13 +52,12 @@ export default class FeatureToggleClientStore parameters: r.parameters, id: r.strategy_id, }; - } else { - return { - name: r.strategy_name, - constraints: r.constraints || [], - parameters: r.parameters, - }; } + return { + name: r.strategy_name, + constraints: r.constraints || [], + parameters: r.parameters, + }; } private async getAll( diff --git a/src/lib/services/feature-toggle-service-v2.ts b/src/lib/services/feature-toggle-service-v2.ts index 8eba5f2b00..5fed05ce1a 100644 --- a/src/lib/services/feature-toggle-service-v2.ts +++ b/src/lib/services/feature-toggle-service-v2.ts @@ -122,7 +122,7 @@ class FeatureToggleServiceV2 { project: projectId, createdBy: userName, environment, - data, + data: { ...data, featureName: newFeatureStrategy.featureName }, }); return data; } catch (e) { @@ -134,13 +134,6 @@ class FeatureToggleServiceV2 { throw e; } } - /* - TODO after 4.1.0 release: - - add FEATURE_STRATEGY_ADD event - - add FEATURE_STRATEGY_REMOVE event - - add FEATURE_STRATEGY_UPDATE event - */ - /** * PUT /api/admin/projects/:projectId/features/:featureName/strategies/:strategyId ? * { @@ -167,6 +160,7 @@ class FeatureToggleServiceV2 { const data = { id: strategy.id, name: strategy.strategyName, + featureName: strategy.featureName, constraints: strategy.constraints || [], parameters: strategy.parameters, }; @@ -217,12 +211,15 @@ class FeatureToggleServiceV2 { } /** - * DELETE /api/admin/projects/:projectId/features/:featureName/strategies/:strategyId ? + * DELETE /api/admin/projects/:projectId/features/:featureName/environments/:environmentName/strategies/:strategyId * { * * } - * @param id - * @param updates + * @param id - strategy id + * @param featureName - Name of the feature the strategy belongs to + * @param userName - Who's doing the change + * @param project - Which project does this feature toggle belong to + * @param environment - Which environment does this strategy belong to */ async deleteStrategy( id: string, @@ -239,6 +236,7 @@ class FeatureToggleServiceV2 { createdBy: userName, data: { id, + featureName, }, }); // If there are no strategies left for environment disable it @@ -401,6 +399,13 @@ class FeatureToggleServiceV2 { project: projectId, tags, }); + await this.eventStore.store({ + type: FEATURE_UPDATED, + createdBy: userName, + data: featureToggle, + project: projectId, + tags, + }); return featureToggle; } diff --git a/src/test/e2e/api/admin/project/feature.strategy.e2e.test.ts b/src/test/e2e/api/admin/project/feature.strategy.e2e.test.ts index 32d4777d6d..24e19f7083 100644 --- a/src/test/e2e/api/admin/project/feature.strategy.e2e.test.ts +++ b/src/test/e2e/api/admin/project/feature.strategy.e2e.test.ts @@ -5,6 +5,8 @@ import { DEFAULT_ENV } from '../../../../../lib/util/constants'; import { FEATURE_ENVIRONMENT_DISABLED, FEATURE_ENVIRONMENT_ENABLED, + FEATURE_METADATA_UPDATED, + FEATURE_STRATEGY_REMOVE, } from '../../../../../lib/types/events'; let app: IUnleashTest; @@ -520,6 +522,13 @@ test('Should patch feature toggle', async () => { expect(toggle.description).toBe('New desc'); expect(toggle.type).toBe('kill-switch'); expect(toggle.archived).toBeFalsy(); + const events = await db.stores.eventStore.getAll({ + type: FEATURE_METADATA_UPDATED, + }); + const updateForOurToggle = events.find((e) => e.data.name === name); + expect(updateForOurToggle).toBeTruthy(); + expect(updateForOurToggle.data.description).toBe('New desc'); + expect(updateForOurToggle.data.type).toBe('kill-switch'); }); test('Should archive feature toggle', async () => { @@ -902,6 +911,55 @@ test('Can not enable environment for feature without strategies', async () => { expect(enabledFeatureEnv.type).toBe('test'); }); }); + +test('Deleting a strategy should include name of feature strategy was deleted from', async () => { + const environment = 'delete_strategy_env'; + const featureName = 'delete_strategy_feature'; + // Create environment + await db.stores.environmentStore.create({ + name: environment, + type: 'test', + }); + // Connect environment to project + await app.request + .post('/api/admin/projects/default/environments') + .send({ environment }) + .expect(200); + + // Create feature + await app.request + .post('/api/admin/projects/default/features') + .send({ + name: featureName, + }) + .set('Content-Type', 'application/json') + .expect(201); + let strategyId; + await app.request + .post( + `/api/admin/projects/default/features/${featureName}/environments/${environment}/strategies`, + ) + .send({ name: 'default', constraints: [], properties: {} }) + .expect(200) + .expect((res) => { + strategyId = res.body.id; + }); + expect(strategyId).toBeTruthy(); + // Delete strategy + await app.request + .delete( + `/api/admin/projects/default/features/${featureName}/environments/${environment}/strategies/${strategyId}`, + ) + .expect(200); + const events = await db.stores.eventStore.getAll({ + type: FEATURE_STRATEGY_REMOVE, + }); + expect(events).toHaveLength(1); + expect(events[0].data.featureName).toBe(featureName); + expect(events[0].environment).toBe(environment); + expect(events[0].data.id).toBe(strategyId); +}); + test('Enabling environment creates a FEATURE_ENVIRONMENT_ENABLED event', async () => { const environment = 'environment_enabled_env'; const featureName = 'com.test.enable.environment.event.sent'; diff --git a/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts b/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts index e61aee4c9c..bfaa0d8a53 100644 --- a/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts +++ b/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts @@ -131,7 +131,6 @@ test('Should be able to get strategy by id', async () => { constraints: [], parameters: {}, }; - await service.createFeatureToggle( 'default', { @@ -145,6 +144,7 @@ test('Should be able to get strategy by id', async () => { 'default', 'Demo', userName, + DEFAULT_ENV, ); const fetchedConfig = await service.getStrategy(createdConfig.id); expect(fetchedConfig).toEqual(createdConfig);