1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00

feat: add more events in integrations (#4815)

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.
This commit is contained in:
Nuno Góis 2023-09-29 16:11:59 +01:00 committed by GitHub
parent a0571ce022
commit 521cc24a22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 819 additions and 348 deletions

View File

@ -71,10 +71,12 @@ export const IntegrationForm: VFC<IntegrationFormProps> = ({
value: environment.name, value: environment.name,
label: environment.name, label: environment.name,
})); }));
const selectableEvents = provider?.events?.map(event => ({ const selectableEvents = provider?.events
value: event, ?.map(event => ({
label: event, value: event,
})); label: event,
}))
.sort((a, b) => a.label.localeCompare(b.label));
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const [formValues, setFormValues] = useState(initialValues); const [formValues, setFormValues] = useState(initialValues);
const [errors, setErrors] = useState<{ const [errors, setErrors] = useState<{

View File

@ -1,16 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Should call datadog webhook for archived toggle 1`] = `"{"text":"%%% \\n some@user.com just archived feature toggle *[some-toggle](http://some-url.com/archive)* \\n %%% ","title":"Unleash notification update"}"`; exports[`Should call datadog webhook for archived toggle 1`] = `"{"text":"%%% \\n *some@user.com* 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 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`] = ` 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`] = ` exports[`Should not include source_type_name when included in the config 2`] = `
{ {

View File

@ -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",
}
`;

View File

@ -1,14 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // 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 <http://some-url.com/projects/default/features/some-toggle|some-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 *<http://some-url.com/projects/default/features/some-toggle|some-toggle>* in project *<http://some-url.com/projects/default|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 for archived toggle 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":" some@user.com just archived feature toggle *<http://some-url.com/archive|some-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 *<http://some-url.com/projects/some-project/archive|some-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 *<http://some-url.com/projects/some-project|some-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* <http://some-url.com/projects/default/features/some-toggle|some-toggle> 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 *<http://some-url.com/projects/default/features/some-toggle|some-toggle>* for the *development* environment in project *<http://some-url.com/projects/default|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* <http://some-url.com/projects/default/features/some-toggle|some-toggle> 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 *<http://some-url.com/projects/default/features/some-toggle|some-toggle>* for the *development* environment in project *<http://some-url.com/projects/default|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 2`] = ` exports[`Should include custom headers from parameters in call to service 2`] = `
{ {

View File

@ -1,14 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // 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`] = ` exports[`Should include custom headers in call to teams 2`] = `
{ {

View File

@ -68,7 +68,7 @@ export default class DatadogAddon extends Addon {
) { ) {
text = Mustache.render(bodyTemplate, context); text = Mustache.render(bodyTemplate, context);
} else { } else {
text = `%%% \n ${this.msgFormatter.format(event)} \n %%% `; text = `%%% \n ${this.msgFormatter.format(event).text} \n %%% `;
} }
const { tags: eventTags } = event; const { tags: eventTags } = event;

View File

@ -24,7 +24,7 @@ import {
STR_STARTS_WITH, STR_STARTS_WITH,
} from '../util'; } from '../util';
const testCases: [string, IEvent, string][] = [ const testCases: [string, IEvent][] = [
[ [
'when groupId changed', 'when groupId changed',
{ {
@ -57,7 +57,6 @@ const testCases: [string, IEvent, string][] = [
project: 'my-other-project', project: 'my-other-project',
environment: 'production', 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', 'when rollout percentage changed',
@ -91,7 +90,6 @@ const testCases: [string, IEvent, string][] = [
project: 'my-other-project', project: 'my-other-project',
environment: 'production', 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', 'when stickiness changed',
@ -125,7 +123,6 @@ const testCases: [string, IEvent, string][] = [
project: 'my-other-project', project: 'my-other-project',
environment: 'production', 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', 'when constraints and rollout percentage and stickiness changed',
@ -167,7 +164,6 @@ const testCases: [string, IEvent, string][] = [
project: 'my-other-project', project: 'my-other-project',
environment: 'production', 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', 'when neither rollout percentage nor stickiness changed',
@ -201,7 +197,6 @@ const testCases: [string, IEvent, string][] = [
project: 'my-other-project', project: 'my-other-project',
environment: 'production', 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', 'when strategy added',
@ -226,7 +221,6 @@ const testCases: [string, IEvent, string][] = [
project: 'my-other-project', project: 'my-other-project',
environment: 'production', 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', 'when strategy removed',
@ -247,17 +241,10 @@ const testCases: [string, IEvent, string][] = [
project: 'my-other-project', project: 'my-other-project',
environment: 'production', 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, NOT_IN, STR_CONTAINS, STR_STARTS_WITH, STR_ENDS_WITH].map(
[IN, 'is one of'], (operator) =>
[NOT_IN, 'is not one of'], <[string, IEvent]>[
[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]>[
'when default strategy updated', 'when default strategy updated',
{ {
id: 39, id: 39,
@ -298,23 +285,22 @@ const testCases: [string, IEvent, string][] = [
project: 'default', project: 'default',
environment: 'production', 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_EQ,
[NUM_GT, 'is a number greater than'], NUM_GT,
[NUM_GTE, 'is a number greater than or equal to'], NUM_GTE,
[NUM_LT, 'is a number less than'], NUM_LT,
[NUM_LTE, 'is a number less than or equal to'], NUM_LTE,
[DATE_BEFORE, 'is a date before'], DATE_BEFORE,
[DATE_AFTER, 'is a date after'], DATE_AFTER,
[SEMVER_EQ, 'is a SemVer equal to'], SEMVER_EQ,
[SEMVER_GT, 'is a SemVer greater than'], SEMVER_GT,
[SEMVER_LT, 'is a SemVer less than'], SEMVER_LT,
].map( ].map(
([operator, display]) => (operator) =>
<[string, IEvent, string]>[ <[string, IEvent]>[
`when default strategy updated with numeric constraint ${operator}`, `when default strategy updated with numeric constraint ${operator}`,
{ {
id: 39, id: 39,
@ -349,7 +335,6 @@ const testCases: [string, IEvent, string][] = [
project: 'default', project: 'default',
environment: 'production', 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', project: 'my-other-project',
environment: 'production', 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', 'when IPs changed',
@ -426,7 +410,6 @@ const testCases: [string, IEvent, string][] = [
project: 'my-other-project', project: 'my-other-project',
environment: 'production', 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', 'when host names changed',
@ -462,7 +445,6 @@ const testCases: [string, IEvent, string][] = [
project: 'my-other-project', project: 'my-other-project',
environment: 'production', 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', 'when no specific text for strategy exists yet',
@ -498,14 +480,13 @@ const testCases: [string, IEvent, string][] = [
project: 'my-other-project', project: 'my-other-project',
environment: 'production', 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}`, () => { test(`Should format specialised text for events ${description}`, () => {
const formatter = new FeatureEventFormatterMd('unleashUrl'); const formatter = new FeatureEventFormatterMd('unleashUrl');
const actual = formatter.format(event); const formattedEvent = formatter.format(event);
expect(actual).toBe(expected); expect(formattedEvent).toMatchSnapshot();
}), }),
); );

View File

@ -1,8 +1,30 @@
import Mustache from 'mustache';
import { 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_ARCHIVED,
FEATURE_CREATED, FEATURE_CREATED,
FEATURE_DELETED,
FEATURE_ENVIRONMENT_DISABLED, FEATURE_ENVIRONMENT_DISABLED,
FEATURE_ENVIRONMENT_ENABLED, FEATURE_ENVIRONMENT_ENABLED,
FEATURE_ENVIRONMENT_VARIANTS_UPDATED,
FEATURE_METADATA_UPDATED, FEATURE_METADATA_UPDATED,
FEATURE_POTENTIALLY_STALE_ON, FEATURE_POTENTIALLY_STALE_ON,
FEATURE_PROJECT_CHANGE, FEATURE_PROJECT_CHANGE,
@ -12,21 +34,247 @@ import {
FEATURE_STRATEGY_ADD, FEATURE_STRATEGY_ADD,
FEATURE_STRATEGY_REMOVE, FEATURE_STRATEGY_REMOVE,
FEATURE_STRATEGY_UPDATE, FEATURE_STRATEGY_UPDATE,
FEATURE_UPDATED, FEATURE_TAGGED,
FEATURE_VARIANTS_UPDATED, FEATURE_UNTAGGED,
GROUP_CREATED,
GROUP_DELETED,
GROUP_UPDATED,
IConstraint, IConstraint,
IEvent, 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'; } from '../types';
interface IEventData {
action: string;
path?: string;
}
interface IFormattedEventData {
text: string;
url?: string;
}
export interface FeatureEventFormatter { export interface FeatureEventFormatter {
format: (event: IEvent) => string; format: (event: IEvent) => IFormattedEventData;
featureLink: (event: IEvent) => string;
} }
export enum LinkStyle { export enum LinkStyle {
SLACK = 0, SLACK = 0,
MD = 1, MD = 1,
} }
const EVENT_MAP: Record<string, IEventData> = {
[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 { export class FeatureEventFormatterMd implements FeatureEventFormatter {
private readonly unleashUrl: string; private readonly unleashUrl: string;
@ -37,174 +285,173 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter {
this.linkStyle = linkStyle; this.linkStyle = linkStyle;
} }
generateArchivedText(event: IEvent): string { generateChangeRequestLink(event: IEvent): string | undefined {
const { createdBy, type } = event; const { preData, data, project, environment } = event;
const action = type === FEATURE_ARCHIVED ? 'archived' : 'revived'; const changeRequestId =
const feature = this.generateFeatureLink(event); data?.changeRequestId || preData?.changeRequestId;
return ` ${createdBy} just ${action} feature toggle *${feature}*`; if (project && changeRequestId) {
} const url = `${this.unleashUrl}/projects/${project}/change-requests/${changeRequestId}`;
const text = `#${changeRequestId}`;
generateFeatureLink(event: IEvent): string { const featureLink = this.generateFeatureLink(event);
if (this.linkStyle === LinkStyle.SLACK) { const featureText = featureLink
return `<${this.featureLink(event)}|${event.featureName}>`; ? ` for feature toggle *${featureLink}*`
} else { : '';
return `[${event.featureName}](${this.featureLink(event)})`; const environmentText = environment
} ? ` in *${environment}* environment`
} : '';
const projectLink = this.generateProjectLink(event);
generateStaleText(event: IEvent): string { const projectText = project ? ` in project *${projectLink}*` : '';
const { createdBy, type } = event; if (this.linkStyle === LinkStyle.SLACK) {
const isStale = type === FEATURE_STALE_ON; return `*<${url}|${text}>*${featureText}${environmentText}${projectText}`;
const feature = this.generateFeatureLink(event); } else {
return `*[${text}](${url})*${featureText}${environmentText}${projectText}`;
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}*`;
} }
}; }
return `${createdBy} updated *${feature}* in project *${project}* ${strategyText()}`;
} }
private applicationHostnameStrategyChangeText( featureLink(event: IEvent): string | undefined {
preData, const { type, project = '', featureName } = event;
data, if (type === FEATURE_ARCHIVED) {
environment: string | undefined, if (project) {
) { return `${this.unleashUrl}/projects/${project}/archive`;
return this.listOfValuesStrategyChangeText( }
preData, return `${this.unleashUrl}/archive`;
data, }
environment,
'hostNames', 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( generateFeatureStrategyChangeText(event: IEvent): string | undefined {
preData, const { environment, data, preData, type } = event;
data, if (type === FEATURE_STRATEGY_UPDATE && (data || preData)) {
environment: string | undefined, const strategyText = () => {
) { switch ((data || preData).name) {
return this.listOfValuesStrategyChangeText( case 'flexibleRollout':
preData, return this.flexibleRolloutStrategyChangeText(event);
data, case 'default':
environment, return this.defaultStrategyChangeText(event);
'IPs', 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( private applicationHostnameStrategyChangeText(event: IEvent) {
preData, return this.listOfValuesStrategyChangeText(event, 'hostNames');
data, }
environment: string | undefined,
) { private remoteAddressStrategyChangeText(event: IEvent) {
return this.listOfValuesStrategyChangeText( return this.listOfValuesStrategyChangeText(event, 'IPs');
preData, }
data,
environment, private userWithIdStrategyChangeText(event: IEvent) {
'userIds', return this.listOfValuesStrategyChangeText(event, 'userIds');
);
} }
private listOfValuesStrategyChangeText( private listOfValuesStrategyChangeText(
preData, event: IEvent,
data,
environment: string | undefined,
propertyName: string, propertyName: string,
) { ) {
const { preData, data, environment } = event;
const userIdText = (values) => const userIdText = (values) =>
values.length === 0 values.length === 0
? `empty set of ${propertyName}` ? `empty set of ${propertyName}`
: `[${values}]`; : `[${values}]`;
const usersText = const usersText =
preData.parameters[propertyName] === data.parameters[propertyName] preData?.parameters[propertyName] === data?.parameters[propertyName]
? '' ? ''
: !preData
? ` ${propertyName} to ${userIdText(
data?.parameters[propertyName],
)}`
: ` ${propertyName} from ${userIdText( : ` ${propertyName} from ${userIdText(
preData.parameters[propertyName], preData.parameters[propertyName],
)} to ${userIdText(data.parameters[propertyName])}`; )} to ${userIdText(data?.parameters[propertyName])}`;
const constraintText = this.constraintChangeText( const constraintText = this.constraintChangeText(
preData.constraints, preData?.constraints,
data.constraints, data?.constraints,
); );
const strategySpecificText = [usersText, constraintText] const strategySpecificText = [usersText, constraintText]
.filter((x) => x.length) .filter((x) => x.length)
.join(';'); .join(';');
return `by updating strategy ${data?.name} in *${environment}*${strategySpecificText}`; return `by updating strategy *${this.getStrategyTitle(
event,
)}* in *${environment}*${strategySpecificText}`;
} }
private flexibleRolloutStrategyChangeText( private flexibleRolloutStrategyChangeText(event: IEvent) {
preData, const { preData, data, environment } = event;
data,
environment: string | undefined,
) {
const { const {
rollout: oldRollout, rollout: oldRollout,
stickiness: oldStickiness, stickiness: oldStickiness,
groupId: oldGroupId, groupId: oldGroupId,
} = preData.parameters; } = preData?.parameters || {};
const { rollout, stickiness, groupId } = data.parameters; const { rollout, stickiness, groupId } = data?.parameters || {};
const stickinessText = const stickinessText =
oldStickiness === stickiness oldStickiness === stickiness
? '' ? ''
: !oldStickiness
? ` stickiness to ${stickiness}`
: ` stickiness from ${oldStickiness} to ${stickiness}`; : ` stickiness from ${oldStickiness} to ${stickiness}`;
const rolloutText = const rolloutText =
oldRollout === rollout oldRollout === rollout
? '' ? ''
: !oldRollout
? ` rollout to ${rollout}%`
: ` rollout from ${oldRollout}% to ${rollout}%`; : ` rollout from ${oldRollout}% to ${rollout}%`;
const groupIdText = const groupIdText =
oldGroupId === groupId oldGroupId === groupId
? '' ? ''
: !oldGroupId
? ` groupId to ${groupId}`
: ` groupId from ${oldGroupId} to ${groupId}`; : ` groupId from ${oldGroupId} to ${groupId}`;
const constraintText = this.constraintChangeText( const constraintText = this.constraintChangeText(
preData.constraints, preData?.constraints,
data.constraints, data?.constraints,
); );
const strategySpecificText = [ const strategySpecificText = [
stickinessText, stickinessText,
@ -214,25 +461,24 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter {
] ]
.filter((txt) => txt.length) .filter((txt) => txt.length)
.join(';'); .join(';');
return `by updating strategy ${data?.name} in *${environment}*${strategySpecificText}`; return `by updating strategy *${this.getStrategyTitle(
event,
)}* in *${environment}*${strategySpecificText}`;
} }
private defaultStrategyChangeText( private defaultStrategyChangeText(event: IEvent) {
preData, const { preData, data, environment } = event;
data, return `by updating strategy *${this.getStrategyTitle(
environment: string | undefined, event,
) { )}* in *${environment}*${this.constraintChangeText(
return `by updating strategy ${ preData?.constraints,
data?.name data?.constraints,
} in *${environment}*${this.constraintChangeText(
preData.constraints,
data.constraints,
)}`; )}`;
} }
private constraintChangeText( private constraintChangeText(
oldConstraints: IConstraint[], oldConstraints: IConstraint[] = [],
newConstraints: IConstraint[], newConstraints: IConstraint[] = [],
) { ) {
const formatConstraints = (constraints: IConstraint[]) => { const formatConstraints = (constraints: IConstraint[]) => {
const constraintOperatorDescriptions = { const constraintOperatorDescriptions = {
@ -255,7 +501,7 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter {
const formatConstraint = (constraint: IConstraint) => { const formatConstraint = (constraint: IConstraint) => {
const val = Object.hasOwn(constraint, 'value') const val = Object.hasOwn(constraint, 'value')
? constraint.value ? constraint.value
: `(${constraint.values.join(',')})`; : `(${constraint.values?.join(',')})`;
const operator = Object.hasOwn( const operator = Object.hasOwn(
constraintOperatorDescriptions, constraintOperatorDescriptions,
constraint.operator, constraint.operator,
@ -279,93 +525,35 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter {
: ` constraints from ${oldConstraintText} to ${newConstraintText}`; : ` constraints from ${oldConstraintText} to ${newConstraintText}`;
} }
generateStrategyRemoveText(event: IEvent): string { format(event: IEvent): {
const { createdBy, environment, project, preData } = event; text: string;
const feature = this.generateFeatureLink(event); url?: string;
return `${createdBy} updated *${feature}* in project *${project}* by removing strategy ${preData?.name} in *${environment}*`; } {
} const { createdBy, type } = event;
const { action, path } = EVENT_MAP[type] || {
action: `triggered *${type}*`,
};
generateStrategyAddText(event: IEvent): string { const context = {
const { createdBy, environment, project, data } = event; user: createdBy,
const feature = this.generateFeatureLink(event); event,
return `${createdBy} updated *${feature}* in project *${project}* by adding strategy ${data?.name} in *${environment}*`; strategyTitle: this.getStrategyTitle(event),
} strategyChangeText: this.generateFeatureStrategyChangeText(event),
changeRequest: this.generateChangeRequestLink(event),
feature: this.generateFeatureLink(event),
project: this.generateProjectLink(event),
};
generateMetadataText(event: IEvent): string { Mustache.escape = (text) => text;
const { createdBy, project } = event;
const feature = this.generateFeatureLink(event);
return `${createdBy} updated the metadata for ${feature} in project *${project}*`;
}
generateProjectChangeText(event: IEvent): string { const text = Mustache.render(action, context);
const { createdBy, project, featureName } = event; const url = path
return `${createdBy} moved ${featureName} to ${project}`; ? `${this.unleashUrl}${Mustache.render(path, context)}`
} : undefined;
generateFeaturePotentiallyStaleOnText(event: IEvent): string { return {
const { project, createdBy } = event; text,
const feature = this.generateFeatureLink(event); url,
};
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);
}
} }
} }

View File

@ -13,6 +13,42 @@ import {
FEATURE_PROJECT_CHANGE, FEATURE_PROJECT_CHANGE,
FEATURE_POTENTIALLY_STALE_ON, FEATURE_POTENTIALLY_STALE_ON,
FEATURE_ENVIRONMENT_VARIANTS_UPDATED, 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'; } from '../types/events';
import { IAddonDefinition } from '../types/model'; import { IAddonDefinition } from '../types/model';
@ -49,20 +85,56 @@ const slackAppDefinition: IAddonDefinition = {
}, },
], ],
events: [ 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_ARCHIVED,
FEATURE_REVIVED, FEATURE_CREATED,
FEATURE_STALE_ON, FEATURE_DELETED,
FEATURE_STALE_OFF,
FEATURE_ENVIRONMENT_ENABLED,
FEATURE_ENVIRONMENT_DISABLED, FEATURE_ENVIRONMENT_DISABLED,
FEATURE_ENVIRONMENT_ENABLED,
FEATURE_ENVIRONMENT_VARIANTS_UPDATED, 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_REMOVE,
FEATURE_STRATEGY_UPDATE, FEATURE_STRATEGY_UPDATE,
FEATURE_STRATEGY_ADD, FEATURE_TAGGED,
FEATURE_METADATA_UPDATED, FEATURE_UNTAGGED,
FEATURE_PROJECT_CHANGE, GROUP_CREATED,
FEATURE_POTENTIALLY_STALE_ON, 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: [ tagTypes: [
{ {

View File

@ -7,6 +7,8 @@ import {
WebAPIRequestError, WebAPIRequestError,
WebAPIRateLimitedError, WebAPIRateLimitedError,
WebAPIHTTPError, WebAPIHTTPError,
KnownBlock,
Block,
} from '@slack/web-api'; } from '@slack/web-api';
import Addon from './addon'; import Addon from './addon';
@ -78,36 +80,41 @@ export default class SlackAppAddon extends Addon {
this.accessToken = accessToken; this.accessToken = accessToken;
} }
const text = this.msgFormatter.format(event); const { text, url } = this.msgFormatter.format(event);
const url = this.msgFormatter.featureLink(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) => { const requests = eventChannels.map((name) => {
return this.slackClient!.chat.postMessage({ return this.slackClient!.chat.postMessage({
channel: name, channel: name,
text, text,
blocks: [ 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',
},
],
},
],
}); });
}); });

View File

@ -47,8 +47,7 @@ export default class SlackAddon extends Addon {
slackChannels.push(defaultChannel); slackChannels.push(defaultChannel);
} }
const text = this.msgFormatter.format(event); const { text, url: featureLink } = this.msgFormatter.format(event);
const featureLink = this.msgFormatter.featureLink(event);
const requests = slackChannels.map((channel) => { const requests = slackChannels.map((channel) => {
const body = { const body = {

View File

@ -27,8 +27,7 @@ export default class TeamsAddon extends Addon {
): Promise<void> { ): Promise<void> {
const { url, customHeaders } = parameters; const { url, customHeaders } = parameters;
const { createdBy } = event; const { createdBy } = event;
const text = this.msgFormatter.format(event); const { text, url: featureLink } = this.msgFormatter.format(event);
const featureLink = this.msgFormatter.featureLink(event);
const body = { const body = {
themeColor: '0076D7', themeColor: '0076D7',

View File

@ -634,7 +634,7 @@ test('should store ADDON_CONFIG_REMOVE event', async () => {
expect(events.length).toBe(3); expect(events.length).toBe(3);
expect(events[2].type).toBe(ADDON_CONFIG_DELETED); 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 () => { test('should hide sensitive fields when fetching', async () => {

View File

@ -12,6 +12,7 @@ import { IUnleashStores, IUnleashConfig } from '../types';
import { IAddonDefinition } from '../types/model'; import { IAddonDefinition } from '../types/model';
import { minutesToMilliseconds } from 'date-fns'; import { minutesToMilliseconds } from 'date-fns';
import EventService from './event-service'; import EventService from './event-service';
import { omitKeys } from '../util';
const SUPPORTED_EVENTS = Object.keys(events).map((k) => events[k]); const SUPPORTED_EVENTS = Object.keys(events).map((k) => events[k]);
@ -205,7 +206,7 @@ export default class AddonService {
await this.eventService.storeEvent({ await this.eventService.storeEvent({
type: events.ADDON_CONFIG_CREATED, type: events.ADDON_CONFIG_CREATED,
createdBy: userName, createdBy: userName,
data: { provider: addonConfig.provider }, data: omitKeys(createdAddon, 'parameters'),
}); });
return createdAddon; return createdAddon;
@ -238,18 +239,20 @@ export default class AddonService {
await this.eventService.storeEvent({ await this.eventService.storeEvent({
type: events.ADDON_CONFIG_UPDATED, type: events.ADDON_CONFIG_UPDATED,
createdBy: userName, createdBy: userName,
data: { id, provider: addonConfig.provider }, preData: omitKeys(existingConfig, 'parameters'),
data: omitKeys(result, 'parameters'),
}); });
this.logger.info(`User ${userName} updated addon ${id}`); this.logger.info(`User ${userName} updated addon ${id}`);
return result; return result;
} }
async removeAddon(id: number, userName: string): Promise<void> { async removeAddon(id: number, userName: string): Promise<void> {
const existingConfig = await this.addonStore.get(id);
await this.addonStore.delete(id); await this.addonStore.delete(id);
await this.eventService.storeEvent({ await this.eventService.storeEvent({
type: events.ADDON_CONFIG_DELETED, type: events.ADDON_CONFIG_DELETED,
createdBy: userName, createdBy: userName,
data: { id }, preData: omitKeys(existingConfig, 'parameters'),
}); });
this.logger.info(`User ${userName} removed addon ${id}`); this.logger.info(`User ${userName} removed addon ${id}`);
} }

View File

@ -130,8 +130,9 @@ class ContextService {
updatedContextField: IContextFieldDto, updatedContextField: IContextFieldDto,
userName: string, userName: string,
): Promise<void> { ): Promise<void> {
// validations const contextField = await this.contextFieldStore.get(
await this.contextFieldStore.get(updatedContextField.name); updatedContextField.name,
);
const value = await contextSchema.validateAsync(updatedContextField); const value = await contextSchema.validateAsync(updatedContextField);
// update // update
@ -139,20 +140,20 @@ class ContextService {
await this.eventService.storeEvent({ await this.eventService.storeEvent({
type: CONTEXT_FIELD_UPDATED, type: CONTEXT_FIELD_UPDATED,
createdBy: userName, createdBy: userName,
preData: contextField,
data: value, data: value,
}); });
} }
async deleteContextField(name: string, userName: string): Promise<void> { async deleteContextField(name: string, userName: string): Promise<void> {
// validate existence const contextField = await this.contextFieldStore.get(name);
await this.contextFieldStore.get(name);
// delete // delete
await this.contextFieldStore.delete(name); await this.contextFieldStore.delete(name);
await this.eventService.storeEvent({ await this.eventService.storeEvent({
type: CONTEXT_FIELD_DELETED, type: CONTEXT_FIELD_DELETED,
createdBy: userName, createdBy: userName,
data: { name }, preData: contextField,
}); });
} }

View File

@ -122,7 +122,7 @@ class FeatureTagService {
createdBy: userName, createdBy: userName,
featureName: featureToggle.name, featureName: featureToggle.name,
project: featureToggle.project, project: featureToggle.project,
data: removedTag, preData: removedTag,
})), })),
); );
@ -171,7 +171,7 @@ class FeatureTagService {
createdBy: userName, createdBy: userName,
featureName, featureName,
project: featureToggle.project, project: featureToggle.project,
data: tag, preData: tag,
tags, tags,
}); });
} }

View File

@ -176,7 +176,7 @@ export class GroupService {
await this.eventService.storeEvent({ await this.eventService.storeEvent({
type: GROUP_DELETED, type: GROUP_DELETED,
createdBy: userName, createdBy: userName,
data: group, preData: group,
}); });
} }

View File

@ -161,7 +161,7 @@ export class SegmentService implements ISegmentService {
await this.eventService.storeEvent({ await this.eventService.storeEvent({
type: SEGMENT_DELETED, type: SEGMENT_DELETED,
createdBy: user.email || user.username, createdBy: user.email || user.username,
data: segment, preData: segment,
}); });
} }
@ -171,7 +171,7 @@ export class SegmentService implements ISegmentService {
await this.eventService.storeEvent({ await this.eventService.storeEvent({
type: SEGMENT_DELETED, type: SEGMENT_DELETED,
createdBy: user.email || user.username, createdBy: user.email || user.username,
data: segment, preData: segment,
}); });
} }

View File

@ -293,7 +293,7 @@ test('should store segment-created and segment-deleted events', async () => {
const events = await db.stores.eventStore.getEvents(); const events = await db.stores.eventStore.getEvents();
expect(events[0].type).toEqual('segment-deleted'); 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].type).toEqual('segment-created');
expect(events[1].data.id).toEqual(segment1.id); expect(events[1].data.id).toEqual(segment1.id);
}); });

View File

@ -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: You can choose to trigger updates for the following events:
- feature-created - addon-config-created
- feature-metadata-updated - addon-config-deleted
- feature-project-change - 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-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-revived
- feature-strategy-update - feature-stale-off
- feature-stale-on
- feature-strategy-add - feature-strategy-add
- feature-strategy-remove - feature-strategy-remove
- feature-stale-on - feature-strategy-update
- feature-stale-off - feature-tagged
- feature-environment-enabled - feature-untagged
- feature-environment-disabled - group-created
- feature-environment-variants-updated - group-deleted
- feature-potentially-stale-on - 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} #### Parameters {#parameters}