diff --git a/src/lib/addons/__snapshots__/datadog.test.ts.snap b/src/lib/addons/__snapshots__/datadog.test.ts.snap index 264081f26a..d8850892cc 100644 --- a/src/lib/addons/__snapshots__/datadog.test.ts.snap +++ b/src/lib/addons/__snapshots__/datadog.test.ts.snap @@ -1,7 +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 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 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 1`] = `"{\\"text\\":\\"%%% \\\\n some@user.com created feature toggle [some-toggle](http://some-url.com/projects//some-toggle) in project *undefined* \\\\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\\"}"`; +exports[`Should call datadog webhook for toggled environment 1`] = `"{\\"text\\":\\"%%% \\\\n some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/some-toggle) in *development* environment in project *default* \\\\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 a6e0702667..0511249616 100644 --- a/src/lib/addons/__snapshots__/slack.test.ts.snap +++ b/src/lib/addons/__snapshots__/slack.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -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 1`] = `"{\\"username\\":\\"Unleash\\",\\"icon_emoji\\":\\":unleash:\\",\\"text\\":\\"some@user.com created feature toggle in project *default*\\",\\"channel\\":\\"#undefined\\",\\"attachments\\":[{\\"actions\\":[{\\"name\\":\\"featureToggle\\",\\"text\\":\\"Open in Unleash\\",\\"type\\":\\"button\\",\\"value\\":\\"featureToggle\\",\\"style\\":\\"primary\\",\\"url\\":\\"http://some-url.com/projects/default/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 slack webhook for archived toggle 1`] = `"{\\"username\\":\\"Unleash\\",\\"icon_emoji\\":\\":unleash:\\",\\"text\\":\\" some@user.com just archived feature toggle **\\",\\"channel\\":\\"#undefined\\",\\"attachments\\":[{\\"actions\\":[{\\"name\\":\\"featureToggle\\",\\"text\\":\\"Open in Unleash\\",\\"type\\":\\"button\\",\\"value\\":\\"featureToggle\\",\\"style\\":\\"primary\\",\\"url\\":\\"http://some-url.com/archive\\"}]}]}"`; -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\\"}]}]}"`; +exports[`Should call webhook for toggled environment 1`] = `"{\\"username\\":\\"Unleash\\",\\"icon_emoji\\":\\":unleash:\\",\\"text\\":\\"some@user.com *disabled* in *development* environment in project *default*\\",\\"channel\\":\\"#undefined\\",\\"attachments\\":[{\\"actions\\":[{\\"name\\":\\"featureToggle\\",\\"text\\":\\"Open in Unleash\\",\\"type\\":\\"button\\",\\"value\\":\\"featureToggle\\",\\"style\\":\\"primary\\",\\"url\\":\\"http://some-url.com/projects/default/some-toggle\\"}]}]}"`; diff --git a/src/lib/addons/__snapshots__/teams.test.ts.snap b/src/lib/addons/__snapshots__/teams.test.ts.snap index 029d7a1cbb..d6327b1a4f 100644 --- a/src/lib/addons/__snapshots__/teams.test.ts.snap +++ b/src/lib/addons/__snapshots__/teams.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -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 1`] = `"{\\"themeColor\\":\\"0076D7\\",\\"summary\\":\\"Message\\",\\"sections\\":[{\\"activityTitle\\":\\"some@user.com created feature toggle [some-toggle](http://some-url.com/projects//some-toggle) in project *undefined*\\",\\"activitySubtitle\\":\\"Unleash notification update\\",\\"facts\\":[{\\"name\\":\\"User\\",\\"value\\":\\"some@user.com\\"},{\\"name\\":\\"Action\\",\\"value\\":\\"feature-created\\"}]}],\\"potentialAction\\":[{\\"@type\\":\\"OpenUri\\",\\"name\\":\\"Go to feature\\",\\"targets\\":[{\\"os\\":\\"default\\",\\"uri\\":\\"http://some-url.com/projects//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 archived toggle 1`] = `"{\\"themeColor\\":\\"0076D7\\",\\"summary\\":\\"Message\\",\\"sections\\":[{\\"activityTitle\\":\\" some@user.com just archived feature toggle *[some-toggle](http://some-url.com/archive)*\\",\\"activitySubtitle\\":\\"Unleash notification update\\",\\"facts\\":[{\\"name\\":\\"User\\",\\"value\\":\\"some@user.com\\"},{\\"name\\":\\"Action\\",\\"value\\":\\"feature-archived\\"}]}],\\"potentialAction\\":[{\\"@type\\":\\"OpenUri\\",\\"name\\":\\"Go to feature\\",\\"targets\\":[{\\"os\\":\\"default\\",\\"uri\\":\\"http://some-url.com/archive\\"}]}]}"`; -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\\"}]}]}"`; +exports[`Should call teams webhook for toggled environment 1`] = `"{\\"themeColor\\":\\"0076D7\\",\\"summary\\":\\"Message\\",\\"sections\\":[{\\"activityTitle\\":\\"some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/some-toggle) in *development* environment in project *default*\\",\\"activitySubtitle\\":\\"Unleash notification update\\",\\"facts\\":[{\\"name\\":\\"User\\",\\"value\\":\\"some@user.com\\"},{\\"name\\":\\"Action\\",\\"value\\":\\"feature-environment-disabled\\"}]}],\\"potentialAction\\":[{\\"@type\\":\\"OpenUri\\",\\"name\\":\\"Go to feature\\",\\"targets\\":[{\\"os\\":\\"default\\",\\"uri\\":\\"http://some-url.com/projects/default/some-toggle\\"}]}]}"`; diff --git a/src/lib/addons/datadog.ts b/src/lib/addons/datadog.ts index 8ff0f5d6f3..dcedd36edd 100644 --- a/src/lib/addons/datadog.ts +++ b/src/lib/addons/datadog.ts @@ -1,65 +1,30 @@ -import YAML from 'js-yaml'; import Addon from './addon'; -import { - FEATURE_CREATED, - FEATURE_UPDATED, - FEATURE_ARCHIVED, - 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'; -import { LogProvider } from '../logger'; -import { IEvent } from '../types/model'; +import { IAddonConfig, IEvent } from '../types/model'; +import { + FeatureEventFormatter, + FeatureEventFormatterMd, + LinkStyle, +} from './feature-event-formatter-md'; export default class DatadogAddon extends Addon { - unleashUrl: string; + private msgFormatter: FeatureEventFormatter; - constructor(config: { unleashUrl: string; getLogger: LogProvider }) { + constructor(config: IAddonConfig) { super(definition, config); - this.unleashUrl = config.unleashUrl; + this.msgFormatter = new FeatureEventFormatterMd( + config.unleashUrl, + LinkStyle.MD, + ); } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async handleEvent(event: IEvent, parameters: any): Promise { const { url = 'https://api.datadoghq.com/api/v1/events', apiKey } = parameters; - let text; - if ([FEATURE_ARCHIVED, FEATURE_REVIVED].includes(event.type)) { - 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); - } + const text = this.msgFormatter.format(event); const { tags: eventTags } = event; const tags = @@ -83,94 +48,4 @@ export default class DatadogAddon extends Addon { `Handled event ${event.type}. Status codes=${res.status}`, ); } - - 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}`; - } - - generateStaleText(event: IEvent): string { - const { createdBy, data, type } = event; - const isStale = type === FEATURE_STALE_ON; - const feature = `[${data.name}](${this.featureLink(event)})`; - - if (isStale) { - return `The feature toggle *${feature}* is now *ready to be removed* from the code. -This was changed by ${createdBy}.`; - } - return `The feature toggle *${feature}* was *unmarked as stale* by ${createdBy}.`; - } - - generateArchivedText(event: IEvent): string { - const { createdBy, data, type } = event; - const action = type === FEATURE_ARCHIVED ? 'archived' : 'revived'; - const feature = `[${data.name}](${this.featureLink(event)})`; - return `The feature toggle *${feature}* was *${action}* by ${createdBy}.`; - } - - generateText(event: IEvent): string { - const { createdBy, data, type } = event; - const action = this.getAction(type); - const feature = `[${data.name}](${this.featureLink(event)})`; - const enabled = `**Enabled**: ${data.enabled ? 'yes' : 'no'}`; - const stale = data.stale ? '("stale")' : ''; - const typeStr = `**Type**: ${data.type}`; - const project = `**Project**: ${data.project}`; - const strategies = `**Activation strategies**: \`\`\`${YAML.dump( - data.strategies, - { skipInvalid: true }, - )}\`\`\``; - return `${createdBy} ${action} feature toggle ${feature} -${enabled}${stale} | ${typeStr} | ${project} -${data.strategies ? strategies : ''}`; - } - - getAction(type: string): string { - switch (type) { - case FEATURE_CREATED: - return 'created'; - case FEATURE_UPDATED: - return 'updated'; - default: - return type; - } - } } diff --git a/src/lib/addons/feature-event-formatter-md.ts b/src/lib/addons/feature-event-formatter-md.ts new file mode 100644 index 0000000000..83291827f2 --- /dev/null +++ b/src/lib/addons/feature-event-formatter-md.ts @@ -0,0 +1,149 @@ +import { IEvent } from '../types/model'; +import { + FEATURE_CREATED, + FEATURE_UPDATED, + FEATURE_ARCHIVED, + FEATURE_STALE_ON, + FEATURE_STRATEGY_UPDATE, + FEATURE_STRATEGY_ADD, + FEATURE_ENVIRONMENT_ENABLED, + FEATURE_REVIVED, + FEATURE_STALE_OFF, + FEATURE_ENVIRONMENT_DISABLED, + FEATURE_STRATEGY_REMOVE, + FEATURE_METADATA_UPDATED, + FEATURE_PROJECT_CHANGE, +} from '../types/events'; + +export interface FeatureEventFormatter { + format: (event: IEvent) => string; + featureLink: (event: IEvent) => string; +} + +export enum LinkStyle { + SLACK, + MD, +} + +export class FeatureEventFormatterMd implements FeatureEventFormatter { + private unleashUrl: string; + + private linkStyle: LinkStyle; + + constructor(unleashUrl: string, linkStyle: LinkStyle = LinkStyle.MD) { + this.unleashUrl = unleashUrl; + this.linkStyle = linkStyle; + } + + generateArchivedText(event: IEvent): string { + const { createdBy, type } = event; + const action = type === FEATURE_ARCHIVED ? 'archived' : 'revived'; + const feature = this.generateFeatureLink(event); + return ` ${createdBy} just ${action} feature toggle *${feature}*`; + } + + generateFeatureLink(event: IEvent): string { + if (this.linkStyle === LinkStyle.SLACK) { + return `<${this.featureLink(event)}|${event.data.name}>`; + } else { + return `[${event.data.name}](${this.featureLink(event)})`; + } + } + + generateStaleText(event: IEvent): string { + const { createdBy, type } = event; + const isStale = type === FEATURE_STALE_ON; + const feature = this.generateFeatureLink(event); + + if (isStale) { + return `${createdBy} marked ${feature} as stale and this feature toggle is now *ready to be removed* from the code.`; + } + return `${createdBy} removed the stale marking on *${feature}*.`; + } + + generateEnvironmentToggleText(event: IEvent): string { + const { createdBy, environment, type, project } = event; + const toggleStatus = + type === FEATURE_ENVIRONMENT_ENABLED ? 'enabled' : 'disabled'; + const feature = this.generateFeatureLink(event); + return `${createdBy} *${toggleStatus}* ${feature} in *${environment}* environment in project *${project}*`; + } + + generateStrategyChangeText(event: IEvent): string { + const { createdBy, environment, project, data, type } = event; + const feature = this.generateFeatureLink(event); + let action; + if (FEATURE_STRATEGY_UPDATE === type) { + action = 'updated in'; + } else if (FEATURE_STRATEGY_ADD === type) { + action = 'added to'; + } else { + action = 'removed from'; + } + const strategyText = `a ${ + data.strategyName ?? '' + } strategy ${action} the *${environment}* environment`; + return `${createdBy} updated *${feature}* with ${strategyText} in project *${project}*`; + } + + generateMetadataText(event: IEvent): string { + const { createdBy, project } = event; + const feature = this.generateFeatureLink(event); + 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}`; + } + + featureLink(event: IEvent): string { + const { type, project = '', data } = event; + if (type === FEATURE_ARCHIVED) { + return `${this.unleashUrl}/archive`; + } + return `${this.unleashUrl}/projects/${project}/${data.name}`; + } + + getAction(type: string): string { + switch (type) { + case FEATURE_CREATED: + return 'created'; + case FEATURE_UPDATED: + return 'updated'; + default: + return type; + } + } + + defaultText(event: IEvent): string { + const { createdBy, project, type } = event; + const action = this.getAction(type); + const feature = this.generateFeatureLink(event); + return `${createdBy} ${action} feature toggle ${feature} in project *${project}*`; + } + + format(event: IEvent): string { + switch (event.type) { + case FEATURE_ARCHIVED: + case FEATURE_REVIVED: + return this.generateArchivedText(event); + case FEATURE_STALE_ON: + case FEATURE_STALE_OFF: + return this.generateStaleText(event); + case FEATURE_ENVIRONMENT_DISABLED: + case FEATURE_ENVIRONMENT_ENABLED: + return this.generateEnvironmentToggleText(event); + case FEATURE_STRATEGY_ADD: + case FEATURE_STRATEGY_REMOVE: + case FEATURE_STRATEGY_UPDATE: + return this.generateStrategyChangeText(event); + case FEATURE_METADATA_UPDATED: + return this.generateMetadataText(event); + case FEATURE_PROJECT_CHANGE: + return this.generateProjectChangeText(event); + default: + return this.defaultText(event); + } + } +} diff --git a/src/lib/addons/index.ts b/src/lib/addons/index.ts index cc75756c9d..81a1423d10 100644 --- a/src/lib/addons/index.ts +++ b/src/lib/addons/index.ts @@ -12,6 +12,7 @@ export interface IAddonProviders { export const getAddons: (args: { getLogger: LogProvider; unleashUrl: string; + newFeatureLink?: boolean; }) => IAddonProviders = ({ getLogger, unleashUrl }) => { const addons = [ new Webhook({ getLogger }), diff --git a/src/lib/addons/slack.test.ts b/src/lib/addons/slack.test.ts index f5418971aa..7cf75201dc 100644 --- a/src/lib/addons/slack.test.ts +++ b/src/lib/addons/slack.test.ts @@ -45,9 +45,11 @@ test('Should call slack webhook', async () => { createdAt: new Date(), type: FEATURE_CREATED, createdBy: 'some@user.com', + project: 'default', data: { name: 'some-toggle', enabled: false, + type: 'release', strategies: [{ name: 'default' }], }, }; diff --git a/src/lib/addons/slack.ts b/src/lib/addons/slack.ts index 75744038aa..590e560b51 100644 --- a/src/lib/addons/slack.ts +++ b/src/lib/addons/slack.ts @@ -1,31 +1,23 @@ -import YAML from 'js-yaml'; import Addon from './addon'; import slackDefinition from './slack-definition'; import { IAddonConfig, IEvent } from '../types/model'; import { - FEATURE_CREATED, - FEATURE_UPDATED, - FEATURE_ARCHIVED, - 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'; + FeatureEventFormatter, + FeatureEventFormatterMd, + LinkStyle, +} from './feature-event-formatter-md'; export default class SlackAddon extends Addon { - unleashUrl: string; + private msgFormatter: FeatureEventFormatter; constructor(args: IAddonConfig) { super(slackDefinition, args); - this.unleashUrl = args.unleashUrl; + this.msgFormatter = new FeatureEventFormatterMd( + args.unleashUrl, + LinkStyle.SLACK, + ); } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -43,34 +35,8 @@ export default class SlackAddon extends Addon { slackChannels.push(defaultChannel); } - let text; - - if ([FEATURE_ARCHIVED, FEATURE_REVIVED].includes(event.type)) { - 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); - } + const text = this.msgFormatter.format(event); + const featureLink = this.msgFormatter.featureLink(event); const requests = slackChannels.map((channel) => { const body = { @@ -87,7 +53,7 @@ export default class SlackAddon extends Addon { type: 'button', value: 'featureToggle', style: 'primary', - url: this.featureLink(event), + url: featureLink, }, ], }, @@ -108,49 +74,6 @@ 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}`; - } - findSlackChannels({ tags }: Pick): string[] { if (tags) { return tags @@ -159,53 +82,6 @@ export default class SlackAddon extends Addon { } return []; } - - generateStaleText(event: IEvent): string { - const { createdBy, data, type } = event; - const isStale = type === FEATURE_STALE_ON; - const feature = `<${this.featureLink(event)}|${data.name}>`; - - if (isStale) { - return `The feature toggle *${feature}* is now *ready to be removed* from the code. :technologist: -This was changed by ${createdBy}.`; - } - return `The feature toggle *${feature}* was *unmarked as stale* by ${createdBy}.`; - } - - generateArchivedText(event: IEvent): string { - const { createdBy, data, type } = event; - const action = type === FEATURE_ARCHIVED ? 'archived' : 'revived'; - const feature = `<${this.featureLink(event)}|${data.name}>`; - return `The feature toggle *${feature}* was *${action}* by ${createdBy}.`; - } - - generateText(event: IEvent): string { - const { createdBy, data, type } = event; - const action = this.getAction(type); - const feature = `<${this.featureLink(event)}|${data.name}>`; - const enabled = `*Enabled*: ${data.enabled ? 'yes' : 'no'}`; - const stale = data.stale ? '("stale")' : ''; - const typeStr = `*Type*: ${data.type}`; - const project = `*Project*: ${data.project}`; - const strategies = `*Activation strategies*: \`\`\`${YAML.dump( - data.strategies, - { skipInvalid: true }, - )}\`\`\``; - return `${createdBy} ${action} feature toggle ${feature} -${enabled}${stale} | ${typeStr} | ${project} -${data.strategies ? strategies : ''}`; - } - - getAction(type: string): string { - switch (type) { - case FEATURE_CREATED: - return 'created'; - case FEATURE_UPDATED: - return 'updated'; - default: - return type; - } - } } module.exports = SlackAddon; diff --git a/src/lib/addons/teams.ts b/src/lib/addons/teams.ts index 16a415a0ee..dbb4f8841b 100644 --- a/src/lib/addons/teams.ts +++ b/src/lib/addons/teams.ts @@ -1,68 +1,27 @@ -import YAML from 'js-yaml'; import Addon from './addon'; -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 { LogProvider } from '../logger'; - import teamsDefinition from './teams-definition'; -import { IEvent } from '../types/model'; +import { IAddonConfig, IEvent } from '../types/model'; +import { + FeatureEventFormatter, + FeatureEventFormatterMd, +} from './feature-event-formatter-md'; export default class TeamsAddon extends Addon { - unleashUrl: string; + private msgFormatter: FeatureEventFormatter; - constructor(args: { unleashUrl: string; getLogger: LogProvider }) { + constructor(args: IAddonConfig) { super(teamsDefinition, args); - this.unleashUrl = args.unleashUrl; + this.msgFormatter = new FeatureEventFormatterMd(args.unleashUrl); } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async handleEvent(event: IEvent, parameters: any): Promise { const { url } = parameters; - const { createdBy, data, type } = event; - let text = ''; - if ([FEATURE_ARCHIVED, FEATURE_REVIVED].includes(event.type)) { - 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); - } + const { createdBy } = event; + const text = this.msgFormatter.format(event); + const featureLink = this.msgFormatter.featureLink(event); - const enabled = `*${data.enabled ? 'yes' : 'no'}*`; - const stale = data.stale ? '("stale")' : ''; const body = { themeColor: '0076D7', summary: 'Message', @@ -77,11 +36,7 @@ export default class TeamsAddon extends Addon { }, { name: 'Action', - value: this.getAction(type), - }, - { - name: 'Enabled', - value: `${enabled}${stale}`, + value: event.type, }, ], }, @@ -93,7 +48,7 @@ export default class TeamsAddon extends Addon { targets: [ { os: 'default', - uri: this.featureLink(event), + uri: featureLink, }, ], }, @@ -110,86 +65,4 @@ export default class TeamsAddon extends Addon { `Handled event ${event.type}. Status codes=${res.status}`, ); } - - 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}`; - } - - generateStaleText(event: IEvent): string { - const { data, type } = event; - const isStale = type === FEATURE_STALE_ON; - if (isStale) { - return `The feature toggle *${data.name}* is now *ready to be removed* from the code.`; - } - return `The feature toggle *${data.name}* was *unmarked* as stale`; - } - - generateArchivedText(event: IEvent): string { - const { data, type } = event; - const action = type === FEATURE_ARCHIVED ? 'archived' : 'revived'; - return `The feature toggle *${data.name}* was *${action}*`; - } - - generateText(event: IEvent): string { - const { data } = event; - const typeStr = `*Type*: ${data.type}`; - const project = `*Project*: ${data.project}`; - const strategies = `*Activation strategies*: \n${YAML.dump( - data.strategies, - { skipInvalid: true }, - )}`; - return `Feature toggle ${data.name} | ${typeStr} | ${project}
${ - data.strategies ? strategies : '' - }`; - } - - getAction(type: string): string { - switch (type) { - case FEATURE_CREATED: - return 'Create'; - case FEATURE_UPDATED: - return 'Update'; - default: - return type; - } - } } diff --git a/src/lib/routes/admin-api/feature.ts b/src/lib/routes/admin-api/feature.ts index 434765ad4c..2701fc0544 100644 --- a/src/lib/routes/admin-api/feature.ts +++ b/src/lib/routes/admin-api/feature.ts @@ -289,24 +289,14 @@ class FeatureController extends Controller { const { featureName } = req.params; const userName = extractUsername(req); await this.featureService2.updateStale(featureName, true, userName); - const feature = - await this.featureService2.storeFeatureUpdatedEventLegacy( - featureName, - userName, - ); - res.json(feature).end(); + res.status(200).end(); } async staleOff(req: IAuthRequest, res: Response): Promise { const { featureName } = req.params; const userName = extractUsername(req); await this.featureService2.updateStale(featureName, false, userName); - const feature = - await this.featureService2.storeFeatureUpdatedEventLegacy( - featureName, - userName, - ); - res.json(feature).end(); + res.status(200).end(); } async archiveToggle(req: IAuthRequest, res: Response): Promise { diff --git a/src/lib/services/feature-toggle-service-v2.ts b/src/lib/services/feature-toggle-service-v2.ts index f1441a5228..9aaeb3df24 100644 --- a/src/lib/services/feature-toggle-service-v2.ts +++ b/src/lib/services/feature-toggle-service-v2.ts @@ -271,7 +271,7 @@ class FeatureToggleServiceV2 { createdBy: userName, data: { id, - featureName, + name: featureName, }, }); // If there are no strategies left for environment disable it diff --git a/src/test/e2e/api/admin/project/features.e2e.test.ts b/src/test/e2e/api/admin/project/features.e2e.test.ts index 03ef78a384..1477134873 100644 --- a/src/test/e2e/api/admin/project/features.e2e.test.ts +++ b/src/test/e2e/api/admin/project/features.e2e.test.ts @@ -1010,7 +1010,7 @@ test('Deleting a strategy should include name of feature strategy was deleted fr type: FEATURE_STRATEGY_REMOVE, }); expect(events).toHaveLength(1); - expect(events[0].data.featureName).toBe(featureName); + expect(events[0].data.name).toBe(featureName); expect(events[0].environment).toBe(environment); expect(events[0].data.id).toBe(strategyId); });