diff --git a/apps/uptime/src/index.ts b/apps/uptime/src/index.ts index 65e0f237f..5c488fa19 100644 --- a/apps/uptime/src/index.ts +++ b/apps/uptime/src/index.ts @@ -14,6 +14,7 @@ import { disconnectProducer, sendUptimeEvent } from "./lib/producer"; import { captureError, mergeWideEvent } from "./lib/tracing"; import { getPreviousMonitorStatus, + sendUptimeAlarmNotificationsIfNeeded, sendUptimeTransitionEmailsIfNeeded, } from "./uptime-transition-emails"; @@ -263,6 +264,11 @@ const app = new Elysia() data: result.data, previousStatus, }); + await sendUptimeAlarmNotificationsIfNeeded({ + schedule: schedule.data, + data: result.data, + previousStatus, + }); } catch (error) { captureError(error, { error_step: "producer_pipeline", diff --git a/apps/uptime/src/uptime-transition-emails.ts b/apps/uptime/src/uptime-transition-emails.ts index 07a8c7343..afd8cbcd6 100644 --- a/apps/uptime/src/uptime-transition-emails.ts +++ b/apps/uptime/src/uptime-transition-emails.ts @@ -1,7 +1,12 @@ import { and, db, eq } from "@databuddy/db"; -import { member, user } from "@databuddy/db/schema"; +import { alarms, member, user } from "@databuddy/db/schema"; import { chQuery } from "@databuddy/db/clickhouse"; import { UptimeAlertEmail } from "@databuddy/email"; +import { + type NotificationChannel, + NotificationClient, +} from "@databuddy/notifications"; +import { buildUptimeNotificationPayload } from "@databuddy/notifications/templates/uptime"; import { Resend } from "resend"; import type { ScheduleData } from "./actions"; import { captureError } from "./lib/tracing"; @@ -159,3 +164,110 @@ export async function sendUptimeTransitionEmailsIfNeeded(options: { captureError(error, { error_step: "transition_email" }); } } + +export async function sendUptimeAlarmNotificationsIfNeeded(options: { + schedule: ScheduleData; + data: UptimeData; + previousStatus: number | undefined; +}): Promise { + const kind = resolveTransitionKind( + options.previousStatus, + options.data.status + ); + if (kind === null) { + return; + } + + if (!options.schedule.websiteId) { + return; + } + + const matchingAlarms = await db.query.alarms.findMany({ + where: and( + eq(alarms.websiteId, options.schedule.websiteId), + eq(alarms.enabled, true), + eq(alarms.triggerType, "uptime") + ), + with: { destinations: true }, + }); + + if (matchingAlarms.length === 0) { + return; + } + + const siteLabel = buildSiteLabel(options.schedule); + const sslExpiry = + options.data.ssl_expiry > 0 ? options.data.ssl_expiry : undefined; + + const payload = buildUptimeNotificationPayload({ + kind, + siteLabel, + url: options.data.url, + checkedAt: options.data.timestamp, + httpCode: options.data.http_code, + error: options.data.error ?? "", + probeRegion: options.data.probe_region, + totalMs: options.data.total_ms, + ttfbMs: options.data.ttfb_ms, + sslValid: options.data.ssl_valid === 1, + sslExpiryMs: sslExpiry, + }); + + const dispatchPromises = matchingAlarms + .filter((alarm) => alarm.destinations && alarm.destinations.length > 0) + .map(async (alarm) => { + const clientConfig: Record> = {}; + const channels: NotificationChannel[] = []; + + for (const dest of alarm.destinations) { + const cfg = (dest.config ?? {}) as Record; + + if (dest.type === "slack") { + clientConfig.slack = { webhookUrl: dest.identifier }; + channels.push("slack"); + } else if (dest.type === "discord") { + clientConfig.discord = { webhookUrl: dest.identifier }; + channels.push("discord"); + } else if (dest.type === "teams") { + clientConfig.teams = { webhookUrl: dest.identifier }; + channels.push("teams"); + } else if (dest.type === "google_chat") { + clientConfig.googleChat = { webhookUrl: dest.identifier }; + channels.push("google-chat"); + } else if (dest.type === "telegram") { + const botToken = typeof cfg.botToken === "string" ? cfg.botToken : ""; + const chatId = dest.identifier || (typeof cfg.chatId === "string" ? cfg.chatId : ""); + if (!botToken || !chatId) { + return; + } + clientConfig.telegram = { botToken, chatId }; + channels.push("telegram"); + } else if (dest.type === "webhook") { + const headers = cfg.headers && typeof cfg.headers === "object" + ? cfg.headers as Record + : undefined; + clientConfig.webhook = { + url: dest.identifier, + headers, + }; + channels.push("webhook"); + } + } + + if (channels.length === 0) { + return; + } + + try { + const client = new NotificationClient(clientConfig); + await client.send(payload, { channels }); + } catch (error) { + captureError(error, { + error_step: "uptime_alarm_notification", + alarm_id: alarm.id, + }); + } + }); + + await Promise.allSettled(dispatchPromises); +}