feat:slack block extension

Signed-off-by: Kai Dubauskas <kai.dubauskas@doordash.com>
This commit is contained in:
Kai Dubauskas
2026-02-06 16:28:09 -05:00
parent 7eda810329
commit 3ff20e3c5a
6 changed files with 106 additions and 2 deletions
@@ -0,0 +1,40 @@
/*
* 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
*/
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';
@@ -209,6 +209,43 @@ describe('SlackNotificationProcessor', () => {
});
});
it('should use a custom block kit renderer when provided', async () => {
const slack = new WebClient();
const customBlocks = [
{
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,
}),
);
},