Merge pull request #32733 from kaidubauskas-dd/kaidd/slack-block-kit-extension

feat(notifications-slack): Create slack block kit extension
This commit is contained in:
Patrik Oldsberg
2026-02-21 00:10:24 +01:00
committed by GitHub
9 changed files with 160 additions and 3 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-notifications-backend-module-slack': minor
---
Add an extension for custom Slack message layouts
+28
View File
@@ -156,6 +156,34 @@ 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.
### Customize Slack Message Structure
You can customize how notifications look in Slack by providing your own message layout through the `notificationsSlackBlockKitExtensionPoint`
```ts
import { createBackendModule } from '@backstage/backend-plugin-api';
import { notificationsSlackBlockKitExtensionPoint } from '@backstage/plugin-notifications-backend-module-slack';
export const notificationsSlackFormattingModule = createBackendModule({
pluginId: 'notifications',
moduleId: 'slack-formatting',
register(reg) {
reg.registerInit({
deps: {
slackBlockKit: notificationsSlackBlockKitExtensionPoint,
},
async init({ slackBlockKit }) {
slackBlockKit.setBlockKitRenderer(payload => [
// Custom block kit layout
]);
},
});
},
});
```
If you do not register a custom renderer, the default renderer is used.
### 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.
@@ -4,6 +4,9 @@
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
import { ExtensionPoint } from '@backstage/backend-plugin-api';
import { KnownBlock } from '@slack/web-api';
import { NotificationPayload } from '@backstage/plugin-notifications-common';
// @public
export const ANNOTATION_SLACK_BOT_NOTIFY = 'slack.com/bot-notify';
@@ -11,4 +14,18 @@ export const ANNOTATION_SLACK_BOT_NOTIFY = 'slack.com/bot-notify';
// @public
const notificationsModuleSlack: BackendFeature;
export default notificationsModuleSlack;
// @public
export interface NotificationsSlackBlockKitExtensionPoint {
// (undocumented)
setBlockKitRenderer(renderer: SlackBlockKitRenderer): void;
}
// @public (undocumented)
export const notificationsSlackBlockKitExtensionPoint: ExtensionPoint<NotificationsSlackBlockKitExtensionPoint>;
// @public (undocumented)
export type SlackBlockKitRenderer = (
payload: NotificationPayload,
) => KnownBlock[];
```
@@ -0,0 +1,43 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createExtensionPoint } from '@backstage/backend-plugin-api';
import { NotificationPayload } from '@backstage/plugin-notifications-common';
import { KnownBlock } from '@slack/web-api';
/**
* @public
*/
export type SlackBlockKitRenderer = (
payload: NotificationPayload,
) => KnownBlock[];
/**
* @public
*
* Extension point for customizing how notification payloads are rendered into
* Slack Block Kit messages before they're sent.
*/
export interface NotificationsSlackBlockKitExtensionPoint {
setBlockKitRenderer(renderer: SlackBlockKitRenderer): void;
}
/**
* @public
*/
export const notificationsSlackBlockKitExtensionPoint =
createExtensionPoint<NotificationsSlackBlockKitExtensionPoint>({
id: 'notifications.slack.blockkit',
});
@@ -22,3 +22,4 @@
export { ANNOTATION_SLACK_BOT_NOTIFY } from './lib';
export { notificationsModuleSlack as default } from './module';
export * from './extensions';
@@ -17,7 +17,7 @@
import { mockServices } from '@backstage/backend-test-utils';
import { SlackNotificationProcessor } from './SlackNotificationProcessor';
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
import { WebClient } from '@slack/web-api';
import { KnownBlock, WebClient } from '@slack/web-api';
import { Entity } from '@backstage/catalog-model';
import pThrottle from 'p-throttle';
import { durationToMilliseconds } from '@backstage/types';
@@ -209,6 +209,43 @@ describe('SlackNotificationProcessor', () => {
});
});
it('should use a custom block kit renderer when provided', async () => {
const slack = new WebClient();
const customBlocks: KnownBlock[] = [
{
type: 'section',
text: { type: 'mrkdwn', text: 'Custom block' },
},
];
const processor = SlackNotificationProcessor.fromConfig(config, {
auth,
logger,
catalog: catalogServiceMock({
entities: DEFAULT_ENTITIES_RESPONSE.items,
}),
slack,
blockKitRenderer: () => customBlocks,
})[0];
await processor.processOptions({
recipients: { type: 'entity', entityRef: 'group:default/mock' },
payload: { title: 'notification' },
});
expect(slack.chat.postMessage).toHaveBeenCalledWith({
channel: 'C12345678',
text: 'notification',
attachments: [
{
color: '#00A699',
blocks: customBlocks,
fallback: 'notification',
},
],
});
});
describe('when a user notification is sent directly', () => {
it('should send a notification to a user', async () => {
const slack = new WebClient();
@@ -37,6 +37,7 @@ import { ANNOTATION_SLACK_BOT_NOTIFY } from './constants';
import { BroadcastRoute } from './types';
import { ExpiryMap, toChatPostMessageArgs } from './util';
import { CatalogService } from '@backstage/plugin-catalog-node';
import { SlackBlockKitRenderer } from '../extensions';
export class SlackNotificationProcessor implements NotificationProcessor {
private readonly logger: LoggerService;
@@ -54,6 +55,7 @@ export class SlackNotificationProcessor implements NotificationProcessor {
private readonly username?: string;
private readonly concurrencyLimit: number;
private readonly throttleInterval: number;
private readonly blockKitRenderer?: SlackBlockKitRenderer;
static fromConfig(
config: Config,
@@ -63,6 +65,7 @@ export class SlackNotificationProcessor implements NotificationProcessor {
catalog: CatalogService;
slack?: WebClient;
broadcastChannels?: string[];
blockKitRenderer?: SlackBlockKitRenderer;
},
): SlackNotificationProcessor[] {
const slackConfig =
@@ -104,6 +107,7 @@ export class SlackNotificationProcessor implements NotificationProcessor {
username?: string;
concurrencyLimit?: number;
throttleInterval?: number;
blockKitRenderer?: SlackBlockKitRenderer;
}) {
const {
auth,
@@ -115,6 +119,7 @@ export class SlackNotificationProcessor implements NotificationProcessor {
username,
concurrencyLimit,
throttleInterval,
blockKitRenderer,
} = options;
this.logger = logger;
this.catalog = catalog;
@@ -126,6 +131,7 @@ export class SlackNotificationProcessor implements NotificationProcessor {
this.concurrencyLimit = concurrencyLimit ?? 10;
this.throttleInterval =
throttleInterval ?? durationToMilliseconds({ minutes: 1 });
this.blockKitRenderer = blockKitRenderer;
this.entityLoader = new DataLoader<string, Entity | undefined>(
async entityRefs => {
@@ -246,6 +252,7 @@ export class SlackNotificationProcessor implements NotificationProcessor {
channel,
payload: options.payload,
username: this.username,
blockKitRenderer: this.blockKitRenderer,
});
this.logger.debug(
@@ -306,6 +313,7 @@ export class SlackNotificationProcessor implements NotificationProcessor {
channel,
payload: formattedPayload,
username: this.username,
blockKitRenderer: this.blockKitRenderer,
}),
);
@@ -19,13 +19,16 @@ import {
NotificationSeverity,
} from '@backstage/plugin-notifications-common';
import { ChatPostMessageArguments, KnownBlock } from '@slack/web-api';
import { SlackBlockKitRenderer } from '../extensions';
export function toChatPostMessageArgs(options: {
channel: string;
payload: NotificationPayload;
username?: string;
blockKitRenderer?: SlackBlockKitRenderer;
}): ChatPostMessageArguments {
const { channel, payload, username } = options;
const { channel, payload, username, blockKitRenderer } = options;
const blocks = (blockKitRenderer ?? toSlackBlockKit)(payload);
const args: ChatPostMessageArguments = {
channel,
@@ -34,7 +37,7 @@ export function toChatPostMessageArgs(options: {
attachments: [
{
color: getColor(payload.severity),
blocks: toSlackBlockKit(payload),
blocks,
fallback: payload.title,
},
],
@@ -20,6 +20,10 @@ import {
import { notificationsProcessingExtensionPoint } from '@backstage/plugin-notifications-node';
import { SlackNotificationProcessor } from './lib/SlackNotificationProcessor';
import { catalogServiceRef } from '@backstage/plugin-catalog-node';
import {
notificationsSlackBlockKitExtensionPoint,
SlackBlockKitRenderer,
} from './extensions';
/**
* The Slack notification processor for use with the notifications plugin.
@@ -31,6 +35,16 @@ export const notificationsModuleSlack = createBackendModule({
pluginId: 'notifications',
moduleId: 'slack',
register(reg) {
let blockKitRenderer: SlackBlockKitRenderer | undefined;
reg.registerExtensionPoint(notificationsSlackBlockKitExtensionPoint, {
setBlockKitRenderer(renderer) {
if (blockKitRenderer) {
throw new Error(`Slack block kit renderer was already registered`);
}
blockKitRenderer = renderer;
},
});
reg.registerInit({
deps: {
auth: coreServices.auth,
@@ -45,6 +59,7 @@ export const notificationsModuleSlack = createBackendModule({
auth,
logger,
catalog,
blockKitRenderer,
}),
);
},