diff --git a/server/private/lib/alerts/processTestAlerts.ts b/server/private/lib/alerts/processTestAlerts.ts index 1a6c1d7c6..f7fa47b20 100644 --- a/server/private/lib/alerts/processTestAlerts.ts +++ b/server/private/lib/alerts/processTestAlerts.ts @@ -7,15 +7,13 @@ import type { } from "@server/routers/alertRule/types"; import { eq, inArray } from "drizzle-orm"; import { sendAlertEmail } from "./sendAlertEmail"; -import { decrypt } from "@server/lib/crypto"; -import config from "@server/lib/config"; import { sendAlertWebhook } from "./sendAlertWebhook"; export async function processTestAlerts(context: TestAlertContext) { + // Process email actions const emailActions = context.actions.filter( (action) => action.type === "email" ); - // Process email actions for (const action of emailActions) { try { const recipients = await resolveEmailRecipients(action); @@ -30,10 +28,10 @@ export async function processTestAlerts(context: TestAlertContext) { } } + // Process webhook actions const webhookActions = context.actions.filter( (action) => action.type === "webhook" ); - const serverSecret = config.getRawConfig().server.secret!; for (const action of webhookActions) { try { @@ -41,8 +39,9 @@ export async function processTestAlerts(context: TestAlertContext) { if (action.config) { try { - const decrypted = decrypt(action.config, serverSecret); - webhookConfig = JSON.parse(decrypted) as WebhookAlertConfig; + webhookConfig = JSON.parse( + action.config + ) as WebhookAlertConfig; } catch (err) { logger.error( `processTestAlerts: failed to decrypt webhook`, @@ -52,7 +51,10 @@ export async function processTestAlerts(context: TestAlertContext) { } } - await sendAlertWebhook(action.webhookUrl, webhookConfig, context); + await sendAlertWebhook(action.webhookUrl, webhookConfig, { + ...context, + isTest: true + }); } catch (err) { logger.error( `processTestAlerts: failed to send alert webhook `, diff --git a/server/private/lib/alerts/sendAlertEmail.ts b/server/private/lib/alerts/sendAlertEmail.ts index 0eef6fb5c..ab7d49acc 100644 --- a/server/private/lib/alerts/sendAlertEmail.ts +++ b/server/private/lib/alerts/sendAlertEmail.ts @@ -15,10 +15,7 @@ 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, - type AlertEventType -} from "@server/routers/alertRule/types"; +import { type AlertEventType } from "@server/routers/alertRule/types"; type EmailAlertContext = { eventType: AlertEventType; diff --git a/server/private/lib/alerts/sendAlertWebhook.ts b/server/private/lib/alerts/sendAlertWebhook.ts index 27e142cc4..d3a5d421b 100644 --- a/server/private/lib/alerts/sendAlertWebhook.ts +++ b/server/private/lib/alerts/sendAlertWebhook.ts @@ -14,13 +14,28 @@ import logger from "@server/logger"; import { AlertContext, - WebhookAlertConfig + WebhookAlertConfig, + type AlertEventType } from "@server/routers/alertRule/types"; const REQUEST_TIMEOUT_MS = 15_000; const MAX_RETRIES = 3; const RETRY_BASE_DELAY_MS = 500; +type WebhookAlertContext = { + 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; + isTest?: boolean; +}; + /** * Sends a single webhook POST for an alert event. * @@ -40,14 +55,14 @@ const RETRY_BASE_DELAY_MS = 500; export async function sendAlertWebhook( url: string, webhookConfig: WebhookAlertConfig, - context: AlertContext + context: WebhookAlertContext ): Promise { const eventType = context.eventType; const timestamp = new Date().toISOString(); const status = deriveStatus(eventType, context.data); const data = { orgId: context.orgId, ...context.data }; - let body: string; + let body: Record; if (webhookConfig.useBodyTemplate && webhookConfig.bodyTemplate?.trim()) { body = renderTemplate(webhookConfig.bodyTemplate, { event: eventType, @@ -56,7 +71,11 @@ export async function sendAlertWebhook( data }); } else { - body = JSON.stringify({ event: eventType, timestamp, status, data }); + body = { event: eventType, timestamp, status, data }; + } + + if (body.data && context.isTest) { + body.data.test = true; } const headers = buildHeaders(webhookConfig); @@ -75,7 +94,7 @@ export async function sendAlertWebhook( response = await fetch(url, { method: webhookConfig.method ?? "POST", headers, - body, + body: JSON.stringify(body), signal: controller.signal }); } catch (err: unknown) { @@ -247,7 +266,10 @@ interface TemplateContext { * left untouched. * 3. The fixed top-level keys: event, timestamp, status. */ -function renderTemplate(template: string, ctx: TemplateContext): string { +function renderTemplate( + template: string, + ctx: TemplateContext +): Record { // Step 1 – expand {{data}} first so its contents are already serialised // and won't be touched by later passes. let rendered = template.replace(/\{\{data\}\}/g, JSON.stringify(ctx.data)); @@ -280,20 +302,19 @@ function renderTemplate(template: string, ctx: TemplateContext): string { // Validate the rendered result is valid JSON; if not, log a warning and // fall back to the default payload so the webhook still fires. try { - JSON.parse(rendered); - return rendered; + return JSON.parse(rendered); } catch { logger.warn( `sendAlertWebhook: body template produced invalid JSON for event ` + `"${ctx.event}" destined for a webhook. Falling back to default ` + `payload. Check that {{data}} is NOT wrapped in quotes in your template.` ); - return JSON.stringify({ + return { event: ctx.event, timestamp: ctx.timestamp, status: ctx.status, data: ctx.data - }); + }; } } diff --git a/src/components/alert-rule-editor/AlertRuleGraphEditor.tsx b/src/components/alert-rule-editor/AlertRuleGraphEditor.tsx index 243ce82f8..a3e5e89cb 100644 --- a/src/components/alert-rule-editor/AlertRuleGraphEditor.tsx +++ b/src/components/alert-rule-editor/AlertRuleGraphEditor.tsx @@ -180,8 +180,9 @@ export default function AlertRuleGraphEditor({ const testAlert = async () => { const isValid = await form.trigger(); + const values = form.getValues(); + if (!isValid) { - const values = form.getValues(); if (values.actions.length === 0) { toast({ variant: "warning", @@ -193,27 +194,14 @@ export default function AlertRuleGraphEditor({ return; } - const values = form.getValues(); try { const payload = formValuesToApiPayload(values); - if (isNew) { - const res = await api.post< - AxiosResponse - >(`/org/${orgId}/test-alert-rule`, payload); - toast({ - title: t("alertingTestAlertSent"), - description: t("alertingTestAlertSentDescription") - }); - } else { - await api.post( - `/org/${orgId}/alert-rule/${alertRuleId}`, - payload - ); - toast({ - title: t("alertingTestAlertSent"), - description: t("alertingTestAlertSentDescription") - }); - } + await api.post(`/org/${orgId}/test-alert-rule`, payload); + + toast({ + title: t("alertingTestAlertSent"), + description: t("alertingTestAlertSentDescription") + }); } catch (e) { toast({ title: t("error"), diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 96024c3a0..e9e324c08 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -1371,7 +1371,7 @@ export const approvalQueries = { }, refetchInterval: (query) => { if (query.state.data) { - return durationToMs(30, "seconds"); + return durationToMs(1.5, "minutes"); } return false; }