test alert email works

This commit is contained in:
Fred KISSIE
2026-08-11 22:25:26 +02:00
parent 899c47e9a3
commit 21032bc22b
9 changed files with 214 additions and 25 deletions
@@ -20,7 +20,10 @@ export async function processTestAlerts(context: TestAlertContext) {
try {
const recipients = await resolveEmailRecipients(action);
if (recipients.length > 0) {
await sendAlertEmail(recipients, context);
await sendAlertEmail(recipients, {
...context,
isTest: true
});
}
} catch (err) {
logger.error(`processTestAlerts: failed to send alert email`, err);
+34 -15
View File
@@ -15,7 +15,24 @@ import { sendEmail } from "@server/emails";
import AlertNotification from "@server/emails/templates/AlertNotification";
import config from "@server/lib/config";
import logger from "@server/logger";
import { AlertContext } from "@server/routers/alertRule/types";
import {
AlertContext,
type AlertEventType
} from "@server/routers/alertRule/types";
type EmailAlertContext = {
eventType: AlertEventType;
orgId: string;
/** Set for site_online / site_offline events */
siteId?: number;
/** Set for health_check_* events */
healthCheckId?: number;
/** Set for resource_* events */
resourceId?: number;
/** Human-readable context data included in emails and webhook payloads */
data: Record<string, unknown>;
isTest?: boolean;
};
/**
* Sends an alert notification email to every address in `recipients`.
@@ -27,7 +44,7 @@ import { AlertContext } from "@server/routers/alertRule/types";
*/
export async function sendAlertEmail(
recipients: string[],
context: AlertContext
context: EmailAlertContext
): Promise<void> {
if (recipients.length === 0) {
return;
@@ -46,7 +63,8 @@ export async function sendAlertEmail(
eventType: context.eventType,
orgId: context.orgId,
data: context.data,
dashboardLink
dashboardLink,
isTestAlert: context.isTest
}),
{
from,
@@ -70,34 +88,35 @@ export async function sendAlertEmail(
// Helpers
// ---------------------------------------------------------------------------
function buildSubject(context: AlertContext): string {
function buildSubject(context: EmailAlertContext): string {
const prefix = context.isTest ? "[Test Alert]" : "[Alert]";
switch (context.eventType) {
case "site_online":
return "[Alert] Site Back Online";
return `${prefix} Site Back Online`;
case "site_offline":
return "[Alert] Site Offline";
return `${prefix} Site Offline`;
case "site_toggle":
return "[Alert] Site Status Changed";
return `${prefix} Site Status Changed`;
case "health_check_healthy":
return "[Alert] Health Check Recovered";
return `${prefix} Health Check Recovered`;
case "health_check_unhealthy":
return "[Alert] Health Check Failing";
return `${prefix} Health Check Failing`;
case "health_check_toggle":
return "[Alert] Health Check Status Changed";
return `${prefix} Health Check Status Changed`;
case "resource_healthy":
return "[Alert] Resource Healthy";
return `${prefix} Resource Healthy`;
case "resource_unhealthy":
return "[Alert] Resource Unhealthy";
return `${prefix} Resource Unhealthy`;
case "resource_degraded":
return "[Alert] Resource Degraded";
return `${prefix} Resource Degraded`;
case "resource_toggle":
return "[Alert] Resource Status Changed";
return `${prefix} Resource Status Changed`;
default: {
// Exhaustiveness fallback should never be reached with a
// well-typed caller, but keeps runtime behaviour predictable.
const _exhaustive: never = context.eventType;
void _exhaustive;
return "[Alert] Event Notification";
return `${prefix} Event Notification`;
}
}
}
@@ -27,7 +27,13 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { and, asc, desc, eq, inArray, like, or, sql } from "drizzle-orm";
import { ListAlertRulesResponse } from "@server/routers/alertRule/types";
import {
ListAlertRulesResponse,
type AlertAction,
type EmailAlertAction
} from "@server/routers/alertRule/types";
import { processTestAlerts } from "@server/private/lib/alerts/processTestAlerts";
import { getRandomItemInArray } from "@app/lib/getRandomItemInArray";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty()
@@ -51,12 +57,12 @@ export const RESOURCE_EVENT_TYPES = [
] as const;
const webhookActionSchema = z.strictObject({
webhookUrl: z.string().url(),
webhookUrl: z.url(),
config: z.string().optional(),
enabled: z.boolean().optional().default(true)
});
const bodySchema = z.strictObject({
const bodySchema = z.object({
eventType: z.enum([
...HC_EVENT_TYPES,
...SITE_EVENT_TYPES,
@@ -97,7 +103,106 @@ export async function testAlertRule(
);
}
const body = parsedBody.data;
const collectedActions: AlertAction[] = [];
if (
body.emails.length > 0 ||
body.roleIds.length > 0 ||
body.userIds.length > 0
) {
collectedActions.push({
type: "email",
emails: body.emails,
roleIds: body.roleIds,
userIds: body.userIds
});
}
for (const action of body.webhookActions) {
collectedActions.push({
type: "webhook",
...action
});
}
let data: Record<string, any> = {};
switch (body.eventType) {
case "site_toggle":
data = {
status: getRandomItemInArray(["online", "offline"]),
siteName: "Test Site Alert"
};
break;
case "site_offline":
data = {
status: "offline",
siteName: "Test Site Alert"
};
break;
case "site_online":
data = {
status: "online",
siteName: "Test Site Alert"
};
break;
case "resource_toggle":
data = {
status: getRandomItemInArray([
"healthy",
"unhealthy",
"degraded"
]),
siteName: "Test Resource Alert"
};
break;
case "resource_healthy":
data = {
status: "healthy",
siteName: "Test Resource Alert"
};
break;
case "resource_unhealthy":
data = {
status: "unhealthy",
siteName: "Test Resource Alert"
};
break;
case "resource_degraded":
data = {
status: "degraded",
siteName: "Test Resource Alert"
};
break;
case "health_check_toggle":
data = {
status: getRandomItemInArray(["healthy", "unhealthy"]),
healthCheckName: "Test Health Check Alert"
};
break;
case "health_check_healthy":
data = {
status: "healthy",
healthCheckName: "Test Health Check Alert"
};
break;
case "health_check_unhealthy":
data = {
status: "unhealthy",
healthCheckName: "Test Health Check Alert"
};
break;
default:
break;
}
// TODO: process alert rule
await processTestAlerts({
eventType: body.eventType,
orgId,
actions: collectedActions,
data
});
} catch (error) {
logger.error(error);
return next(
+1 -1
View File
@@ -809,7 +809,7 @@ authenticated.get(
);
authenticated.post(
"/org/:orgId/alert-rule/test",
"/org/:orgId/test-alert-rule",
verifyValidLicense,
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.testAlertRule),