diff --git a/server/src/domain/notifications/providers/rocketChat.ts b/server/src/domain/notifications/providers/rocketChat.ts index fb2d05003..821515b25 100644 --- a/server/src/domain/notifications/providers/rocketChat.ts +++ b/server/src/domain/notifications/providers/rocketChat.ts @@ -64,7 +64,7 @@ export class RocketChatProvider extends NotificationProvider { try { await got.post(notification.address, { - json: { text: this.buildText(message) }, + json: this.buildPayload(message), headers: { "Content-Type": "application/json", }, @@ -83,34 +83,38 @@ export class RocketChatProvider extends NotificationProvider { } } - private buildText(message: NotificationMessage): string { - const lines = [ - message.content.title, - message.content.summary, - "", - `Monitor: ${message.monitor.name}`, - `Type: ${message.monitor.type}`, - `Status: ${message.monitor.status}`, - `URL: ${message.monitor.url}`, - ]; + private buildPayload(message: NotificationMessage): RocketChatPayload { + return { + text: message.content.summary, + attachments: [ + { + color: this.severityColor(message.severity), + title: message.content.title, + title_link: `${message.clientHost}/infrastructure/${message.monitor.id}`, + fields: [ + { title: "Monitor", value: message.monitor.name, short: true }, + { title: "Type", value: message.monitor.type, short: true }, + { title: "Status", value: message.monitor.status, short: true }, + { title: "URL", value: message.monitor.url, short: false }, + ], + ts: message.content.timestamp.toISOString(), + }, + ], + }; + } - if (message.content.details?.length) { - lines.push("", "Details:", ...message.content.details.map((detail) => `- ${detail}`)); + private severityColor(severity: NotificationMessage["severity"]): string { + switch (severity) { + case "critical": + return "#FF0000"; + case "warning": + return "#FFA500"; + case "success": + return "#00FF00"; + case "info": + return "#0000FF"; + default: + return "#808080"; } - if (message.content.thresholds?.length) { - lines.push( - "", - "Thresholds:", - ...message.content.thresholds.map( - (threshold) => `- ${threshold.metric.toUpperCase()}: ${threshold.formattedValue} (threshold: ${threshold.threshold}${threshold.unit})` - ) - ); - } - - if (message.content.incident) { - lines.push("", `Incident: ${message.clientHost}/infrastructure/${message.monitor.id}`); - } - - return lines.join("\n"); } } diff --git a/server/test/unit/providers/notifications/rocketChatProvider.test.ts b/server/test/unit/providers/notifications/rocketChatProvider.test.ts index 077892353..401cac58b 100644 --- a/server/test/unit/providers/notifications/rocketChatProvider.test.ts +++ b/server/test/unit/providers/notifications/rocketChatProvider.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, jest } from "@jest/globals"; +import type { NotificationSeverity } from "../../../../src/domain/notifications/notification.type.ts"; import { createMockLogger } from "../../../helpers/createMockLogger.ts"; -import { makeMessage, makeMessageWithIncident, makeMessageWithThresholds, makeNotification } from "../../../helpers/notificationMessage.ts"; +import { makeMessage, makeNotification } from "../../../helpers/notificationMessage.ts"; import { testNotificationProviderContract } from "../../../helpers/notificationProviderContract.ts"; const mockGotPost = jest.fn().mockResolvedValue({}); @@ -84,7 +85,7 @@ describe("RocketChatProvider", () => { ); }); - it("sends monitor details as a text-only message", async () => { + it("sends a rich monitor attachment without webhook overrides", async () => { const { provider } = createProvider(); const result = await provider.sendMessage(makeNotification({ type: "rocket_chat" }), makeMessage()); @@ -96,22 +97,45 @@ describe("RocketChatProvider", () => { timeout: { request: 30000 }, retry: { limit: 0 }, json: { - text: [ - "Monitor Down: Test Monitor", - 'Monitor "Test Monitor" is currently down.', - "", - "Monitor: Test Monitor", - "Type: http", - "Status: down", - "URL: https://example.com", - "", - "Details:", - "- URL: https://example.com", - "- Status: Down", - ].join("\n"), + text: 'Monitor "Test Monitor" is currently down.', + attachments: [ + { + color: "#FF0000", + title: "Monitor Down: Test Monitor", + title_link: "https://app.example.com/infrastructure/mon-1", + fields: [ + { title: "Monitor", value: "Test Monitor", short: true }, + { title: "Type", value: "http", short: true }, + { title: "Status", value: "down", short: true }, + { title: "URL", value: "https://example.com", short: false }, + ], + ts: "2025-01-01T00:00:00.000Z", + }, + ], }, }) ); + + const payload = mockGotPost.mock.calls[0][1].json; + expect(payload).not.toHaveProperty("alias"); + expect(payload).not.toHaveProperty("avatar"); + expect(payload).not.toHaveProperty("emoji"); + expect(payload).not.toHaveProperty("channel"); + }); + + it.each([ + ["critical", "#FF0000"], + ["warning", "#FFA500"], + ["success", "#00FF00"], + ["info", "#0000FF"], + ["unknown", "#808080"], + ])("maps %s severity to %s", async (severity, color) => { + const { provider } = createProvider(); + + await provider.sendMessage(makeNotification({ type: "rocket_chat" }), makeMessage({ severity: severity as NotificationSeverity })); + + const payload = mockGotPost.mock.calls[0][1].json; + expect(payload.attachments[0].color).toBe(color); }); it("returns false when the message webhook URL is missing", async () => { @@ -123,40 +147,6 @@ describe("RocketChatProvider", () => { expect(mockGotPost).not.toHaveBeenCalled(); }); - it("includes threshold breaches in the text", async () => { - const { provider } = createProvider(); - - await provider.sendMessage(makeNotification({ type: "rocket_chat" }), makeMessageWithThresholds()); - - const payload = mockGotPost.mock.calls[0][1].json; - expect(payload.text).toContain("Thresholds:\n- CPU: 90.0% (threshold: 80%)\n- MEMORY: 85.0% (threshold: 70%)"); - }); - - it("includes a monitor-scoped link when incident data is present", async () => { - const { provider } = createProvider(); - - await provider.sendMessage(makeNotification({ type: "rocket_chat" }), makeMessageWithIncident()); - - const payload = mockGotPost.mock.calls[0][1].json; - expect(payload.text).toContain("Incident: https://app.example.com/infrastructure/mon-1"); - }); - - it("omits the incident link when incident data is absent", async () => { - const { provider } = createProvider(); - - await provider.sendMessage( - makeNotification({ type: "rocket_chat" }), - makeMessage({ - type: "monitor_up", - severity: "success", - monitor: { id: "mon-1", name: "Test Monitor", url: "https://example.com", type: "http", status: "up" }, - }) - ); - - const payload = mockGotPost.mock.calls[0][1].json; - expect(payload.text).not.toContain("Incident:"); - }); - it("returns false and logs when message delivery fails", async () => { mockGotPost.mockRejectedValue(new Error("network failure")); const { provider, logger } = createProvider();