Adds support for broadcasting notifications to specified Slack channels.
Signed-off-by: Henrik Edegård <henrik.edegard@fortnox.se>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-notifications-backend-module-slack': minor
|
||||
---
|
||||
|
||||
Enables optional routes to Slack channels for broadcast notifications based on origin and/or topics.
|
||||
@@ -154,6 +154,56 @@ notifications:
|
||||
Multiple instances can be added in the `slack` array, allowing you to have multiple configurations if you need to send
|
||||
messages to more than one Slack workspace. Org-Wide App installation is not currently supported.
|
||||
|
||||
### Broadcast Channel Routing
|
||||
|
||||
For more granular control over where broadcast notifications are sent, you can use `broadcastRoutes` to route notifications to different Slack channels based on their origin and/or topic. This is useful when you want different types of notifications to go to different channels.
|
||||
|
||||
```yaml
|
||||
notifications:
|
||||
processors:
|
||||
slack:
|
||||
- token: xoxb-XXXXXXXXX
|
||||
# Legacy option - used as fallback when no routes match
|
||||
broadcastChannels:
|
||||
- general-notifications
|
||||
# Route broadcasts based on origin and/or topic
|
||||
broadcastRoutes:
|
||||
# Most specific: matches both origin AND topic
|
||||
- origin: plugin:catalog
|
||||
topic: alerts
|
||||
channel: catalog-alerts
|
||||
# Origin only: all notifications from this origin
|
||||
- origin: plugin:catalog
|
||||
channel: catalog-updates
|
||||
# Topic only: all notifications with this topic (any origin)
|
||||
- topic: security
|
||||
channel: security-team
|
||||
# Multiple channels: send to several channels at once
|
||||
- origin: external:monitoring
|
||||
channel:
|
||||
- ops-team
|
||||
- on-call-alerts
|
||||
```
|
||||
|
||||
#### Route Matching Precedence
|
||||
|
||||
Routes are evaluated in the following order of priority:
|
||||
|
||||
1. **Origin + Topic match** (most specific) - A route that specifies both `origin` and `topic` will match first
|
||||
2. **Origin-only match** - A route with only `origin` specified (no `topic`)
|
||||
3. **Topic-only match** - A route with only `topic` specified (no `origin`)
|
||||
4. **Default fallback** - If no routes match, falls back to `broadcastChannels`
|
||||
|
||||
The first matching route wins. If no routes match and no `broadcastChannels` are configured, the broadcast notification will not be sent to Slack.
|
||||
|
||||
#### Configuration Options
|
||||
|
||||
| Property | Type | Description |
|
||||
| --------- | ---------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| `origin` | `string` | Optional. The notification origin to match (e.g., `plugin:catalog`, `external:my-service`) |
|
||||
| `topic` | `string` | Optional. The notification topic to match (e.g., `alerts`, `updates`, `security`) |
|
||||
| `channel` | `string` or `string[]` | Required. The Slack channel(s) to send to. Can be channel IDs, channel names, or user IDs |
|
||||
|
||||
### Entity Requirements
|
||||
|
||||
Entities must be annotated with the following annotation:
|
||||
|
||||
@@ -26,8 +26,32 @@ export interface Config {
|
||||
* Broadcast notification receivers when receiver is set to config
|
||||
* These can be Slack User IDs, Slack User Email addresses, Slack Channel
|
||||
* Names, or Slack Channel IDs. Any valid identifier that chat.postMessage can accept.
|
||||
* @deprecated Use broadcastRoutes instead for more granular control
|
||||
*/
|
||||
broadcastChannels?: string[];
|
||||
/**
|
||||
* Optional username to display as the sender of the notification
|
||||
*/
|
||||
username?: string;
|
||||
/**
|
||||
* Routes for broadcast notifications based on origin and/or topic.
|
||||
* Routes are evaluated in order, first match wins.
|
||||
* Origin+topic matches take precedence over origin-only matches.
|
||||
*/
|
||||
broadcastRoutes?: Array<{
|
||||
/**
|
||||
* The origin to match (e.g., 'plugin:catalog', 'external:my-service')
|
||||
*/
|
||||
origin?: string;
|
||||
/**
|
||||
* The topic to match (e.g., 'entity-updated', 'alerts')
|
||||
*/
|
||||
topic?: string;
|
||||
/**
|
||||
* The Slack channel(s) to send to. Can be channel IDs, channel names, or user IDs.
|
||||
*/
|
||||
channel: string | string[];
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
+456
@@ -393,6 +393,462 @@ describe('SlackNotificationProcessor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when broadcast routes are configured', () => {
|
||||
it('should route by origin and topic (highest priority)', async () => {
|
||||
const slack = new WebClient();
|
||||
const routesConfig = mockServices.rootConfig({
|
||||
data: {
|
||||
app: {
|
||||
baseUrl: 'https://example.org',
|
||||
},
|
||||
notifications: {
|
||||
processors: {
|
||||
slack: [
|
||||
{
|
||||
token: 'mock-token',
|
||||
broadcastChannels: ['default-channel'],
|
||||
broadcastRoutes: [
|
||||
{ origin: 'plugin:catalog', channel: 'catalog-channel' },
|
||||
{ topic: 'alerts', channel: 'alerts-channel' },
|
||||
{
|
||||
origin: 'plugin:catalog',
|
||||
topic: 'alerts',
|
||||
channel: 'catalog-alerts-channel',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const processor = SlackNotificationProcessor.fromConfig(routesConfig, {
|
||||
auth,
|
||||
logger,
|
||||
catalog: catalogServiceMock({
|
||||
entities: DEFAULT_ENTITIES_RESPONSE.items,
|
||||
}),
|
||||
slack,
|
||||
})[0];
|
||||
|
||||
await processor.postProcess(
|
||||
{
|
||||
origin: 'plugin:catalog',
|
||||
id: '1234',
|
||||
user: null,
|
||||
created: new Date(),
|
||||
payload: {
|
||||
title: 'notification',
|
||||
topic: 'alerts',
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: { type: 'broadcast' },
|
||||
payload: { title: 'notification', topic: 'alerts' },
|
||||
},
|
||||
);
|
||||
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ channel: 'catalog-alerts-channel' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should route by origin only when no origin+topic match', async () => {
|
||||
const slack = new WebClient();
|
||||
const routesConfig = mockServices.rootConfig({
|
||||
data: {
|
||||
app: {
|
||||
baseUrl: 'https://example.org',
|
||||
},
|
||||
notifications: {
|
||||
processors: {
|
||||
slack: [
|
||||
{
|
||||
token: 'mock-token',
|
||||
broadcastRoutes: [
|
||||
{ origin: 'plugin:catalog', channel: 'catalog-channel' },
|
||||
{ topic: 'alerts', channel: 'alerts-channel' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const processor = SlackNotificationProcessor.fromConfig(routesConfig, {
|
||||
auth,
|
||||
logger,
|
||||
catalog: catalogServiceMock({
|
||||
entities: DEFAULT_ENTITIES_RESPONSE.items,
|
||||
}),
|
||||
slack,
|
||||
})[0];
|
||||
|
||||
await processor.postProcess(
|
||||
{
|
||||
origin: 'plugin:catalog',
|
||||
id: '1234',
|
||||
user: null,
|
||||
created: new Date(),
|
||||
payload: {
|
||||
title: 'notification',
|
||||
topic: 'updates',
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: { type: 'broadcast' },
|
||||
payload: { title: 'notification', topic: 'updates' },
|
||||
},
|
||||
);
|
||||
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ channel: 'catalog-channel' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should route by topic only when no origin match', async () => {
|
||||
const slack = new WebClient();
|
||||
const routesConfig = mockServices.rootConfig({
|
||||
data: {
|
||||
app: {
|
||||
baseUrl: 'https://example.org',
|
||||
},
|
||||
notifications: {
|
||||
processors: {
|
||||
slack: [
|
||||
{
|
||||
token: 'mock-token',
|
||||
broadcastRoutes: [
|
||||
{ origin: 'plugin:catalog', channel: 'catalog-channel' },
|
||||
{ topic: 'alerts', channel: 'alerts-channel' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const processor = SlackNotificationProcessor.fromConfig(routesConfig, {
|
||||
auth,
|
||||
logger,
|
||||
catalog: catalogServiceMock({
|
||||
entities: DEFAULT_ENTITIES_RESPONSE.items,
|
||||
}),
|
||||
slack,
|
||||
})[0];
|
||||
|
||||
await processor.postProcess(
|
||||
{
|
||||
origin: 'plugin:unknown',
|
||||
id: '1234',
|
||||
user: null,
|
||||
created: new Date(),
|
||||
payload: {
|
||||
title: 'notification',
|
||||
topic: 'alerts',
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: { type: 'broadcast' },
|
||||
payload: { title: 'notification', topic: 'alerts' },
|
||||
},
|
||||
);
|
||||
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ channel: 'alerts-channel' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should fall back to broadcastChannels when no route matches', async () => {
|
||||
const slack = new WebClient();
|
||||
const routesConfig = mockServices.rootConfig({
|
||||
data: {
|
||||
app: {
|
||||
baseUrl: 'https://example.org',
|
||||
},
|
||||
notifications: {
|
||||
processors: {
|
||||
slack: [
|
||||
{
|
||||
token: 'mock-token',
|
||||
broadcastChannels: ['default-channel'],
|
||||
broadcastRoutes: [
|
||||
{ origin: 'plugin:catalog', channel: 'catalog-channel' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const processor = SlackNotificationProcessor.fromConfig(routesConfig, {
|
||||
auth,
|
||||
logger,
|
||||
catalog: catalogServiceMock({
|
||||
entities: DEFAULT_ENTITIES_RESPONSE.items,
|
||||
}),
|
||||
slack,
|
||||
})[0];
|
||||
|
||||
await processor.postProcess(
|
||||
{
|
||||
origin: 'plugin:unknown',
|
||||
id: '1234',
|
||||
user: null,
|
||||
created: new Date(),
|
||||
payload: {
|
||||
title: 'notification',
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: { type: 'broadcast' },
|
||||
payload: { title: 'notification' },
|
||||
},
|
||||
);
|
||||
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ channel: 'default-channel' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should support multiple channels in a route', async () => {
|
||||
const slack = new WebClient();
|
||||
const routesConfig = mockServices.rootConfig({
|
||||
data: {
|
||||
app: {
|
||||
baseUrl: 'https://example.org',
|
||||
},
|
||||
notifications: {
|
||||
processors: {
|
||||
slack: [
|
||||
{
|
||||
token: 'mock-token',
|
||||
broadcastRoutes: [
|
||||
{
|
||||
origin: 'plugin:catalog',
|
||||
channel: ['channel1', 'channel2', 'channel3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const processor = SlackNotificationProcessor.fromConfig(routesConfig, {
|
||||
auth,
|
||||
logger,
|
||||
catalog: catalogServiceMock({
|
||||
entities: DEFAULT_ENTITIES_RESPONSE.items,
|
||||
}),
|
||||
slack,
|
||||
})[0];
|
||||
|
||||
await processor.postProcess(
|
||||
{
|
||||
origin: 'plugin:catalog',
|
||||
id: '1234',
|
||||
user: null,
|
||||
created: new Date(),
|
||||
payload: {
|
||||
title: 'notification',
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: { type: 'broadcast' },
|
||||
payload: { title: 'notification' },
|
||||
},
|
||||
);
|
||||
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledTimes(3);
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ channel: 'channel1' }),
|
||||
);
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ channel: 'channel2' }),
|
||||
);
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ channel: 'channel3' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should support single channel as string in a route', async () => {
|
||||
const slack = new WebClient();
|
||||
const routesConfig = mockServices.rootConfig({
|
||||
data: {
|
||||
app: {
|
||||
baseUrl: 'https://example.org',
|
||||
},
|
||||
notifications: {
|
||||
processors: {
|
||||
slack: [
|
||||
{
|
||||
token: 'mock-token',
|
||||
broadcastRoutes: [
|
||||
{ topic: 'alerts', channel: 'single-alerts-channel' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const processor = SlackNotificationProcessor.fromConfig(routesConfig, {
|
||||
auth,
|
||||
logger,
|
||||
catalog: catalogServiceMock({
|
||||
entities: DEFAULT_ENTITIES_RESPONSE.items,
|
||||
}),
|
||||
slack,
|
||||
})[0];
|
||||
|
||||
await processor.postProcess(
|
||||
{
|
||||
origin: 'plugin:any',
|
||||
id: '1234',
|
||||
user: null,
|
||||
created: new Date(),
|
||||
payload: {
|
||||
title: 'notification',
|
||||
topic: 'alerts',
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: { type: 'broadcast' },
|
||||
payload: { title: 'notification', topic: 'alerts' },
|
||||
},
|
||||
);
|
||||
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ channel: 'single-alerts-channel' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not send when no route matches and no broadcastChannels configured', async () => {
|
||||
const slack = new WebClient();
|
||||
const routesConfig = mockServices.rootConfig({
|
||||
data: {
|
||||
app: {
|
||||
baseUrl: 'https://example.org',
|
||||
},
|
||||
notifications: {
|
||||
processors: {
|
||||
slack: [
|
||||
{
|
||||
token: 'mock-token',
|
||||
broadcastRoutes: [
|
||||
{ origin: 'plugin:catalog', channel: 'catalog-channel' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const processor = SlackNotificationProcessor.fromConfig(routesConfig, {
|
||||
auth,
|
||||
logger,
|
||||
catalog: catalogServiceMock({
|
||||
entities: DEFAULT_ENTITIES_RESPONSE.items,
|
||||
}),
|
||||
slack,
|
||||
})[0];
|
||||
|
||||
await processor.postProcess(
|
||||
{
|
||||
origin: 'plugin:unknown',
|
||||
id: '1234',
|
||||
user: null,
|
||||
created: new Date(),
|
||||
payload: {
|
||||
title: 'notification',
|
||||
topic: 'unknown-topic',
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: { type: 'broadcast' },
|
||||
payload: { title: 'notification', topic: 'unknown-topic' },
|
||||
},
|
||||
);
|
||||
|
||||
expect(slack.chat.postMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should match origin+topic route over origin-only route', async () => {
|
||||
const slack = new WebClient();
|
||||
const routesConfig = mockServices.rootConfig({
|
||||
data: {
|
||||
app: {
|
||||
baseUrl: 'https://example.org',
|
||||
},
|
||||
notifications: {
|
||||
processors: {
|
||||
slack: [
|
||||
{
|
||||
token: 'mock-token',
|
||||
broadcastRoutes: [
|
||||
// Origin-only route defined first
|
||||
{ origin: 'plugin:catalog', channel: 'general-catalog' },
|
||||
// More specific origin+topic route defined second
|
||||
{
|
||||
origin: 'plugin:catalog',
|
||||
topic: 'entity-deleted',
|
||||
channel: 'catalog-deletions',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const processor = SlackNotificationProcessor.fromConfig(routesConfig, {
|
||||
auth,
|
||||
logger,
|
||||
catalog: catalogServiceMock({
|
||||
entities: DEFAULT_ENTITIES_RESPONSE.items,
|
||||
}),
|
||||
slack,
|
||||
})[0];
|
||||
|
||||
await processor.postProcess(
|
||||
{
|
||||
origin: 'plugin:catalog',
|
||||
id: '1234',
|
||||
user: null,
|
||||
created: new Date(),
|
||||
payload: {
|
||||
title: 'notification',
|
||||
topic: 'entity-deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: { type: 'broadcast' },
|
||||
payload: { title: 'notification', topic: 'entity-deleted' },
|
||||
},
|
||||
);
|
||||
|
||||
// Should use the origin+topic match, not the origin-only match
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ channel: 'catalog-deletions' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when slack.com/bot-notify annotation is missing', () => {
|
||||
it('should not send notification to a group without annotation', async () => {
|
||||
const slack = new WebClient();
|
||||
|
||||
@@ -34,6 +34,7 @@ import { ChatPostMessageArguments, WebClient } from '@slack/web-api';
|
||||
import DataLoader from 'dataloader';
|
||||
import pThrottle from 'p-throttle';
|
||||
import { ANNOTATION_SLACK_BOT_NOTIFY } from './constants';
|
||||
import { BroadcastRoute } from './types';
|
||||
import { ExpiryMap, toChatPostMessageArgs } from './util';
|
||||
import { CatalogService } from '@backstage/plugin-catalog-node';
|
||||
|
||||
@@ -48,6 +49,7 @@ export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
private readonly messagesSent: Counter;
|
||||
private readonly messagesFailed: Counter;
|
||||
private readonly broadcastChannels?: string[];
|
||||
private readonly broadcastRoutes?: BroadcastRoute[];
|
||||
private readonly entityLoader: DataLoader<string, Entity | undefined>;
|
||||
private readonly username?: string;
|
||||
|
||||
@@ -68,9 +70,14 @@ export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
const slack = options.slack ?? new WebClient(token);
|
||||
const broadcastChannels = c.getOptionalStringArray('broadcastChannels');
|
||||
const username = c.getOptionalString('username');
|
||||
const broadcastRoutesConfig = c.getOptionalConfigArray('broadcastRoutes');
|
||||
const broadcastRoutes = broadcastRoutesConfig?.map(route =>
|
||||
this.parseBroadcastRoute(route),
|
||||
);
|
||||
return new SlackNotificationProcessor({
|
||||
slack,
|
||||
broadcastChannels,
|
||||
broadcastRoutes,
|
||||
username,
|
||||
...options,
|
||||
});
|
||||
@@ -83,15 +90,24 @@ export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
logger: LoggerService;
|
||||
catalog: CatalogService;
|
||||
broadcastChannels?: string[];
|
||||
broadcastRoutes?: BroadcastRoute[];
|
||||
username?: string;
|
||||
}) {
|
||||
const { auth, catalog, logger, slack, broadcastChannels, username } =
|
||||
options;
|
||||
const {
|
||||
auth,
|
||||
catalog,
|
||||
logger,
|
||||
slack,
|
||||
broadcastChannels,
|
||||
broadcastRoutes,
|
||||
username,
|
||||
} = options;
|
||||
this.logger = logger;
|
||||
this.catalog = catalog;
|
||||
this.auth = auth;
|
||||
this.slack = slack;
|
||||
this.broadcastChannels = broadcastChannels;
|
||||
this.broadcastRoutes = broadcastRoutes;
|
||||
this.username = username;
|
||||
|
||||
this.entityLoader = new DataLoader<string, Entity | undefined>(
|
||||
@@ -235,7 +251,8 @@ export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
|
||||
// Handle broadcast case
|
||||
if (notification.user === null) {
|
||||
destinations.push(...(this.broadcastChannels ?? []));
|
||||
const routedChannels = this.getBroadcastDestinations(notification);
|
||||
destinations.push(...routedChannels);
|
||||
} else if (options.recipients.type === 'entity') {
|
||||
// Handle user-specific notification
|
||||
const entityRefs = [options.recipients.entityRef].flat();
|
||||
@@ -384,4 +401,88 @@ export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
throw new Error(`Failed to send notification: ${response.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private static parseBroadcastRoute(route: Config): BroadcastRoute {
|
||||
const channelValue = route.getOptional('channel');
|
||||
let channels: string[];
|
||||
|
||||
if (typeof channelValue === 'string') {
|
||||
channels = [channelValue];
|
||||
} else if (Array.isArray(channelValue)) {
|
||||
channels = channelValue as string[];
|
||||
} else {
|
||||
throw new Error(
|
||||
'broadcastRoutes entry must have a channel property (string or string[])',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
origin: route.getOptionalString('origin'),
|
||||
topic: route.getOptionalString('topic'),
|
||||
channels,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the destination channels for a broadcast notification based on
|
||||
* configured routes. Routes are matched by origin and/or topic.
|
||||
*
|
||||
* Matching precedence:
|
||||
* 1. Routes with both origin AND topic matching (most specific)
|
||||
* 2. Routes with only origin matching
|
||||
* 3. Routes with only topic matching
|
||||
* 4. Default broadcastChannels (least specific fallback)
|
||||
*
|
||||
* The first matching route wins within each precedence level.
|
||||
*/
|
||||
private getBroadcastDestinations(notification: Notification): string[] {
|
||||
const { origin } = notification;
|
||||
const { topic } = notification.payload;
|
||||
|
||||
if (!this.broadcastRoutes || this.broadcastRoutes.length === 0) {
|
||||
// Fall back to legacy broadcastChannels config
|
||||
return this.broadcastChannels ?? [];
|
||||
}
|
||||
|
||||
// Find most specific match
|
||||
// Priority 1: origin AND topic match
|
||||
const originAndTopicMatch = this.broadcastRoutes.find(
|
||||
route =>
|
||||
route.origin !== undefined &&
|
||||
route.topic !== undefined &&
|
||||
route.origin === origin &&
|
||||
route.topic === topic,
|
||||
);
|
||||
|
||||
if (originAndTopicMatch) {
|
||||
return originAndTopicMatch.channels;
|
||||
}
|
||||
|
||||
// Priority 2: origin-only match (no topic specified in route)
|
||||
const originOnlyMatch = this.broadcastRoutes.find(
|
||||
route =>
|
||||
route.origin !== undefined &&
|
||||
route.topic === undefined &&
|
||||
route.origin === origin,
|
||||
);
|
||||
|
||||
if (originOnlyMatch) {
|
||||
return originOnlyMatch.channels;
|
||||
}
|
||||
|
||||
// Priority 3: topic-only match (no origin specified in route)
|
||||
const topicOnlyMatch = this.broadcastRoutes.find(
|
||||
route =>
|
||||
route.topic !== undefined &&
|
||||
route.origin === undefined &&
|
||||
route.topic === topic,
|
||||
);
|
||||
|
||||
if (topicOnlyMatch) {
|
||||
return topicOnlyMatch.channels;
|
||||
}
|
||||
|
||||
// No match found, fall back to legacy broadcastChannels
|
||||
return this.broadcastChannels ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,3 +18,16 @@ export interface SlackNotificationOptions {
|
||||
url: string;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for routing broadcast notifications to specific Slack channels
|
||||
* based on origin and/or topic.
|
||||
*/
|
||||
export type BroadcastRoute = {
|
||||
/** The origin to match (e.g., 'plugin:catalog', 'external:my-service') */
|
||||
origin?: string;
|
||||
/** The topic to match (e.g., 'entity-updated', 'alerts') */
|
||||
topic?: string;
|
||||
/** The Slack channel(s) to send to */
|
||||
channels: string[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user