Skip to content

Commit

Permalink
feat: slack app addon default channels (#4308)
Browse files Browse the repository at this point in the history
https://linear.app/unleash/issue/2-1249/add-support-for-default-slack-channels

Adds support for default Slack channels (multiple, comma-separated). 

Some of the events we are handling do not have associated tags, or maybe
the tags are empty. This adds a "default Slack channels" parameter to
the addon configuration in order to post messages to those channels in
those cases.

<img width="643" alt="image"
src="https://github.com/Unleash/unleash/assets/14320932/ee23d6c7-33b7-4968-a0b1-13b546b5b2a2">

---------

Co-authored-by: Gastón Fournier <gaston@getunleash.io>
  • Loading branch information
nunogois and gastonfournier committed Jul 21, 2023
1 parent 34c4dd5 commit d6c8493
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 5 deletions.
42 changes: 42 additions & 0 deletions src/lib/addons/__snapshots__/slack-app.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,45 @@ exports[`SlackAppAddon should post to all channels in tags 2`] = `
"text": "some@user.com *enabled* <http://some-url.com/projects/default/features/some-toggle|some-toggle> in *development* environment in project *default*",
}
`;

exports[`SlackAppAddon should use defaultChannels if no tagged channels are found 1`] = `
{
"attachments": [
{
"actions": [
{
"name": "featureToggle",
"style": "primary",
"text": "Open in Unleash",
"type": "button",
"url": "http://some-url.com/projects/default/features/some-toggle",
"value": "featureToggle",
},
],
},
],
"channel": 1,
"text": "some@user.com *enabled* <http://some-url.com/projects/default/features/some-toggle|some-toggle> in *development* environment in project *default*",
}
`;

exports[`SlackAppAddon should use defaultChannels if no tagged channels are found 2`] = `
{
"attachments": [
{
"actions": [
{
"name": "featureToggle",
"style": "primary",
"text": "Open in Unleash",
"type": "button",
"url": "http://some-url.com/projects/default/features/some-toggle",
"value": "featureToggle",
},
],
},
],
"channel": 2,
"text": "some@user.com *enabled* <http://some-url.com/projects/default/features/some-toggle|some-toggle> in *development* environment in project *default*",
}
`;
9 changes: 9 additions & 0 deletions src/lib/addons/slack-app-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ const slackAppDefinition: IAddonDefinition = {
required: true,
sensitive: true,
},
{
name: 'defaultChannels',
displayName: 'Default channels',
description:
'A comma-separated list of channels to post to if no tagged channels are found (e.g. a toggle without tags, or an event with no tags associated).',
type: 'text',
required: false,
sensitive: false,
},
],
events: [
FEATURE_CREATED,
Expand Down
32 changes: 32 additions & 0 deletions src/lib/addons/slack-app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ describe('SlackAppAddon', () => {
jest.useFakeTimers();
slackApiCalls.length = 0;
conversationsList.mockClear();
postMessage.mockClear();
addon = new SlackAppAddon({
getLogger,
unleashUrl: 'http://some-url.com',
Expand Down Expand Up @@ -153,6 +154,37 @@ describe('SlackAppAddon', () => {
expect(conversationsList).toHaveBeenCalledTimes(2);
});

it('should not post a message if there are no tagged channels and no defaultChannels', async () => {
const eventWithoutTags: IEvent = {
...event,
tags: [],
};

await addon.handleEvent(eventWithoutTags, {
accessToken,
});

expect(slackApiCalls.length).toBe(0);
});

it('should use defaultChannels if no tagged channels are found', async () => {
const eventWithoutTags: IEvent = {
...event,
tags: [],
};

await addon.handleEvent(eventWithoutTags, {
accessToken,
defaultChannels: 'general, another-channel-1',
});

expect(slackApiCalls.length).toBe(2);
expect(slackApiCalls[0].channel).toBe(1);
expect(slackApiCalls[0]).toMatchSnapshot();
expect(slackApiCalls[1].channel).toBe(2);
expect(slackApiCalls[1]).toMatchSnapshot();
});

it('should log error when an API call fails', async () => {
postMessage = jest.fn().mockRejectedValue(mockError);

Expand Down
21 changes: 16 additions & 5 deletions src/lib/addons/slack-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const CACHE_SECONDS = 30;

interface ISlackAppAddonParameters {
accessToken: string;
defaultChannels: string;
}

export default class SlackAppAddon extends Addon {
Expand Down Expand Up @@ -52,17 +53,20 @@ export default class SlackAppAddon extends Addon {
parameters: ISlackAppAddonParameters,
): Promise<void> {
try {
const { accessToken } = parameters;
const { accessToken, defaultChannels } = parameters;
if (!accessToken) {
this.logger.warn('No access token provided.');
return;
}

const taggedChannels = this.findTaggedChannels(event);
if (!taggedChannels.length) {
const eventChannels = taggedChannels.length
? taggedChannels
: this.getDefaultChannels(defaultChannels);

if (!eventChannels.length) {
this.logger.debug(
`No Slack channels tagged for event ${event.type}`,
event,
`No Slack channels found for event ${event.type}.`,
);
return;
}
Expand Down Expand Up @@ -99,7 +103,7 @@ export default class SlackAppAddon extends Addon {
const url = this.msgFormatter.featureLink(event);

const slackChannelsToPostTo = currentSlackChannels.filter(
({ id, name }) => id && name && taggedChannels.includes(name),
({ id, name }) => id && name && eventChannels.includes(name),
);

const requests = slackChannelsToPostTo.map(({ id }) =>
Expand Down Expand Up @@ -151,6 +155,13 @@ export default class SlackAppAddon extends Addon {
return [];
}

getDefaultChannels(defaultChannels?: string): string[] {
if (defaultChannels) {
return defaultChannels.split(',').map((c) => c.trim());
}
return [];
}

startCacheInvalidation(): void {
this.slackChannelsCacheTimeout = setInterval(() => {
this.slackChannels = undefined;
Expand Down

0 comments on commit d6c8493

Please sign in to comment.