From 7fb57d8090fe80a7989af010b422b196fb1acad8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 23 Sep 2026 11:46:58 -0700 Subject: [PATCH] fix incident links with helper --- server/src/domain/monitors/monitor.type.ts | 16 ++++++++++++++++ .../src/domain/notifications/providers/matrix.ts | 8 ++++---- .../src/domain/notifications/providers/ntfy.ts | 4 ++-- .../domain/notifications/providers/pushover.ts | 4 ++-- .../domain/notifications/providers/rocketChat.ts | 16 ++-------------- .../domain/notifications/providers/signalgrid.ts | 4 ++-- .../src/domain/notifications/providers/slack.ts | 4 ++-- .../src/domain/notifications/providers/teams.ts | 4 ++-- .../domain/notifications/providers/telegram.ts | 4 ++-- .../src/domain/notifications/providers/utils.ts | 4 ++++ .../domain/notifications/providers/webhook.ts | 4 ++-- .../notifications/matrixProvider.test.ts | 8 ++++++++ .../providers/notifications/ntfyProvider.test.ts | 2 +- .../notifications/pushoverProvider.test.ts | 6 ++++++ .../notifications/rocketChatProvider.test.ts | 1 + .../notifications/signalgridProvider.test.ts | 6 ++++++ .../notifications/slackProvider.test.ts | 8 ++++++++ .../notifications/teamsProvider.test.ts | 2 +- .../notifications/telegramProvider.test.ts | 6 ++++++ .../notifications/webhookProvider.test.ts | 6 ++++++ 20 files changed, 83 insertions(+), 34 deletions(-) diff --git a/server/src/domain/monitors/monitor.type.ts b/server/src/domain/monitors/monitor.type.ts index 38c0338eb..b033c560a 100644 --- a/server/src/domain/monitors/monitor.type.ts +++ b/server/src/domain/monitors/monitor.type.ts @@ -51,6 +51,22 @@ export const UptimeDetailsSupportedTypes = ["http", "ping", "port", "game", "grp export type UptimeDetailsSupportedType = (typeof UptimeDetailsSupportedTypes)[number]; export const supportsUptimeDetails = (type: MonitorType): type is UptimeDetailsSupportedType => UptimeDetailsSupportedTypes.some((t) => t === type); +const MonitorPaths: Record = { + http: "uptime", + port: "uptime", + ping: "uptime", + game: "uptime", + grpc: "uptime", + websocket: "uptime", + dns: "uptime", + unknown: "uptime", + docker: "docker/host", + hardware: "infrastructure", + pagespeed: "pagespeed", +}; + +export const getMonitorPath = (type: MonitorType): string => MonitorPaths[type]; + // Types whose check leaves the instance, and whose failure to reach the target can therefore be the instance's // own loss of egress. A hardware check is an ordinary outbound HTTP request to the Capture agent, so it counts // however far away the agent is. `unknown` is excluded because no request is made for it. diff --git a/server/src/domain/notifications/providers/matrix.ts b/server/src/domain/notifications/providers/matrix.ts index 1fafc44b1..7e66b5c37 100644 --- a/server/src/domain/notifications/providers/matrix.ts +++ b/server/src/domain/notifications/providers/matrix.ts @@ -3,7 +3,7 @@ import got from "got"; import { NotificationProvider } from "@/domain/notifications/providers/INotificationProvider.js"; import type { AlertMatrixPayload, Notification } from "@/domain/notifications/notification.type.js"; import type { NotificationMessage } from "@/domain/notifications/notification.type.js"; -import { getTestMessage } from "@/domain/notifications/providers/utils.js"; +import { getTestMessage, incidentUrl } from "@/domain/notifications/providers/utils.js"; import { randomUUID } from "crypto"; export class MatrixProvider extends NotificationProvider { @@ -172,10 +172,10 @@ export class MatrixProvider extends NotificationProvider { // Incident link (if incident exists) if (message.content.incident) { - const incidentUrl = `${message.clientHost}/infrastructure/${message.monitor.id}`; + const url = incidentUrl(message); plainLines.push(""); - plainLines.push(`View Incident: ${incidentUrl}`); - htmlLines.push(`

View Incident

`); + plainLines.push(`View Incident: ${url}`); + htmlLines.push(`

View Incident

`); } // Footer with timestamp diff --git a/server/src/domain/notifications/providers/ntfy.ts b/server/src/domain/notifications/providers/ntfy.ts index b415f648b..35f36f5e3 100644 --- a/server/src/domain/notifications/providers/ntfy.ts +++ b/server/src/domain/notifications/providers/ntfy.ts @@ -2,7 +2,7 @@ const SERVICE_NAME = "NtfyProvider"; import type { Notification } from "@/domain/notifications/notification.type.js"; import { NotificationProvider } from "@/domain/notifications/providers/INotificationProvider.js"; import type { NotificationMessage } from "@/domain/notifications/notification.type.js"; -import { getTestMessage } from "@/domain/notifications/providers/utils.js"; +import { getTestMessage, incidentUrl } from "@/domain/notifications/providers/utils.js"; import got from "got"; export class NtfyProvider extends NotificationProvider { @@ -142,7 +142,7 @@ export class NtfyProvider extends NotificationProvider { } if (message.content.incident) { - lines.push("", `${message.clientHost}/infrastructure/${message.monitor.id}`); + lines.push("", incidentUrl(message)); } return lines.join("\n"); diff --git a/server/src/domain/notifications/providers/pushover.ts b/server/src/domain/notifications/providers/pushover.ts index b1831e405..4243a0e32 100644 --- a/server/src/domain/notifications/providers/pushover.ts +++ b/server/src/domain/notifications/providers/pushover.ts @@ -2,7 +2,7 @@ const SERVICE_NAME = "PushoverProvider"; import type { Notification } from "@/domain/notifications/notification.type.js"; import { NotificationProvider } from "@/domain/notifications/providers/INotificationProvider.js"; import type { NotificationMessage } from "@/domain/notifications/notification.type.js"; -import { getTestMessage } from "@/domain/notifications/providers/utils.js"; +import { getTestMessage, incidentUrl } from "@/domain/notifications/providers/utils.js"; import got from "got"; export class PushoverProvider extends NotificationProvider { @@ -101,7 +101,7 @@ export class PushoverProvider extends NotificationProvider { if (message.content.incident) { lines.push(""); - lines.push(`View Incident: ${message.clientHost}/incidents/${message.monitor.id}`); + lines.push(`View Incident: ${incidentUrl(message)}`); } return lines.join("\n"); diff --git a/server/src/domain/notifications/providers/rocketChat.ts b/server/src/domain/notifications/providers/rocketChat.ts index 2ef8d7efb..78d93ce8f 100644 --- a/server/src/domain/notifications/providers/rocketChat.ts +++ b/server/src/domain/notifications/providers/rocketChat.ts @@ -1,5 +1,5 @@ const SERVICE_NAME = "RocketChatProvider"; -import { supportsUptimeDetails, type MonitorType } from "@/domain/monitors/monitor.type.js"; +import { getMonitorPath, type MonitorType } from "@/domain/monitors/monitor.type.js"; import type { Notification, NotificationMessage } from "@/domain/notifications/notification.type.js"; import { NotificationProvider } from "@/domain/notifications/providers/INotificationProvider.js"; import { getTestMessage } from "@/domain/notifications/providers/utils.js"; @@ -139,19 +139,7 @@ export class RocketChatProvider extends NotificationProvider { return undefined; } - if (message.monitor.type === "hardware") { - return `${clientHost}/infrastructure/${message.monitor.id}`; - } - - if (message.monitor.type === "pagespeed") { - return `${clientHost}/pagespeed/${message.monitor.id}`; - } - - if (supportsUptimeDetails(message.monitor.type as MonitorType)) { - return `${clientHost}/uptime/${message.monitor.id}`; - } - - return undefined; + return `${clientHost}/${getMonitorPath(message.monitor.type as MonitorType)}/${message.monitor.id}`; } private severityColor(severity: NotificationMessage["severity"]): string { diff --git a/server/src/domain/notifications/providers/signalgrid.ts b/server/src/domain/notifications/providers/signalgrid.ts index fdd930724..889206c67 100644 --- a/server/src/domain/notifications/providers/signalgrid.ts +++ b/server/src/domain/notifications/providers/signalgrid.ts @@ -1,7 +1,7 @@ const SERVICE_NAME = "SignalgridProvider"; import type { Notification, NotificationMessage, NotificationSeverity } from "@/domain/notifications/notification.type.js"; import { NotificationProvider } from "@/domain/notifications/providers/INotificationProvider.js"; -import { getTestMessage } from "@/domain/notifications/providers/utils.js"; +import { getTestMessage, incidentUrl } from "@/domain/notifications/providers/utils.js"; import got from "got"; export class SignalgridProvider extends NotificationProvider { @@ -115,7 +115,7 @@ export class SignalgridProvider extends NotificationProvider { if (message.content.incident) { lines.push(""); - lines.push(`View Incident: ${message.clientHost}/incidents/${message.monitor.id}`); + lines.push(`View Incident: ${incidentUrl(message)}`); } return lines.join("\n"); diff --git a/server/src/domain/notifications/providers/slack.ts b/server/src/domain/notifications/providers/slack.ts index 23f9b2f61..f73526ce0 100644 --- a/server/src/domain/notifications/providers/slack.ts +++ b/server/src/domain/notifications/providers/slack.ts @@ -2,7 +2,7 @@ const SERVICE_NAME = "SlackProvider"; import type { Notification } from "@/domain/notifications/notification.type.js"; import { NotificationProvider } from "@/domain/notifications/providers/INotificationProvider.js"; import type { NotificationMessage } from "@/domain/notifications/notification.type.js"; -import { getTestMessage } from "@/domain/notifications/providers/utils.js"; +import { getTestMessage, incidentUrl } from "@/domain/notifications/providers/utils.js"; import got, { HTTPError } from "got"; export class SlackProvider extends NotificationProvider { @@ -161,7 +161,7 @@ export class SlackProvider extends NotificationProvider { text: "View Incident", emoji: true, }, - url: `${message.clientHost}/infrastructure/${message.monitor.id}`, + url: incidentUrl(message), style: "primary", }, ], diff --git a/server/src/domain/notifications/providers/teams.ts b/server/src/domain/notifications/providers/teams.ts index 36d37a210..98e9e3c43 100644 --- a/server/src/domain/notifications/providers/teams.ts +++ b/server/src/domain/notifications/providers/teams.ts @@ -2,7 +2,7 @@ const SERVICE_NAME = "TeamsProvider"; import type { Notification } from "@/domain/notifications/notification.type.js"; import { NotificationProvider } from "@/domain/notifications/providers/INotificationProvider.js"; import type { NotificationMessage } from "@/domain/notifications/notification.type.js"; -import { getTestMessage } from "@/domain/notifications/providers/utils.js"; +import { getTestMessage, incidentUrl } from "@/domain/notifications/providers/utils.js"; import got, { HTTPError } from "got"; // Types for Adaptive Card elements @@ -254,7 +254,7 @@ export class TeamsProvider extends NotificationProvider { actions.push({ type: "Action.OpenUrl", title: "View Incident", - url: `${message.clientHost}/incidents/${message.content.incident.id}`, + url: incidentUrl(message), }); } diff --git a/server/src/domain/notifications/providers/telegram.ts b/server/src/domain/notifications/providers/telegram.ts index 72e31b5bf..26b2c2786 100644 --- a/server/src/domain/notifications/providers/telegram.ts +++ b/server/src/domain/notifications/providers/telegram.ts @@ -2,7 +2,7 @@ const SERVICE_NAME = "TelegramProvider"; import type { Notification } from "@/domain/notifications/notification.type.js"; import { NotificationProvider } from "@/domain/notifications/providers/INotificationProvider.js"; import type { NotificationMessage } from "@/domain/notifications/notification.type.js"; -import { getTestMessage } from "@/domain/notifications/providers/utils.js"; +import { getTestMessage, incidentUrl } from "@/domain/notifications/providers/utils.js"; import got from "got"; export class TelegramProvider extends NotificationProvider { @@ -101,7 +101,7 @@ export class TelegramProvider extends NotificationProvider { if (message.content.incident) { lines.push(""); - lines.push(`View Incident`); + lines.push(`View Incident`); } return lines.join("\n"); diff --git a/server/src/domain/notifications/providers/utils.ts b/server/src/domain/notifications/providers/utils.ts index 3eaf428fe..4ac4d96fc 100644 --- a/server/src/domain/notifications/providers/utils.ts +++ b/server/src/domain/notifications/providers/utils.ts @@ -1,4 +1,8 @@ import { IEmailService } from "@/service/emailService.js"; +import type { NotificationMessage } from "@/domain/notifications/notification.type.js"; + +export const incidentUrl = (message: NotificationMessage): string => `${message.clientHost}/incidents/${message.monitor.id}`; + export const buildTestEmail = async (emailService: IEmailService) => { const context = { testName: "Monitoring System" }; const html = await emailService.buildEmail("testEmailTemplate", context); diff --git a/server/src/domain/notifications/providers/webhook.ts b/server/src/domain/notifications/providers/webhook.ts index fc5a80fde..64fbb26e9 100644 --- a/server/src/domain/notifications/providers/webhook.ts +++ b/server/src/domain/notifications/providers/webhook.ts @@ -2,7 +2,7 @@ const SERVICE_NAME = "WebhookProvider"; import type { Notification } from "@/domain/notifications/notification.type.js"; import { NotificationProvider } from "@/domain/notifications/providers/INotificationProvider.js"; import type { NotificationMessage } from "@/domain/notifications/notification.type.js"; -import { getTestMessage } from "@/domain/notifications/providers/utils.js"; +import { getTestMessage, incidentUrl } from "@/domain/notifications/providers/utils.js"; import got from "got"; export class WebhookProvider extends NotificationProvider { @@ -74,7 +74,7 @@ export class WebhookProvider extends NotificationProvider { // Incident link if (message.content.incident) { - lines.push(`[View Incident](${message.clientHost}/infrastructure/${message.monitor.id})`); + lines.push(`[View Incident](${incidentUrl(message)})`); } // Return webhook payload with both text and structured data diff --git a/server/test/unit/providers/notifications/matrixProvider.test.ts b/server/test/unit/providers/notifications/matrixProvider.test.ts index 1294a677b..7cd0ea427 100644 --- a/server/test/unit/providers/notifications/matrixProvider.test.ts +++ b/server/test/unit/providers/notifications/matrixProvider.test.ts @@ -105,6 +105,14 @@ describe("MatrixProvider", () => { expect(body.formatted_body).toContain("View Incident"); }); + it("links the incident to the monitor's incidents page", async () => { + const { provider } = createProvider(); + await provider.sendMessage(makeNotification() as any, makeMessageWithIncident()); + const body = mockGotPut.mock.calls[0][1].json; + expect(body.body).toContain("View Incident: https://app.example.com/incidents/mon-1"); + expect(body.formatted_body).toContain('href="https://app.example.com/incidents/mon-1"'); + }); + it("omits optional sections when not present", async () => { const { provider } = createProvider(); const msg = makeMessage(); diff --git a/server/test/unit/providers/notifications/ntfyProvider.test.ts b/server/test/unit/providers/notifications/ntfyProvider.test.ts index d365584a1..bced0b6b3 100644 --- a/server/test/unit/providers/notifications/ntfyProvider.test.ts +++ b/server/test/unit/providers/notifications/ntfyProvider.test.ts @@ -106,7 +106,7 @@ describe("NtfyProvider", () => { it("includes incident links in text", async () => { const { provider } = createProvider(); await provider.sendMessage(makeNtfyNotification() as any, makeMessageWithIncident()); - expect(mockGotPost.mock.calls[0][1].body).toContain("/infrastructure/mon-1"); + expect(mockGotPost.mock.calls[0][1].body).toContain("https://app.example.com/incidents/mon-1"); }); }); diff --git a/server/test/unit/providers/notifications/pushoverProvider.test.ts b/server/test/unit/providers/notifications/pushoverProvider.test.ts index a227b0b1b..457803131 100644 --- a/server/test/unit/providers/notifications/pushoverProvider.test.ts +++ b/server/test/unit/providers/notifications/pushoverProvider.test.ts @@ -99,6 +99,12 @@ describe("PushoverProvider", () => { expect(mockGotPost.mock.calls[0][1].form.message).toContain("View Incident"); }); + it("links the incident to the monitor's incidents page", async () => { + const { provider } = createProvider(); + await provider.sendMessage(makeNotification() as any, makeMessageWithIncident()); + expect(mockGotPost.mock.calls[0][1].form.message).toContain("View Incident: https://app.example.com/incidents/mon-1"); + }); + it("omits optional sections when not present", async () => { const { provider } = createProvider(); const msg = makeMessage(); diff --git a/server/test/unit/providers/notifications/rocketChatProvider.test.ts b/server/test/unit/providers/notifications/rocketChatProvider.test.ts index 816e19801..0568c144e 100644 --- a/server/test/unit/providers/notifications/rocketChatProvider.test.ts +++ b/server/test/unit/providers/notifications/rocketChatProvider.test.ts @@ -134,6 +134,7 @@ describe("RocketChatProvider", () => { ["ping", "https://app.example.com/uptime/mon-1"], ["pagespeed", "https://app.example.com/pagespeed/mon-1"], ["hardware", "https://app.example.com/infrastructure/mon-1"], + ["docker", "https://app.example.com/docker/host/mon-1"], ])("links %s monitors to %s", async (type, expectedLink) => { const { provider } = createProvider(); diff --git a/server/test/unit/providers/notifications/signalgridProvider.test.ts b/server/test/unit/providers/notifications/signalgridProvider.test.ts index 8c40bb1d9..b0812509a 100644 --- a/server/test/unit/providers/notifications/signalgridProvider.test.ts +++ b/server/test/unit/providers/notifications/signalgridProvider.test.ts @@ -139,6 +139,12 @@ describe("SignalgridProvider", () => { expect(mockGotPost.mock.calls[0][1].form.body).toContain("View Incident"); }); + it("links the incident to the monitor's incidents page", async () => { + const { provider } = createProvider(); + await provider.sendMessage(makeNotification() as any, makeMessageWithIncident()); + expect(mockGotPost.mock.calls[0][1].form.body).toContain("View Incident: https://app.example.com/incidents/mon-1"); + }); + it("omits optional sections when not present", async () => { const { provider } = createProvider(); const msg = makeMessage(); diff --git a/server/test/unit/providers/notifications/slackProvider.test.ts b/server/test/unit/providers/notifications/slackProvider.test.ts index 20058ab11..86c17f99a 100644 --- a/server/test/unit/providers/notifications/slackProvider.test.ts +++ b/server/test/unit/providers/notifications/slackProvider.test.ts @@ -96,6 +96,14 @@ describe("SlackProvider", () => { expect(text).toContain("Additional Information"); }); + it("links the incident button to the monitor's incidents page", async () => { + const { provider } = createProvider(); + await provider.sendMessage(makeNotification() as any, makeMessageWithIncident()); + const blocks = mockGotPost.mock.calls[0][1].json.blocks; + const actions = blocks.find((b: any) => b.type === "actions"); + expect(actions.elements[0].url).toBe("https://app.example.com/incidents/mon-1"); + }); + it("omits threshold, details, and incident sections when not present", async () => { const { provider } = createProvider(); const msg = makeMessage(); diff --git a/server/test/unit/providers/notifications/teamsProvider.test.ts b/server/test/unit/providers/notifications/teamsProvider.test.ts index c3766ec16..514190fe5 100644 --- a/server/test/unit/providers/notifications/teamsProvider.test.ts +++ b/server/test/unit/providers/notifications/teamsProvider.test.ts @@ -154,7 +154,7 @@ describe("TeamsProvider", () => { expect(card.actions).toHaveLength(1); expect(card.actions[0].type).toBe("Action.OpenUrl"); expect(card.actions[0].title).toBe("View Incident"); - expect(card.actions[0].url).toContain("/incidents/inc-1"); + expect(card.actions[0].url).toBe("https://app.example.com/incidents/mon-1"); }); it("omits actions when no incident is present", async () => { diff --git a/server/test/unit/providers/notifications/telegramProvider.test.ts b/server/test/unit/providers/notifications/telegramProvider.test.ts index f8f853e62..8fe90c412 100644 --- a/server/test/unit/providers/notifications/telegramProvider.test.ts +++ b/server/test/unit/providers/notifications/telegramProvider.test.ts @@ -99,6 +99,12 @@ describe("TelegramProvider", () => { expect(mockGotPost.mock.calls[0][1].json.text).toContain("View Incident"); }); + it("links the incident to the monitor's incidents page", async () => { + const { provider } = createProvider(); + await provider.sendMessage(makeNotification() as any, makeMessageWithIncident()); + expect(mockGotPost.mock.calls[0][1].json.text).toContain('View Incident'); + }); + it("omits optional sections when not present", async () => { const { provider } = createProvider(); const msg = makeMessage(); diff --git a/server/test/unit/providers/notifications/webhookProvider.test.ts b/server/test/unit/providers/notifications/webhookProvider.test.ts index fd16f2c06..659a32e2f 100644 --- a/server/test/unit/providers/notifications/webhookProvider.test.ts +++ b/server/test/unit/providers/notifications/webhookProvider.test.ts @@ -86,6 +86,12 @@ describe("WebhookProvider", () => { expect(mockGotPost.mock.calls[0][1].json.text).toContain("View Incident"); }); + it("links the incident to the monitor's incidents page", async () => { + const { provider } = createProvider(); + await provider.sendMessage(makeNotification() as any, makeMessageWithIncident()); + expect(mockGotPost.mock.calls[0][1].json.text).toContain("[View Incident](https://app.example.com/incidents/mon-1)"); + }); + it("omits threshold and incident sections when not present", async () => { const { provider } = createProvider(); const msg = makeMessage();