feat(notifications): add Rocket.Chat attachments

This commit is contained in:
Marcos Paulo Diniz committed 2026-08-07 14:55:05 -03:00
1 parent ee25c6b479
commit 2c780b8101
2 files changed
+71 -77

No files matched your search

@@ -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");
}
}
@@ -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();