diff --git a/.changeset/large-months-decide.md b/.changeset/large-months-decide.md new file mode 100644 index 0000000000..05d5086794 --- /dev/null +++ b/.changeset/large-months-decide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-node': major +--- + +add notifications filtering by processors diff --git a/.changeset/polite-otters-talk.md b/.changeset/polite-otters-talk.md new file mode 100644 index 0000000000..e56b935c1e --- /dev/null +++ b/.changeset/polite-otters-talk.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend-module-email': minor +--- + +add notification filters diff --git a/.changeset/wild-ears-walk.md b/.changeset/wild-ears-walk.md new file mode 100644 index 0000000000..e6762b82ee --- /dev/null +++ b/.changeset/wild-ears-walk.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend': major +--- + +adding filtering of notifications by processors diff --git a/plugins/notifications-backend-module-email/config.d.ts b/plugins/notifications-backend-module-email/config.d.ts index db49d0016a..92f3385d1b 100644 --- a/plugins/notifications-backend-module-email/config.d.ts +++ b/plugins/notifications-backend-module-email/config.d.ts @@ -15,6 +15,7 @@ */ import { HumanDuration } from '@backstage/types'; +import { NotificationSeverity } from '@backstage/plugin-notifications-common'; export interface Config { /** @@ -117,6 +118,20 @@ export interface Config { */ ttl?: HumanDuration; }; + filter?: { + /** + * Minimum severity. A notification with lower severity will not be emailed + */ + minSeverity?: NotificationSeverity; + /** + * Maximum severity. A notification with higher severity will not be emailed + */ + maxSeverity?: NotificationSeverity; + /** + * A notification who's topic is in this array will not be emailed + */ + excludedTopics?: string[]; + }; }; }; }; diff --git a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts index c06d28f903..a78ebd3260 100644 --- a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts +++ b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts @@ -15,6 +15,7 @@ */ import { NotificationProcessor, + NotificationProcessorFilters, NotificationSendOptions, } from '@backstage/plugin-notifications-node'; import { @@ -28,7 +29,11 @@ import { CATALOG_FILTER_EXISTS, CatalogClient, } from '@backstage/catalog-client'; -import { Notification } from '@backstage/plugin-notifications-common'; +import { + Notification, + notificationSeverities, + NotificationSeverity, +} from '@backstage/plugin-notifications-common'; import { createSendmailTransport, createSesTransport, @@ -51,6 +56,7 @@ export class NotificationsEmailProcessor implements NotificationProcessor { private readonly concurrencyLimit: number; private readonly throttleInterval: number; private readonly frontendBaseUrl: string; + private readonly filter: NotificationProcessorFilters; constructor( private readonly logger: LoggerService, @@ -80,6 +86,30 @@ export class NotificationsEmailProcessor implements NotificationProcessor { ? durationToMilliseconds(readDurationFromConfig(cacheConfig)) : 3_600_000; this.frontendBaseUrl = config.getString('app.baseUrl'); + this.filter = {}; + const minSeverity = emailProcessorConfig.getOptionalString( + 'filter.minSeverity', + ) as NotificationSeverity; + if (minSeverity) { + if (notificationSeverities.includes(minSeverity)) { + this.filter.minSeverity = minSeverity; + } else { + throw new Error(`Invalid minSeverity: ${minSeverity}`); + } + } + const maxSeverity = emailProcessorConfig.getOptionalString( + 'filter.maxSeverity', + ) as NotificationSeverity; + if (maxSeverity) { + if (notificationSeverities.includes(maxSeverity)) { + this.filter.maxSeverity = maxSeverity; + } else { + throw new Error(`Invalid maxSeverity: ${maxSeverity}`); + } + } + this.filter.excludedTopics = emailProcessorConfig.getOptionalStringArray( + 'filter.excludedTopics', + ); } private async getTransporter() { @@ -312,4 +342,8 @@ export class NotificationsEmailProcessor implements NotificationProcessor { await this.sendTemplateEmail(notification, emails); } + + getNotificationFilters(): NotificationProcessorFilters { + return this.filter; + } } diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index 585b1ad104..bb5c9e06ac 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -48,7 +48,9 @@ import { SignalsService } from '@backstage/plugin-signals-node'; import { NewNotificationSignal, Notification, + NotificationPayload, NotificationReadSignal, + notificationSeverities, NotificationStatus, } from '@backstage/plugin-notifications-common'; import { parseEntityOrderFieldParams } from './parseEntityOrderFieldParams'; @@ -177,9 +179,46 @@ export async function createRouter( return users; }; - const processOptions = async (opts: NotificationSendOptions) => { - let ret = opts; + const filterProcessors = (payload: NotificationPayload) => { + const result: NotificationProcessor[] = []; + for (const processor of processors) { + if (processor.getNotificationFilters) { + const filters = processor.getNotificationFilters(); + if (filters.minSeverity) { + if ( + notificationSeverities.indexOf(payload.severity ?? 'normal') > + notificationSeverities.indexOf(filters.minSeverity) + ) { + continue; + } + } + + if (filters.maxSeverity) { + if ( + notificationSeverities.indexOf(payload.severity ?? 'normal') < + notificationSeverities.indexOf(filters.maxSeverity) + ) { + continue; + } + } + + if (filters.excludedTopics && payload.topic) { + if (filters.excludedTopics.includes(payload.topic)) { + continue; + } + } + } + result.push(processor); + } + + return result; + }; + + const processOptions = async (opts: NotificationSendOptions) => { + const filtered = filterProcessors(opts.payload); + let ret = opts; + for (const processor of filtered) { try { ret = processor.processOptions ? await processor.processOptions(ret) @@ -197,8 +236,9 @@ export async function createRouter( notification: Notification, opts: NotificationSendOptions, ) => { + const filtered = filterProcessors(notification.payload); let ret = notification; - for (const processor of processors) { + for (const processor of filtered) { try { ret = processor.preProcess ? await processor.preProcess(ret, opts) @@ -216,7 +256,8 @@ export async function createRouter( notification: Notification, opts: NotificationSendOptions, ) => { - for (const processor of processors) { + const filtered = filterProcessors(notification.payload); + for (const processor of filtered) { if (processor.postProcess) { try { await processor.postProcess(notification, opts); diff --git a/plugins/notifications-node/api-report.md b/plugins/notifications-node/api-report.md index 1914b562d1..27e6422f8a 100644 --- a/plugins/notifications-node/api-report.md +++ b/plugins/notifications-node/api-report.md @@ -8,6 +8,7 @@ import { DiscoveryService } from '@backstage/backend-plugin-api'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { Notification as Notification_2 } from '@backstage/plugin-notifications-common'; import { NotificationPayload } from '@backstage/plugin-notifications-common'; +import { NotificationSeverity } from '@backstage/plugin-notifications-common'; import { ServiceRef } from '@backstage/backend-plugin-api'; // @public (undocumented) @@ -23,6 +24,7 @@ export class DefaultNotificationService implements NotificationService { // @public export interface NotificationProcessor { getName(): string; + getNotificationFilters?(): NotificationProcessorFilters; postProcess?( notification: Notification_2, options: NotificationSendOptions, @@ -36,6 +38,13 @@ export interface NotificationProcessor { ): Promise; } +// @public (undocumented) +export type NotificationProcessorFilters = { + minSeverity?: NotificationSeverity; + maxSeverity?: NotificationSeverity; + excludedTopics?: string[]; +}; + // @public (undocumented) export type NotificationRecipients = | { diff --git a/plugins/notifications-node/src/extensions.ts b/plugins/notifications-node/src/extensions.ts index 6461c3dc89..7adb6caf50 100644 --- a/plugins/notifications-node/src/extensions.ts +++ b/plugins/notifications-node/src/extensions.ts @@ -14,7 +14,10 @@ * limitations under the License. */ import { createExtensionPoint } from '@backstage/backend-plugin-api'; -import { Notification } from '@backstage/plugin-notifications-common'; +import { + Notification, + NotificationSeverity, +} from '@backstage/plugin-notifications-common'; import { NotificationSendOptions } from './service'; /** @@ -90,6 +93,11 @@ export interface NotificationProcessor { notification: Notification, options: NotificationSendOptions, ): Promise; + + /** + * notification filters are used to call the processor only in certain conditions + */ + getNotificationFilters?(): NotificationProcessorFilters; } /** @@ -108,3 +116,12 @@ export const notificationsProcessingExtensionPoint = createExtensionPoint({ id: 'notifications.processing', }); + +/** + * @public + */ +export type NotificationProcessorFilters = { + minSeverity?: NotificationSeverity; + maxSeverity?: NotificationSeverity; + excludedTopics?: string[]; +};