fix incident links with helper

This commit is contained in:
Alex Holliday committed 2026-09-23 11:46:58 -07:00
1 parent 618258d282
commit 7fb57d8090
20 files changed
+83 -34

No files matched your search

@@ -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<MonitorType, string> = {
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.
@@ -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(`<p><a href="${this.escapeHtml(incidentUrl)}">View Incident</a></p>`);
plainLines.push(`View Incident: ${url}`);
htmlLines.push(`<p><a href="${this.escapeHtml(url)}">View Incident</a></p>`);
}
// Footer with timestamp
@@ -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");
@@ -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");
@@ -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 {
@@ -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");
@@ -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",
},
],
@@ -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),
});
}
@@ -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(`<a href="${message.clientHost}/incidents/${message.monitor.id}">View Incident</a>`);
lines.push(`<a href="${incidentUrl(message)}">View Incident</a>`);
}
return lines.join("\n");
@@ -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);
@@ -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
@@ -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();
@@ -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");
});
});
@@ -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();
@@ -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();
@@ -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();
@@ -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();
@@ -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 () => {
@@ -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('<a href="https://app.example.com/incidents/mon-1">View Incident</a>');
});
it("omits optional sections when not present", async () => {
const { provider } = createProvider();
const msg = makeMessage();
@@ -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();