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}