diff --git a/.changeset/curvy-socks-punch.md b/.changeset/curvy-socks-punch.md new file mode 100644 index 0000000000..a353824c13 --- /dev/null +++ b/.changeset/curvy-socks-punch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend-module-slack': minor +--- + +**BREAKING**: Only send direct messages to user entity recipients. Notifications sent to non-user entities no longer send Slack direct messages to resolved users. diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts index d9aef27e46..ce7898566b 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts @@ -355,6 +355,51 @@ describe('SlackNotificationProcessor', () => { }); }); + describe('when recipients include both users and a group', () => { + it('should still DM explicit user recipients', async () => { + const slack = new WebClient(); + + const processor = SlackNotificationProcessor.fromConfig(config, { + auth, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + await processor.processOptions({ + recipients: { + type: 'entity', + entityRef: ['group:default/mock', 'user:default/mock'], + }, + payload: { title: 'notification' }, + }); + + await processor.postProcess( + { + origin: 'plugin', + id: 'explicit-user-1', + user: 'user:default/mock', + created: new Date(), + payload: { title: 'notification' }, + }, + { + recipients: { + type: 'entity', + entityRef: ['group:default/mock', 'user:default/mock'], + }, + payload: { title: 'notification' }, + }, + ); + + expect(slack.chat.postMessage).toHaveBeenCalledTimes(2); + expect(slack.chat.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ channel: 'U12345678' }), + ); + }); + }); + describe('when broadcast channels are not configured', () => { it('should not send broadcast messages', async () => { const slack = new WebClient(); diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts index 3bc833bcab..de084df464 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts @@ -19,6 +19,7 @@ import { Entity, isUserEntity, parseEntityRef, + stringifyEntityRef, UserEntity, } from '@backstage/catalog-model'; import { Config, readDurationFromConfig } from '@backstage/config'; @@ -280,8 +281,15 @@ export class SlackNotificationProcessor implements NotificationProcessor { } else if (options.recipients.type === 'entity') { // Handle user-specific notification const entityRefs = [options.recipients.entityRef].flat(); - if (entityRefs.some(e => parseEntityRef(e).kind === 'group')) { - // We've already dispatched a slack channel message, so let's not send a DM. + const explicitUserEntityRefs = entityRefs + .filter(entityRef => parseEntityRef(entityRef).kind === 'user') + .map(entityRef => stringifyEntityRef(parseEntityRef(entityRef))); + const normalizedUserRef = stringifyEntityRef( + parseEntityRef(notification.user), + ); + + if (!explicitUserEntityRefs.includes(normalizedUserRef)) { + // This user was resolved from a non-user entity. Skip sending a DM. return; }