From 521cc24a22162f5e9e50e90cdce88b41a6831ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Fri, 29 Sep 2023 16:11:59 +0100 Subject: [PATCH] feat: add more events in integrations (#4815) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://linear.app/unleash/issue/2-1253/add-support-for-more-events-in-the-slack-app-integration Adds support for a lot more events in our integrations. Here is how the full list looks like: - ADDON_CONFIG_CREATED - ADDON_CONFIG_DELETED - ADDON_CONFIG_UPDATED - API_TOKEN_CREATED - API_TOKEN_DELETED - CHANGE_ADDED - CHANGE_DISCARDED - CHANGE_EDITED - CHANGE_REQUEST_APPLIED - CHANGE_REQUEST_APPROVAL_ADDED - CHANGE_REQUEST_APPROVED - CHANGE_REQUEST_CANCELLED - CHANGE_REQUEST_CREATED - CHANGE_REQUEST_DISCARDED - CHANGE_REQUEST_REJECTED - CHANGE_REQUEST_SENT_TO_REVIEW - CONTEXT_FIELD_CREATED - CONTEXT_FIELD_DELETED - CONTEXT_FIELD_UPDATED - FEATURE_ARCHIVED - FEATURE_CREATED - FEATURE_DELETED - FEATURE_ENVIRONMENT_DISABLED - FEATURE_ENVIRONMENT_ENABLED - FEATURE_ENVIRONMENT_VARIANTS_UPDATED - FEATURE_METADATA_UPDATED - FEATURE_POTENTIALLY_STALE_ON - FEATURE_PROJECT_CHANGE - FEATURE_REVIVED - FEATURE_STALE_OFF - FEATURE_STALE_ON - FEATURE_STRATEGY_ADD - FEATURE_STRATEGY_REMOVE - FEATURE_STRATEGY_UPDATE - FEATURE_TAGGED - FEATURE_UNTAGGED - GROUP_CREATED - GROUP_DELETED - GROUP_UPDATED - PROJECT_CREATED - PROJECT_DELETED - SEGMENT_CREATED - SEGMENT_DELETED - SEGMENT_UPDATED - SERVICE_ACCOUNT_CREATED - SERVICE_ACCOUNT_DELETED - SERVICE_ACCOUNT_UPDATED - USER_CREATED - USER_DELETED - USER_UPDATED I added the events that I thought were relevant based on my own discretion. Know of any event we should add? Let me know and I'll add it 🙂 For now I only added these events to the new Slack App integration, but we can add them to the other integrations as well since they are now supported. The event formatter was refactored and changed quite a bit in order to make it easier to maintain and add new events in the future. As a result, events are now posted with different text. Do we consider this a breaking change? If so, I can keep the old event formatter around, create a new one and only use it for the new Slack App integration. I noticed we don't have good 404 behaviors in the UI for things that are deleted in the meantime, that's why I avoided some links to specific resources (like feature strategies, integration configurations, etc), but we could add them later if we improve this. This PR also tries to add some consistency to the the way we log events. --- .../IntegrationForm/IntegrationForm.tsx | 10 +- .../addons/__snapshots__/datadog.test.ts.snap | 12 +- .../feature-event-formatter-md.test.ts.snap | 183 +++++ .../addons/__snapshots__/slack.test.ts.snap | 10 +- .../addons/__snapshots__/teams.test.ts.snap | 10 +- src/lib/addons/datadog.ts | 2 +- .../addons/feature-event-formatter-md.test.ts | 57 +- src/lib/addons/feature-event-formatter-md.ts | 638 ++++++++++++------ src/lib/addons/slack-app-definition.ts | 90 ++- src/lib/addons/slack-app.ts | 59 +- src/lib/addons/slack.ts | 3 +- src/lib/addons/teams.ts | 3 +- src/lib/services/addon-service.test.ts | 2 +- src/lib/services/addon-service.ts | 9 +- src/lib/services/context-service.ts | 11 +- src/lib/services/feature-tag-service.ts | 4 +- src/lib/services/group-service.ts | 2 +- src/lib/services/segment-service.ts | 4 +- src/test/e2e/api/client/segment.e2e.test.ts | 2 +- .../docs/reference/integrations/slack-app.md | 56 +- 20 files changed, 819 insertions(+), 348 deletions(-) create mode 100644 src/lib/addons/__snapshots__/feature-event-formatter-md.test.ts.snap diff --git a/frontend/src/component/integrations/IntegrationForm/IntegrationForm.tsx b/frontend/src/component/integrations/IntegrationForm/IntegrationForm.tsx index 88e72044c4..84e33d0799 100644 --- a/frontend/src/component/integrations/IntegrationForm/IntegrationForm.tsx +++ b/frontend/src/component/integrations/IntegrationForm/IntegrationForm.tsx @@ -71,10 +71,12 @@ export const IntegrationForm: VFC = ({ value: environment.name, label: environment.name, })); - const selectableEvents = provider?.events?.map(event => ({ - value: event, - label: event, - })); + const selectableEvents = provider?.events + ?.map(event => ({ + value: event, + label: event, + })) + .sort((a, b) => a.label.localeCompare(b.label)); const { uiConfig } = useUiConfig(); const [formValues, setFormValues] = useState(initialValues); const [errors, setErrors] = useState<{ diff --git a/src/lib/addons/__snapshots__/datadog.test.ts.snap b/src/lib/addons/__snapshots__/datadog.test.ts.snap index dd80d0d460..c24851de7f 100644 --- a/src/lib/addons/__snapshots__/datadog.test.ts.snap +++ b/src/lib/addons/__snapshots__/datadog.test.ts.snap @@ -1,16 +1,16 @@ // 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* archived *some-toggle* in project ** \\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"}"`; +exports[`Should call datadog webhook for archived toggle with project info 1`] = `"{"text":"%%% \\n *some@user.com* archived *some-toggle* in project *[some-project](http://some-url.com/projects/some-project)* \\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"}"`; +exports[`Should call datadog webhook 1`] = `"{"text":"%%% \\n *some@user.com* created *[some-toggle](http://some-url.com/projects//features/some-toggle)* in project ** \\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"}"`; +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)* for the *development* environment in project *[default](http://some-url.com/projects/default)* \\n %%% ","title":"Unleash notification update"}"`; exports[`Should call datadog webhook with JSON when template set 1`] = `"{"text":"{\\n \\"event\\": \\"feature-created\\",\\n \\"createdBy\\": \\"some@user.com\\"\\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"}"`; +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)* for the *development* environment in project *[default](http://some-url.com/projects/default)* \\n %%% ","title":"Unleash notification update"}"`; exports[`Should include customHeaders in headers when calling service 2`] = ` { @@ -20,7 +20,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)* for the *development* environment in project *[default](http://some-url.com/projects/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 2`] = ` { diff --git a/src/lib/addons/__snapshots__/feature-event-formatter-md.test.ts.snap b/src/lib/addons/__snapshots__/feature-event-formatter-md.test.ts.snap new file mode 100644 index 0000000000..86170f3590 --- /dev/null +++ b/src/lib/addons/__snapshots__/feature-event-formatter-md.test.ts.snap @@ -0,0 +1,183 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Should format specialised text for events when IPs changed 1`] = ` +{ + "text": "*user@company.com* updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *[my-other-project](unleashUrl/projects/my-other-project)* by updating strategy *remoteAddress* in *production* IPs from empty set of IPs to [127.0.0.1]; constraints from empty set of constraints to [appName is one of (x,y)]", + "url": "unleashUrl/projects/my-other-project/features/new-feature", +} +`; + +exports[`Should format specialised text for events when constraints and rollout percentage and stickiness changed 1`] = ` +{ + "text": "*user@company.com* updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *[my-other-project](unleashUrl/projects/my-other-project)* by updating strategy *flexibleRollout* in *production* stickiness from default to random; rollout from 67% to 32%; constraints from empty set of constraints to [appName is one of (x,y)]", + "url": "unleashUrl/projects/my-other-project/features/new-feature", +} +`; + +exports[`Should format specialised text for events when default strategy updated 1`] = ` +{ + "text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from empty set of constraints to [appName is one of (x,y), appName not is one of (x)]", + "url": "unleashUrl/projects/default/features/aaa", +} +`; + +exports[`Should format specialised text for events when default strategy updated 2`] = ` +{ + "text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from empty set of constraints to [appName is not one of (x,y), appName not is not one of (x)]", + "url": "unleashUrl/projects/default/features/aaa", +} +`; + +exports[`Should format specialised text for events when default strategy updated 3`] = ` +{ + "text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from empty set of constraints to [appName is a string that contains (x,y), appName not is a string that contains (x)]", + "url": "unleashUrl/projects/default/features/aaa", +} +`; + +exports[`Should format specialised text for events when default strategy updated 4`] = ` +{ + "text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from empty set of constraints to [appName is a string that starts with (x,y), appName not is a string that starts with (x)]", + "url": "unleashUrl/projects/default/features/aaa", +} +`; + +exports[`Should format specialised text for events when default strategy updated 5`] = ` +{ + "text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from empty set of constraints to [appName is a string that ends with (x,y), appName not is a string that ends with (x)]", + "url": "unleashUrl/projects/default/features/aaa", +} +`; + +exports[`Should format specialised text for events when default strategy updated with numeric constraint DATE_AFTER 1`] = ` +{ + "text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a date after 4] to empty set of constraints", + "url": "unleashUrl/projects/default/features/aaa", +} +`; + +exports[`Should format specialised text for events when default strategy updated with numeric constraint DATE_BEFORE 1`] = ` +{ + "text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a date before 4] to empty set of constraints", + "url": "unleashUrl/projects/default/features/aaa", +} +`; + +exports[`Should format specialised text for events when default strategy updated with numeric constraint NUM_EQ 1`] = ` +{ + "text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a number equal to 4] to empty set of constraints", + "url": "unleashUrl/projects/default/features/aaa", +} +`; + +exports[`Should format specialised text for events when default strategy updated with numeric constraint NUM_GT 1`] = ` +{ + "text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a number greater than 4] to empty set of constraints", + "url": "unleashUrl/projects/default/features/aaa", +} +`; + +exports[`Should format specialised text for events when default strategy updated with numeric constraint NUM_GTE 1`] = ` +{ + "text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a number greater than or equal to 4] to empty set of constraints", + "url": "unleashUrl/projects/default/features/aaa", +} +`; + +exports[`Should format specialised text for events when default strategy updated with numeric constraint NUM_LT 1`] = ` +{ + "text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a number less than 4] to empty set of constraints", + "url": "unleashUrl/projects/default/features/aaa", +} +`; + +exports[`Should format specialised text for events when default strategy updated with numeric constraint NUM_LTE 1`] = ` +{ + "text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a number less than or equal to 4] to empty set of constraints", + "url": "unleashUrl/projects/default/features/aaa", +} +`; + +exports[`Should format specialised text for events when default strategy updated with numeric constraint SEMVER_EQ 1`] = ` +{ + "text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a SemVer equal to 4] to empty set of constraints", + "url": "unleashUrl/projects/default/features/aaa", +} +`; + +exports[`Should format specialised text for events when default strategy updated with numeric constraint SEMVER_GT 1`] = ` +{ + "text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a SemVer greater than 4] to empty set of constraints", + "url": "unleashUrl/projects/default/features/aaa", +} +`; + +exports[`Should format specialised text for events when default strategy updated with numeric constraint SEMVER_LT 1`] = ` +{ + "text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a SemVer less than 4] to empty set of constraints", + "url": "unleashUrl/projects/default/features/aaa", +} +`; + +exports[`Should format specialised text for events when groupId changed 1`] = ` +{ + "text": "*user@company.com* updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *[my-other-project](unleashUrl/projects/my-other-project)* by updating strategy *flexibleRollout* in *production* groupId from new-feature to different-feature", + "url": "unleashUrl/projects/my-other-project/features/new-feature", +} +`; + +exports[`Should format specialised text for events when host names changed 1`] = ` +{ + "text": "*user@company.com* updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *[my-other-project](unleashUrl/projects/my-other-project)* by updating strategy *applicationHostname* in *production* hostNames from empty set of hostNames to [unleash.com]; constraints from empty set of constraints to [appName is one of (x,y)]", + "url": "unleashUrl/projects/my-other-project/features/new-feature", +} +`; + +exports[`Should format specialised text for events when neither rollout percentage nor stickiness changed 1`] = ` +{ + "text": "*user@company.com* updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *[my-other-project](unleashUrl/projects/my-other-project)* by updating strategy *flexibleRollout* in *production*", + "url": "unleashUrl/projects/my-other-project/features/new-feature", +} +`; + +exports[`Should format specialised text for events when no specific text for strategy exists yet 1`] = ` +{ + "text": "*user@company.com* updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *[my-other-project](unleashUrl/projects/my-other-project)* by updating strategy *newStrategy* in *production*", + "url": "unleashUrl/projects/my-other-project/features/new-feature", +} +`; + +exports[`Should format specialised text for events when rollout percentage changed 1`] = ` +{ + "text": "*user@company.com* updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *[my-other-project](unleashUrl/projects/my-other-project)* by updating strategy *flexibleRollout* in *production* rollout from 67% to 32%", + "url": "unleashUrl/projects/my-other-project/features/new-feature", +} +`; + +exports[`Should format specialised text for events when stickiness changed 1`] = ` +{ + "text": "*user@company.com* updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *[my-other-project](unleashUrl/projects/my-other-project)* by updating strategy *flexibleRollout* in *production* stickiness from default to random", + "url": "unleashUrl/projects/my-other-project/features/new-feature", +} +`; + +exports[`Should format specialised text for events when strategy added 1`] = ` +{ + "text": "*user@company.com* added strategy *flexibleRollout* to *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* for the *production* environment in project *[my-other-project](unleashUrl/projects/my-other-project)*", + "url": "unleashUrl/projects/my-other-project/features/new-feature", +} +`; + +exports[`Should format specialised text for events when strategy removed 1`] = ` +{ + "text": "*user@company.com* removed strategy *default* from *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* for the *production* environment in project *[my-other-project](unleashUrl/projects/my-other-project)*", + "url": "unleashUrl/projects/my-other-project/features/new-feature", +} +`; + +exports[`Should format specialised text for events when userIds changed 1`] = ` +{ + "text": "*user@company.com* updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *[my-other-project](unleashUrl/projects/my-other-project)* by updating strategy *userWithId* in *production* userIds from empty set of userIds to [a,b]; constraints from empty set of constraints to [appName is one of (x,y)]", + "url": "unleashUrl/projects/my-other-project/features/new-feature", +} +`; diff --git a/src/lib/addons/__snapshots__/slack.test.ts.snap b/src/lib/addons/__snapshots__/slack.test.ts.snap index ef1dfa2ef3..65fc2beb02 100644 --- a/src/lib/addons/__snapshots__/slack.test.ts.snap +++ b/src/lib/addons/__snapshots__/slack.test.ts.snap @@ -1,14 +1,14 @@ // 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 in project *default*","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; +exports[`Should call slack webhook 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"*some@user.com* created ** in project **","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/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":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/archive"}]}]}"`; +exports[`Should call slack webhook for archived toggle 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"*some@user.com* archived *some-toggle* in project **","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects//archive"}]}]}"`; -exports[`Should call slack webhook for archived toggle with project info 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":" some@user.com just archived feature toggle **","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/some-project/archive"}]}]}"`; +exports[`Should call slack webhook for archived toggle with project info 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"*some@user.com* archived *some-toggle* in project **","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/some-project/archive"}]}]}"`; -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":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; +exports[`Should call webhook for toggled environment 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"*some@user.com* disabled ** for the *development* environment in project **","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; -exports[`Should include custom headers from parameters in call to service 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"some@user.com *disabled* in *development* environment in project *default*","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; +exports[`Should include custom headers from parameters in call to service 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"*some@user.com* disabled ** for the *development* environment in project **","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; exports[`Should include custom headers from parameters in call to service 2`] = ` { diff --git a/src/lib/addons/__snapshots__/teams.test.ts.snap b/src/lib/addons/__snapshots__/teams.test.ts.snap index 76877e1d36..7f25aeaa35 100644 --- a/src/lib/addons/__snapshots__/teams.test.ts.snap +++ b/src/lib/addons/__snapshots__/teams.test.ts.snap @@ -1,14 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -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//features/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//features/some-toggle"}]}]}"`; +exports[`Should call teams webhook 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* created *[some-toggle](http://some-url.com/projects//features/some-toggle)* in project **","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//features/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 archived toggle 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* archived *some-toggle* in project **","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/projects//archive"}]}]}"`; -exports[`Should call teams webhook for archived toggle with project info 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":" some@user.com just archived feature toggle *[some-toggle](http://some-url.com/projects/some-project/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/projects/some-project/archive"}]}]}"`; +exports[`Should call teams webhook for archived toggle with project info 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* archived *some-toggle* in project *[some-project](http://some-url.com/projects/some-project)*","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/projects/some-project/archive"}]}]}"`; -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/features/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/features/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/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/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/features/some-toggle"}]}]}"`; -exports[`Should include custom headers in call to teams 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/features/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/features/some-toggle"}]}]}"`; +exports[`Should include custom headers in call to teams 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/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/features/some-toggle"}]}]}"`; exports[`Should include custom headers in call to teams 2`] = ` { diff --git a/src/lib/addons/datadog.ts b/src/lib/addons/datadog.ts index ca931c84e4..6835aa8d19 100644 --- a/src/lib/addons/datadog.ts +++ b/src/lib/addons/datadog.ts @@ -68,7 +68,7 @@ export default class DatadogAddon extends Addon { ) { text = Mustache.render(bodyTemplate, context); } else { - text = `%%% \n ${this.msgFormatter.format(event)} \n %%% `; + text = `%%% \n ${this.msgFormatter.format(event).text} \n %%% `; } const { tags: eventTags } = event; diff --git a/src/lib/addons/feature-event-formatter-md.test.ts b/src/lib/addons/feature-event-formatter-md.test.ts index 91e83035b3..0a7a090fa1 100644 --- a/src/lib/addons/feature-event-formatter-md.test.ts +++ b/src/lib/addons/feature-event-formatter-md.test.ts @@ -24,7 +24,7 @@ import { STR_STARTS_WITH, } from '../util'; -const testCases: [string, IEvent, string][] = [ +const testCases: [string, IEvent][] = [ [ 'when groupId changed', { @@ -57,7 +57,6 @@ const testCases: [string, IEvent, string][] = [ project: 'my-other-project', environment: 'production', }, - 'user@company.com updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *my-other-project* by updating strategy flexibleRollout in *production* groupId from new-feature to different-feature', ], [ 'when rollout percentage changed', @@ -91,7 +90,6 @@ const testCases: [string, IEvent, string][] = [ project: 'my-other-project', environment: 'production', }, - 'user@company.com updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *my-other-project* by updating strategy flexibleRollout in *production* rollout from 67% to 32%', ], [ 'when stickiness changed', @@ -125,7 +123,6 @@ const testCases: [string, IEvent, string][] = [ project: 'my-other-project', environment: 'production', }, - 'user@company.com updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *my-other-project* by updating strategy flexibleRollout in *production* stickiness from default to random', ], [ 'when constraints and rollout percentage and stickiness changed', @@ -167,7 +164,6 @@ const testCases: [string, IEvent, string][] = [ project: 'my-other-project', environment: 'production', }, - 'user@company.com updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *my-other-project* by updating strategy flexibleRollout in *production* stickiness from default to random; rollout from 67% to 32%; constraints from empty set of constraints to [appName is one of (x,y)]', ], [ 'when neither rollout percentage nor stickiness changed', @@ -201,7 +197,6 @@ const testCases: [string, IEvent, string][] = [ project: 'my-other-project', environment: 'production', }, - 'user@company.com updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *my-other-project* by updating strategy flexibleRollout in *production*', ], [ 'when strategy added', @@ -226,7 +221,6 @@ const testCases: [string, IEvent, string][] = [ project: 'my-other-project', environment: 'production', }, - 'user@company.com updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *my-other-project* by adding strategy flexibleRollout in *production*', ], [ 'when strategy removed', @@ -247,17 +241,10 @@ const testCases: [string, IEvent, string][] = [ project: 'my-other-project', environment: 'production', }, - 'user@company.com updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *my-other-project* by removing strategy default in *production*', ], - ...[ - [IN, 'is one of'], - [NOT_IN, 'is not one of'], - [STR_CONTAINS, 'is a string that contains'], - [STR_STARTS_WITH, 'is a string that starts with'], - [STR_ENDS_WITH, 'is a string that ends with'], - ].map( - ([operator, display]) => - <[string, IEvent, string]>[ + ...[IN, NOT_IN, STR_CONTAINS, STR_STARTS_WITH, STR_ENDS_WITH].map( + (operator) => + <[string, IEvent]>[ 'when default strategy updated', { id: 39, @@ -298,23 +285,22 @@ const testCases: [string, IEvent, string][] = [ project: 'default', environment: 'production', }, - `admin updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *default* by updating strategy default in *production* constraints from empty set of constraints to [appName ${display} (x,y), appName not ${display} (x)]`, ], ), ...[ - [NUM_EQ, 'is a number equal to'], - [NUM_GT, 'is a number greater than'], - [NUM_GTE, 'is a number greater than or equal to'], - [NUM_LT, 'is a number less than'], - [NUM_LTE, 'is a number less than or equal to'], - [DATE_BEFORE, 'is a date before'], - [DATE_AFTER, 'is a date after'], - [SEMVER_EQ, 'is a SemVer equal to'], - [SEMVER_GT, 'is a SemVer greater than'], - [SEMVER_LT, 'is a SemVer less than'], + NUM_EQ, + NUM_GT, + NUM_GTE, + NUM_LT, + NUM_LTE, + DATE_BEFORE, + DATE_AFTER, + SEMVER_EQ, + SEMVER_GT, + SEMVER_LT, ].map( - ([operator, display]) => - <[string, IEvent, string]>[ + (operator) => + <[string, IEvent]>[ `when default strategy updated with numeric constraint ${operator}`, { id: 39, @@ -349,7 +335,6 @@ const testCases: [string, IEvent, string][] = [ project: 'default', environment: 'production', }, - `admin updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *default* by updating strategy default in *production* constraints from [appName ${display} 4] to empty set of constraints`, ], ), [ @@ -390,7 +375,6 @@ const testCases: [string, IEvent, string][] = [ project: 'my-other-project', environment: 'production', }, - 'user@company.com updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *my-other-project* by updating strategy userWithId in *production* userIds from empty set of userIds to [a,b]; constraints from empty set of constraints to [appName is one of (x,y)]', ], [ 'when IPs changed', @@ -426,7 +410,6 @@ const testCases: [string, IEvent, string][] = [ project: 'my-other-project', environment: 'production', }, - 'user@company.com updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *my-other-project* by updating strategy remoteAddress in *production* IPs from empty set of IPs to [127.0.0.1]; constraints from empty set of constraints to [appName is one of (x,y)]', ], [ 'when host names changed', @@ -462,7 +445,6 @@ const testCases: [string, IEvent, string][] = [ project: 'my-other-project', environment: 'production', }, - 'user@company.com updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *my-other-project* by updating strategy applicationHostname in *production* hostNames from empty set of hostNames to [unleash.com]; constraints from empty set of constraints to [appName is one of (x,y)]', ], [ 'when no specific text for strategy exists yet', @@ -498,14 +480,13 @@ const testCases: [string, IEvent, string][] = [ project: 'my-other-project', environment: 'production', }, - 'user@company.com updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *my-other-project* by updating strategy newStrategy in *production*', ], ]; -testCases.forEach(([description, event, expected]) => +testCases.forEach(([description, event]) => test(`Should format specialised text for events ${description}`, () => { const formatter = new FeatureEventFormatterMd('unleashUrl'); - const actual = formatter.format(event); - expect(actual).toBe(expected); + const formattedEvent = formatter.format(event); + expect(formattedEvent).toMatchSnapshot(); }), ); diff --git a/src/lib/addons/feature-event-formatter-md.ts b/src/lib/addons/feature-event-formatter-md.ts index 1f32c7e7f6..0351235a5f 100644 --- a/src/lib/addons/feature-event-formatter-md.ts +++ b/src/lib/addons/feature-event-formatter-md.ts @@ -1,8 +1,30 @@ +import Mustache from 'mustache'; import { + ADDON_CONFIG_CREATED, + ADDON_CONFIG_DELETED, + ADDON_CONFIG_UPDATED, + API_TOKEN_CREATED, + API_TOKEN_DELETED, + CHANGE_ADDED, + CHANGE_DISCARDED, + CHANGE_EDITED, + CHANGE_REQUEST_APPLIED, + CHANGE_REQUEST_APPROVAL_ADDED, + CHANGE_REQUEST_APPROVED, + CHANGE_REQUEST_CANCELLED, + CHANGE_REQUEST_CREATED, + CHANGE_REQUEST_DISCARDED, + CHANGE_REQUEST_REJECTED, + CHANGE_REQUEST_SENT_TO_REVIEW, + CONTEXT_FIELD_CREATED, + CONTEXT_FIELD_DELETED, + CONTEXT_FIELD_UPDATED, FEATURE_ARCHIVED, FEATURE_CREATED, + FEATURE_DELETED, FEATURE_ENVIRONMENT_DISABLED, FEATURE_ENVIRONMENT_ENABLED, + FEATURE_ENVIRONMENT_VARIANTS_UPDATED, FEATURE_METADATA_UPDATED, FEATURE_POTENTIALLY_STALE_ON, FEATURE_PROJECT_CHANGE, @@ -12,21 +34,247 @@ import { FEATURE_STRATEGY_ADD, FEATURE_STRATEGY_REMOVE, FEATURE_STRATEGY_UPDATE, - FEATURE_UPDATED, - FEATURE_VARIANTS_UPDATED, + FEATURE_TAGGED, + FEATURE_UNTAGGED, + GROUP_CREATED, + GROUP_DELETED, + GROUP_UPDATED, IConstraint, IEvent, + PROJECT_CREATED, + PROJECT_DELETED, + SEGMENT_CREATED, + SEGMENT_DELETED, + SEGMENT_UPDATED, + SERVICE_ACCOUNT_CREATED, + SERVICE_ACCOUNT_DELETED, + SERVICE_ACCOUNT_UPDATED, + USER_CREATED, + USER_DELETED, + USER_UPDATED, } from '../types'; +interface IEventData { + action: string; + path?: string; +} + +interface IFormattedEventData { + text: string; + url?: string; +} + export interface FeatureEventFormatter { - format: (event: IEvent) => string; - featureLink: (event: IEvent) => string; + format: (event: IEvent) => IFormattedEventData; } export enum LinkStyle { SLACK = 0, MD = 1, } +const EVENT_MAP: Record = { + [ADDON_CONFIG_CREATED]: { + action: '*{{user}}* created a new *{{event.data.provider}}* integration configuration', + path: '/integrations', + }, + [ADDON_CONFIG_DELETED]: { + action: '*{{user}}* deleted a *{{event.preData.provider}}* integration configuration', + path: '/integrations', + }, + [ADDON_CONFIG_UPDATED]: { + action: '*{{user}}* updated a *{{event.preData.provider}}* integration configuration', + path: '/integrations', + }, + [API_TOKEN_CREATED]: { + action: '*{{user}}* created API token *{{event.data.username}}*', + path: '/admin/api', + }, + [API_TOKEN_DELETED]: { + action: '*{{user}}* deleted API token *{{event.preData.username}}*', + path: '/admin/api', + }, + [CHANGE_ADDED]: { + action: '*{{user}}* added a change to change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_DISCARDED]: { + action: '*{{user}}* discarded a change in change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_EDITED]: { + action: '*{{user}}* edited a change in change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_APPLIED]: { + action: '*{{user}}* applied change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_APPROVAL_ADDED]: { + action: '*{{user}}* added an approval to change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_APPROVED]: { + action: '*{{user}}* approved change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_CANCELLED]: { + action: '*{{user}}* cancelled change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_CREATED]: { + action: '*{{user}}* created change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_DISCARDED]: { + action: '*{{user}}* discarded change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_REJECTED]: { + action: '*{{user}}* rejected change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_SENT_TO_REVIEW]: { + action: '*{{user}}* sent to review change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CONTEXT_FIELD_CREATED]: { + action: '*{{user}}* created context field *{{event.data.name}}*', + path: '/context', + }, + [CONTEXT_FIELD_DELETED]: { + action: '*{{user}}* deleted context field *{{event.preData.name}}*', + path: '/context', + }, + [CONTEXT_FIELD_UPDATED]: { + action: '*{{user}}* updated context field *{{event.preData.name}}*', + path: '/context', + }, + [FEATURE_ARCHIVED]: { + action: '*{{user}}* archived *{{event.featureName}}* in project *{{project}}*', + path: '/projects/{{event.project}}/archive', + }, + [FEATURE_CREATED]: { + action: '*{{user}}* created *{{feature}}* in project *{{project}}*', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_DELETED]: { + action: '*{{user}}* deleted *{{event.featureName}}* in project *{{project}}*', + path: '/projects/{{event.project}}', + }, + [FEATURE_ENVIRONMENT_DISABLED]: { + action: '*{{user}}* disabled *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_ENVIRONMENT_ENABLED]: { + action: '*{{user}}* disabled *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_ENVIRONMENT_VARIANTS_UPDATED]: { + action: '*{{user}}* updated variants for *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', + path: '/projects/{{event.project}}/features/{{event.featureName}}/variants', + }, + [FEATURE_METADATA_UPDATED]: { + action: '*{{user}}* updated *{{feature}}* metadata in project *{{project}}*', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_POTENTIALLY_STALE_ON]: { + action: '*{{feature}}* was marked as potentially stale in project *{{project}}*', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_PROJECT_CHANGE]: { + action: '*{{user}}* moved *{{feature}}* from *{{event.data.oldProject}}* to *{{project}}*', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_REVIVED]: { + action: '*{{user}}* revived *{{feature}}* in project *{{project}}*', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_STALE_OFF]: { + action: '*{{user}}* removed the stale marking on *{{feature}}* in project *{{project}}*', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_STALE_ON]: { + action: '*{{user}}* marked *{{feature}}* as stale in project *{{project}}*', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_STRATEGY_ADD]: { + action: '*{{user}}* added strategy *{{strategyTitle}}* to *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_STRATEGY_REMOVE]: { + action: '*{{user}}* removed strategy *{{strategyTitle}}* from *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_STRATEGY_UPDATE]: { + action: '*{{user}}* updated *{{feature}}* in project *{{project}}* {{strategyChangeText}}', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_TAGGED]: { + action: '*{{user}}* tagged *{{feature}}* with *{{event.data.type}}:{{event.data.value}}* in project *{{project}}*', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_UNTAGGED]: { + action: '*{{user}}* untagged *{{feature}}* with *{{event.preData.type}}:{{event.preData.value}}* in project *{{project}}*', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [GROUP_CREATED]: { + action: '*{{user}}* created group *{{event.data.name}}*', + path: '/admin/groups', + }, + [GROUP_DELETED]: { + action: '*{{user}}* deleted group *{{event.preData.name}}*', + path: '/admin/groups', + }, + [GROUP_UPDATED]: { + action: '*{{user}}* updated group *{{event.preData.name}}*', + path: '/admin/groups', + }, + [PROJECT_CREATED]: { + action: '*{{user}}* created project *{{project}}*', + path: '/projects', + }, + [PROJECT_DELETED]: { + action: '*{{user}}* deleted project *{{event.project}}*', + path: '/projects', + }, + [SEGMENT_CREATED]: { + action: '*{{user}}* created segment *{{event.data.name}}*', + path: '/segments', + }, + [SEGMENT_DELETED]: { + action: '*{{user}}* deleted segment *{{event.preData.name}}*', + path: '/segments', + }, + [SEGMENT_UPDATED]: { + action: '*{{user}}* updated segment *{{event.preData.name}}*', + path: '/segments', + }, + [SERVICE_ACCOUNT_CREATED]: { + action: '*{{user}}* created service account *{{event.data.name}}*', + path: '/admin/service-accounts', + }, + [SERVICE_ACCOUNT_DELETED]: { + action: '*{{user}}* deleted service account *{{event.preData.name}}*', + path: '/admin/service-accounts', + }, + [SERVICE_ACCOUNT_UPDATED]: { + action: '*{{user}}* updated service account *{{event.preData.name}}*', + path: '/admin/service-accounts', + }, + [USER_CREATED]: { + action: '*{{user}}* created user *{{event.data.name}}*', + path: '/admin/users', + }, + [USER_DELETED]: { + action: '*{{user}}* deleted user *{{event.preData.name}}*', + path: '/admin/users', + }, + [USER_UPDATED]: { + action: '*{{user}}* updated user *{{event.preData.name}}*', + path: '/admin/users', + }, +}; + export class FeatureEventFormatterMd implements FeatureEventFormatter { private readonly unleashUrl: string; @@ -37,174 +285,173 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter { 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.featureName}>`; - } else { - return `[${event.featureName}](${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, preData } = event; - const feature = this.generateFeatureLink(event); - const strategyText = () => { - switch (data.name) { - case 'flexibleRollout': - return this.flexibleRolloutStrategyChangeText( - preData, - data, - environment, - ); - case 'default': - return this.defaultStrategyChangeText( - preData, - data, - environment, - ); - case 'userWithId': - return this.userWithIdStrategyChangeText( - preData, - data, - environment, - ); - case 'remoteAddress': - return this.remoteAddressStrategyChangeText( - preData, - data, - environment, - ); - case 'applicationHostname': - return this.applicationHostnameStrategyChangeText( - preData, - data, - environment, - ); - default: - return `by updating strategy ${data?.name} in *${environment}*`; + generateChangeRequestLink(event: IEvent): string | undefined { + const { preData, data, project, environment } = event; + const changeRequestId = + data?.changeRequestId || preData?.changeRequestId; + if (project && changeRequestId) { + const url = `${this.unleashUrl}/projects/${project}/change-requests/${changeRequestId}`; + const text = `#${changeRequestId}`; + const featureLink = this.generateFeatureLink(event); + const featureText = featureLink + ? ` for feature toggle *${featureLink}*` + : ''; + const environmentText = environment + ? ` in *${environment}* environment` + : ''; + const projectLink = this.generateProjectLink(event); + const projectText = project ? ` in project *${projectLink}*` : ''; + if (this.linkStyle === LinkStyle.SLACK) { + return `*<${url}|${text}>*${featureText}${environmentText}${projectText}`; + } else { + return `*[${text}](${url})*${featureText}${environmentText}${projectText}`; } - }; - - return `${createdBy} updated *${feature}* in project *${project}* ${strategyText()}`; + } } - private applicationHostnameStrategyChangeText( - preData, - data, - environment: string | undefined, - ) { - return this.listOfValuesStrategyChangeText( - preData, - data, - environment, - 'hostNames', + featureLink(event: IEvent): string | undefined { + const { type, project = '', featureName } = event; + if (type === FEATURE_ARCHIVED) { + if (project) { + return `${this.unleashUrl}/projects/${project}/archive`; + } + return `${this.unleashUrl}/archive`; + } + + if (featureName) { + return `${this.unleashUrl}/projects/${project}/features/${featureName}`; + } + } + + generateFeatureLink(event: IEvent): string | undefined { + if (event.featureName) { + if (this.linkStyle === LinkStyle.SLACK) { + return `<${this.featureLink(event)}|${event.featureName}>`; + } else { + return `[${event.featureName}](${this.featureLink(event)})`; + } + } + } + + generateProjectLink(event: IEvent): string | undefined { + if (event.project) { + if (this.linkStyle === LinkStyle.SLACK) { + return `<${this.unleashUrl}/projects/${event.project}|${event.project}>`; + } else { + return `[${event.project}](${this.unleashUrl}/projects/${event.project})`; + } + } + } + + getStrategyTitle(event: IEvent): string | undefined { + return ( + event.preData?.title || + event.data?.title || + event.preData?.name || + event.data?.name ); } - private remoteAddressStrategyChangeText( - preData, - data, - environment: string | undefined, - ) { - return this.listOfValuesStrategyChangeText( - preData, - data, - environment, - 'IPs', - ); + generateFeatureStrategyChangeText(event: IEvent): string | undefined { + const { environment, data, preData, type } = event; + if (type === FEATURE_STRATEGY_UPDATE && (data || preData)) { + const strategyText = () => { + switch ((data || preData).name) { + case 'flexibleRollout': + return this.flexibleRolloutStrategyChangeText(event); + case 'default': + return this.defaultStrategyChangeText(event); + case 'userWithId': + return this.userWithIdStrategyChangeText(event); + case 'remoteAddress': + return this.remoteAddressStrategyChangeText(event); + case 'applicationHostname': + return this.applicationHostnameStrategyChangeText( + event, + ); + default: + return `by updating strategy *${this.getStrategyTitle( + event, + )}* in *${environment}*`; + } + }; + + return strategyText(); + } } - private userWithIdStrategyChangeText( - preData, - data, - environment: string | undefined, - ) { - return this.listOfValuesStrategyChangeText( - preData, - data, - environment, - 'userIds', - ); + private applicationHostnameStrategyChangeText(event: IEvent) { + return this.listOfValuesStrategyChangeText(event, 'hostNames'); + } + + private remoteAddressStrategyChangeText(event: IEvent) { + return this.listOfValuesStrategyChangeText(event, 'IPs'); + } + + private userWithIdStrategyChangeText(event: IEvent) { + return this.listOfValuesStrategyChangeText(event, 'userIds'); } private listOfValuesStrategyChangeText( - preData, - data, - environment: string | undefined, + event: IEvent, propertyName: string, ) { + const { preData, data, environment } = event; const userIdText = (values) => values.length === 0 ? `empty set of ${propertyName}` : `[${values}]`; const usersText = - preData.parameters[propertyName] === data.parameters[propertyName] + preData?.parameters[propertyName] === data?.parameters[propertyName] ? '' + : !preData + ? ` ${propertyName} to ${userIdText( + data?.parameters[propertyName], + )}` : ` ${propertyName} from ${userIdText( preData.parameters[propertyName], - )} to ${userIdText(data.parameters[propertyName])}`; + )} to ${userIdText(data?.parameters[propertyName])}`; const constraintText = this.constraintChangeText( - preData.constraints, - data.constraints, + preData?.constraints, + data?.constraints, ); const strategySpecificText = [usersText, constraintText] .filter((x) => x.length) .join(';'); - return `by updating strategy ${data?.name} in *${environment}*${strategySpecificText}`; + return `by updating strategy *${this.getStrategyTitle( + event, + )}* in *${environment}*${strategySpecificText}`; } - private flexibleRolloutStrategyChangeText( - preData, - data, - environment: string | undefined, - ) { + private flexibleRolloutStrategyChangeText(event: IEvent) { + const { preData, data, environment } = event; const { rollout: oldRollout, stickiness: oldStickiness, groupId: oldGroupId, - } = preData.parameters; - const { rollout, stickiness, groupId } = data.parameters; + } = preData?.parameters || {}; + const { rollout, stickiness, groupId } = data?.parameters || {}; const stickinessText = oldStickiness === stickiness ? '' + : !oldStickiness + ? ` stickiness to ${stickiness}` : ` stickiness from ${oldStickiness} to ${stickiness}`; const rolloutText = oldRollout === rollout ? '' + : !oldRollout + ? ` rollout to ${rollout}%` : ` rollout from ${oldRollout}% to ${rollout}%`; const groupIdText = oldGroupId === groupId ? '' + : !oldGroupId + ? ` groupId to ${groupId}` : ` groupId from ${oldGroupId} to ${groupId}`; const constraintText = this.constraintChangeText( - preData.constraints, - data.constraints, + preData?.constraints, + data?.constraints, ); const strategySpecificText = [ stickinessText, @@ -214,25 +461,24 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter { ] .filter((txt) => txt.length) .join(';'); - return `by updating strategy ${data?.name} in *${environment}*${strategySpecificText}`; + return `by updating strategy *${this.getStrategyTitle( + event, + )}* in *${environment}*${strategySpecificText}`; } - private defaultStrategyChangeText( - preData, - data, - environment: string | undefined, - ) { - return `by updating strategy ${ - data?.name - } in *${environment}*${this.constraintChangeText( - preData.constraints, - data.constraints, + private defaultStrategyChangeText(event: IEvent) { + const { preData, data, environment } = event; + return `by updating strategy *${this.getStrategyTitle( + event, + )}* in *${environment}*${this.constraintChangeText( + preData?.constraints, + data?.constraints, )}`; } private constraintChangeText( - oldConstraints: IConstraint[], - newConstraints: IConstraint[], + oldConstraints: IConstraint[] = [], + newConstraints: IConstraint[] = [], ) { const formatConstraints = (constraints: IConstraint[]) => { const constraintOperatorDescriptions = { @@ -255,7 +501,7 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter { const formatConstraint = (constraint: IConstraint) => { const val = Object.hasOwn(constraint, 'value') ? constraint.value - : `(${constraint.values.join(',')})`; + : `(${constraint.values?.join(',')})`; const operator = Object.hasOwn( constraintOperatorDescriptions, constraint.operator, @@ -279,93 +525,35 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter { : ` constraints from ${oldConstraintText} to ${newConstraintText}`; } - generateStrategyRemoveText(event: IEvent): string { - const { createdBy, environment, project, preData } = event; - const feature = this.generateFeatureLink(event); - return `${createdBy} updated *${feature}* in project *${project}* by removing strategy ${preData?.name} in *${environment}*`; - } + format(event: IEvent): { + text: string; + url?: string; + } { + const { createdBy, type } = event; + const { action, path } = EVENT_MAP[type] || { + action: `triggered *${type}*`, + }; - generateStrategyAddText(event: IEvent): string { - const { createdBy, environment, project, data } = event; - const feature = this.generateFeatureLink(event); - return `${createdBy} updated *${feature}* in project *${project}* by adding strategy ${data?.name} in *${environment}*`; - } + const context = { + user: createdBy, + event, + strategyTitle: this.getStrategyTitle(event), + strategyChangeText: this.generateFeatureStrategyChangeText(event), + changeRequest: this.generateChangeRequestLink(event), + feature: this.generateFeatureLink(event), + project: this.generateProjectLink(event), + }; - generateMetadataText(event: IEvent): string { - const { createdBy, project } = event; - const feature = this.generateFeatureLink(event); - return `${createdBy} updated the metadata for ${feature} in project *${project}*`; - } + Mustache.escape = (text) => text; - generateProjectChangeText(event: IEvent): string { - const { createdBy, project, featureName } = event; - return `${createdBy} moved ${featureName} to ${project}`; - } + const text = Mustache.render(action, context); + const url = path + ? `${this.unleashUrl}${Mustache.render(path, context)}` + : undefined; - generateFeaturePotentiallyStaleOnText(event: IEvent): string { - const { project, createdBy } = event; - const feature = this.generateFeatureLink(event); - - return `${createdBy} marked feature toggle *${feature}* (in project *${project}*) as *potentially stale*.`; - } - - featureLink(event: IEvent): string { - const { type, project = '', featureName } = event; - if (type === FEATURE_ARCHIVED) { - if (project) { - return `${this.unleashUrl}/projects/${project}/archive`; - } - return `${this.unleashUrl}/archive`; - } - - return `${this.unleashUrl}/projects/${project}/features/${featureName}`; - } - - getAction(type: string): string { - switch (type) { - case FEATURE_CREATED: - return 'created'; - case FEATURE_UPDATED: - return 'updated'; - case FEATURE_VARIANTS_UPDATED: - return 'updated variants for'; - 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_REMOVE: - return this.generateStrategyRemoveText(event); - case FEATURE_STRATEGY_ADD: - return this.generateStrategyAddText(event); - case FEATURE_STRATEGY_UPDATE: - return this.generateStrategyChangeText(event); - case FEATURE_METADATA_UPDATED: - return this.generateMetadataText(event); - case FEATURE_PROJECT_CHANGE: - return this.generateProjectChangeText(event); - case FEATURE_POTENTIALLY_STALE_ON: - return this.generateFeaturePotentiallyStaleOnText(event); - default: - return this.defaultText(event); - } + return { + text, + url, + }; } } diff --git a/src/lib/addons/slack-app-definition.ts b/src/lib/addons/slack-app-definition.ts index c71b0446fc..03662d8036 100644 --- a/src/lib/addons/slack-app-definition.ts +++ b/src/lib/addons/slack-app-definition.ts @@ -13,6 +13,42 @@ import { FEATURE_PROJECT_CHANGE, FEATURE_POTENTIALLY_STALE_ON, FEATURE_ENVIRONMENT_VARIANTS_UPDATED, + FEATURE_DELETED, + FEATURE_TAGGED, + FEATURE_UNTAGGED, + CONTEXT_FIELD_CREATED, + CONTEXT_FIELD_UPDATED, + CONTEXT_FIELD_DELETED, + PROJECT_CREATED, + PROJECT_DELETED, + ADDON_CONFIG_CREATED, + ADDON_CONFIG_UPDATED, + ADDON_CONFIG_DELETED, + USER_CREATED, + USER_UPDATED, + USER_DELETED, + SEGMENT_CREATED, + SEGMENT_UPDATED, + SEGMENT_DELETED, + GROUP_CREATED, + GROUP_UPDATED, + CHANGE_REQUEST_CREATED, + CHANGE_REQUEST_DISCARDED, + CHANGE_ADDED, + CHANGE_DISCARDED, + CHANGE_EDITED, + CHANGE_REQUEST_REJECTED, + CHANGE_REQUEST_APPROVED, + CHANGE_REQUEST_APPROVAL_ADDED, + CHANGE_REQUEST_CANCELLED, + CHANGE_REQUEST_SENT_TO_REVIEW, + CHANGE_REQUEST_APPLIED, + API_TOKEN_CREATED, + API_TOKEN_DELETED, + SERVICE_ACCOUNT_CREATED, + SERVICE_ACCOUNT_DELETED, + SERVICE_ACCOUNT_UPDATED, + GROUP_DELETED, } from '../types/events'; import { IAddonDefinition } from '../types/model'; @@ -49,20 +85,56 @@ const slackAppDefinition: IAddonDefinition = { }, ], events: [ - FEATURE_CREATED, + ADDON_CONFIG_CREATED, + ADDON_CONFIG_DELETED, + ADDON_CONFIG_UPDATED, + API_TOKEN_CREATED, + API_TOKEN_DELETED, + CHANGE_ADDED, + CHANGE_DISCARDED, + CHANGE_EDITED, + CHANGE_REQUEST_APPLIED, + CHANGE_REQUEST_APPROVAL_ADDED, + CHANGE_REQUEST_APPROVED, + CHANGE_REQUEST_CANCELLED, + CHANGE_REQUEST_CREATED, + CHANGE_REQUEST_DISCARDED, + CHANGE_REQUEST_REJECTED, + CHANGE_REQUEST_SENT_TO_REVIEW, + CONTEXT_FIELD_CREATED, + CONTEXT_FIELD_DELETED, + CONTEXT_FIELD_UPDATED, FEATURE_ARCHIVED, - FEATURE_REVIVED, - FEATURE_STALE_ON, - FEATURE_STALE_OFF, - FEATURE_ENVIRONMENT_ENABLED, + FEATURE_CREATED, + FEATURE_DELETED, FEATURE_ENVIRONMENT_DISABLED, + FEATURE_ENVIRONMENT_ENABLED, FEATURE_ENVIRONMENT_VARIANTS_UPDATED, + FEATURE_METADATA_UPDATED, + FEATURE_POTENTIALLY_STALE_ON, + FEATURE_PROJECT_CHANGE, + FEATURE_REVIVED, + FEATURE_STALE_OFF, + FEATURE_STALE_ON, + FEATURE_STRATEGY_ADD, FEATURE_STRATEGY_REMOVE, FEATURE_STRATEGY_UPDATE, - FEATURE_STRATEGY_ADD, - FEATURE_METADATA_UPDATED, - FEATURE_PROJECT_CHANGE, - FEATURE_POTENTIALLY_STALE_ON, + FEATURE_TAGGED, + FEATURE_UNTAGGED, + GROUP_CREATED, + GROUP_DELETED, + GROUP_UPDATED, + PROJECT_CREATED, + PROJECT_DELETED, + SEGMENT_CREATED, + SEGMENT_DELETED, + SEGMENT_UPDATED, + SERVICE_ACCOUNT_CREATED, + SERVICE_ACCOUNT_DELETED, + SERVICE_ACCOUNT_UPDATED, + USER_CREATED, + USER_DELETED, + USER_UPDATED, ], tagTypes: [ { diff --git a/src/lib/addons/slack-app.ts b/src/lib/addons/slack-app.ts index e2b9fe0579..d5ac9d4449 100644 --- a/src/lib/addons/slack-app.ts +++ b/src/lib/addons/slack-app.ts @@ -7,6 +7,8 @@ import { WebAPIRequestError, WebAPIRateLimitedError, WebAPIHTTPError, + KnownBlock, + Block, } from '@slack/web-api'; import Addon from './addon'; @@ -78,36 +80,41 @@ export default class SlackAppAddon extends Addon { this.accessToken = accessToken; } - const text = this.msgFormatter.format(event); - const url = this.msgFormatter.featureLink(event); + const { text, url } = this.msgFormatter.format(event); + + const blocks: (Block | KnownBlock)[] = [ + { + type: 'section', + text: { + type: 'mrkdwn', + text, + }, + }, + ]; + + if (url) { + blocks.push({ + type: 'actions', + elements: [ + { + type: 'button', + url, + text: { + type: 'plain_text', + text: 'Open in Unleash', + }, + value: 'featureToggle', + style: 'primary', + }, + ], + }); + } + const requests = eventChannels.map((name) => { return this.slackClient!.chat.postMessage({ channel: name, text, - blocks: [ - { - type: 'section', - text: { - type: 'mrkdwn', - text, - }, - }, - { - type: 'actions', - elements: [ - { - type: 'button', - url, - text: { - type: 'plain_text', - text: 'Open in Unleash', - }, - value: 'featureToggle', - style: 'primary', - }, - ], - }, - ], + blocks, }); }); diff --git a/src/lib/addons/slack.ts b/src/lib/addons/slack.ts index 4400b704e2..137d3540be 100644 --- a/src/lib/addons/slack.ts +++ b/src/lib/addons/slack.ts @@ -47,8 +47,7 @@ export default class SlackAddon extends Addon { slackChannels.push(defaultChannel); } - const text = this.msgFormatter.format(event); - const featureLink = this.msgFormatter.featureLink(event); + const { text, url: featureLink } = this.msgFormatter.format(event); const requests = slackChannels.map((channel) => { const body = { diff --git a/src/lib/addons/teams.ts b/src/lib/addons/teams.ts index 44c284ee3f..3b73d4a29d 100644 --- a/src/lib/addons/teams.ts +++ b/src/lib/addons/teams.ts @@ -27,8 +27,7 @@ export default class TeamsAddon extends Addon { ): Promise { const { url, customHeaders } = parameters; const { createdBy } = event; - const text = this.msgFormatter.format(event); - const featureLink = this.msgFormatter.featureLink(event); + const { text, url: featureLink } = this.msgFormatter.format(event); const body = { themeColor: '0076D7', diff --git a/src/lib/services/addon-service.test.ts b/src/lib/services/addon-service.test.ts index 91be2a89f2..5ee506a238 100644 --- a/src/lib/services/addon-service.test.ts +++ b/src/lib/services/addon-service.test.ts @@ -634,7 +634,7 @@ test('should store ADDON_CONFIG_REMOVE event', async () => { expect(events.length).toBe(3); expect(events[2].type).toBe(ADDON_CONFIG_DELETED); - expect(events[2].data.id).toBe(addonConfig.id); + expect(events[2].preData.id).toBe(addonConfig.id); }); test('should hide sensitive fields when fetching', async () => { diff --git a/src/lib/services/addon-service.ts b/src/lib/services/addon-service.ts index 3f32bd96b8..72e4af9ec0 100644 --- a/src/lib/services/addon-service.ts +++ b/src/lib/services/addon-service.ts @@ -12,6 +12,7 @@ import { IUnleashStores, IUnleashConfig } from '../types'; import { IAddonDefinition } from '../types/model'; import { minutesToMilliseconds } from 'date-fns'; import EventService from './event-service'; +import { omitKeys } from '../util'; const SUPPORTED_EVENTS = Object.keys(events).map((k) => events[k]); @@ -205,7 +206,7 @@ export default class AddonService { await this.eventService.storeEvent({ type: events.ADDON_CONFIG_CREATED, createdBy: userName, - data: { provider: addonConfig.provider }, + data: omitKeys(createdAddon, 'parameters'), }); return createdAddon; @@ -238,18 +239,20 @@ export default class AddonService { await this.eventService.storeEvent({ type: events.ADDON_CONFIG_UPDATED, createdBy: userName, - data: { id, provider: addonConfig.provider }, + preData: omitKeys(existingConfig, 'parameters'), + data: omitKeys(result, 'parameters'), }); this.logger.info(`User ${userName} updated addon ${id}`); return result; } async removeAddon(id: number, userName: string): Promise { + const existingConfig = await this.addonStore.get(id); await this.addonStore.delete(id); await this.eventService.storeEvent({ type: events.ADDON_CONFIG_DELETED, createdBy: userName, - data: { id }, + preData: omitKeys(existingConfig, 'parameters'), }); this.logger.info(`User ${userName} removed addon ${id}`); } diff --git a/src/lib/services/context-service.ts b/src/lib/services/context-service.ts index 04e54322f4..6ad41b3325 100644 --- a/src/lib/services/context-service.ts +++ b/src/lib/services/context-service.ts @@ -130,8 +130,9 @@ class ContextService { updatedContextField: IContextFieldDto, userName: string, ): Promise { - // validations - await this.contextFieldStore.get(updatedContextField.name); + const contextField = await this.contextFieldStore.get( + updatedContextField.name, + ); const value = await contextSchema.validateAsync(updatedContextField); // update @@ -139,20 +140,20 @@ class ContextService { await this.eventService.storeEvent({ type: CONTEXT_FIELD_UPDATED, createdBy: userName, + preData: contextField, data: value, }); } async deleteContextField(name: string, userName: string): Promise { - // validate existence - await this.contextFieldStore.get(name); + const contextField = await this.contextFieldStore.get(name); // delete await this.contextFieldStore.delete(name); await this.eventService.storeEvent({ type: CONTEXT_FIELD_DELETED, createdBy: userName, - data: { name }, + preData: contextField, }); } diff --git a/src/lib/services/feature-tag-service.ts b/src/lib/services/feature-tag-service.ts index 025b85cce6..4d785435dd 100644 --- a/src/lib/services/feature-tag-service.ts +++ b/src/lib/services/feature-tag-service.ts @@ -122,7 +122,7 @@ class FeatureTagService { createdBy: userName, featureName: featureToggle.name, project: featureToggle.project, - data: removedTag, + preData: removedTag, })), ); @@ -171,7 +171,7 @@ class FeatureTagService { createdBy: userName, featureName, project: featureToggle.project, - data: tag, + preData: tag, tags, }); } diff --git a/src/lib/services/group-service.ts b/src/lib/services/group-service.ts index 6de21866ec..92da56b676 100644 --- a/src/lib/services/group-service.ts +++ b/src/lib/services/group-service.ts @@ -176,7 +176,7 @@ export class GroupService { await this.eventService.storeEvent({ type: GROUP_DELETED, createdBy: userName, - data: group, + preData: group, }); } diff --git a/src/lib/services/segment-service.ts b/src/lib/services/segment-service.ts index 8a98387dfc..fc040c54fc 100644 --- a/src/lib/services/segment-service.ts +++ b/src/lib/services/segment-service.ts @@ -161,7 +161,7 @@ export class SegmentService implements ISegmentService { await this.eventService.storeEvent({ type: SEGMENT_DELETED, createdBy: user.email || user.username, - data: segment, + preData: segment, }); } @@ -171,7 +171,7 @@ export class SegmentService implements ISegmentService { await this.eventService.storeEvent({ type: SEGMENT_DELETED, createdBy: user.email || user.username, - data: segment, + preData: segment, }); } diff --git a/src/test/e2e/api/client/segment.e2e.test.ts b/src/test/e2e/api/client/segment.e2e.test.ts index 92ab36cbf5..66fc14d2f7 100644 --- a/src/test/e2e/api/client/segment.e2e.test.ts +++ b/src/test/e2e/api/client/segment.e2e.test.ts @@ -293,7 +293,7 @@ test('should store segment-created and segment-deleted events', async () => { const events = await db.stores.eventStore.getEvents(); expect(events[0].type).toEqual('segment-deleted'); - expect(events[0].data.id).toEqual(segment1.id); + expect(events[0].preData.id).toEqual(segment1.id); expect(events[1].type).toEqual('segment-created'); expect(events[1].data.id).toEqual(segment1.id); }); diff --git a/website/docs/reference/integrations/slack-app.md b/website/docs/reference/integrations/slack-app.md index e83da6684d..ba014afb15 100644 --- a/website/docs/reference/integrations/slack-app.md +++ b/website/docs/reference/integrations/slack-app.md @@ -32,20 +32,56 @@ The configuration settings allow you to choose the events you're interested in a You can choose to trigger updates for the following events: -- feature-created -- feature-metadata-updated -- feature-project-change +- addon-config-created +- addon-config-deleted +- addon-config-updated +- api-token-created +- api-token-deleted +- change-added +- change-discarded +- change-edited +- change-request-applied +- change-request-approval-added +- change-request-approved +- change-request-cancelled +- change-request-created +- change-request-discarded +- change-request-rejected +- change-request-sent-to-review +- context-field-created +- context-field-deleted +- context-field-updated - feature-archived +- feature-created +- feature-deleted +- feature-environment-disabled +- feature-environment-enabled +- feature-environment-variants-updated +- feature-metadata-updated +- feature-potentially-stale-on +- feature-project-change - feature-revived -- feature-strategy-update +- feature-stale-off +- feature-stale-on - feature-strategy-add - feature-strategy-remove -- feature-stale-on -- feature-stale-off -- feature-environment-enabled -- feature-environment-disabled -- feature-environment-variants-updated -- feature-potentially-stale-on +- feature-strategy-update +- feature-tagged +- feature-untagged +- group-created +- group-deleted +- group-updated +- project-created +- project-deleted +- segment-created +- segment-deleted +- segment-updated +- service-account-created +- service-account-deleted +- service-account-updated +- user-created +- user-deleted +- user-updated #### Parameters {#parameters}