Openstatus
www.openstatus.dev
1import { slackDataSchema } from "@openstatus/db/src/schema";
2import {
3 COLORS,
4 type NotificationContext,
5 buildCommonMessageData,
6} from "@openstatus/notification-base";
7import {
8 buildAlertBlocks,
9 buildDegradedBlocks,
10 buildRecoveryBlocks,
11} from "./blocks";
12
13// biome-ignore lint/suspicious/noExplicitAny: <explanation>
14const postToWebhook = async (body: any, webhookUrl: string) => {
15 if (!webhookUrl || webhookUrl.trim() === "") {
16 throw new Error("Slack webhook URL is required");
17 }
18
19 const res = await fetch(webhookUrl, {
20 method: "POST",
21 body: JSON.stringify(body),
22 });
23 if (!res.ok) {
24 throw new Error(`Failed to send Slack notification: ${res.statusText}`);
25 }
26};
27
28export const sendAlert = async ({
29 monitor,
30 notification,
31 statusCode,
32 message,
33 cronTimestamp,
34 latency,
35 regions,
36}: NotificationContext) => {
37 const notificationData = slackDataSchema.parse(JSON.parse(notification.data));
38 const { slack: webhookUrl } = notificationData;
39
40 const context = {
41 monitor,
42 notification,
43 statusCode,
44 message,
45 cronTimestamp,
46 latency,
47 regions,
48 };
49
50 const data = buildCommonMessageData(context);
51 const blocks = buildAlertBlocks(data);
52
53 await postToWebhook(
54 {
55 attachments: [
56 {
57 color: COLORS.red,
58 blocks,
59 },
60 ],
61 },
62 webhookUrl,
63 );
64};
65
66export const sendRecovery = async ({
67 monitor,
68 notification,
69 statusCode,
70 message,
71 incident,
72 cronTimestamp,
73 regions,
74 latency,
75}: NotificationContext) => {
76 const notificationData = slackDataSchema.parse(JSON.parse(notification.data));
77 const { slack: webhookUrl } = notificationData;
78
79 const context = {
80 monitor,
81 notification,
82 statusCode,
83 message,
84 cronTimestamp,
85 latency,
86 regions,
87 };
88
89 const data = buildCommonMessageData(context, { incident });
90 const blocks = buildRecoveryBlocks(data);
91
92 await postToWebhook(
93 {
94 attachments: [
95 {
96 color: COLORS.green,
97 blocks,
98 },
99 ],
100 },
101 webhookUrl,
102 );
103};
104
105export const sendDegraded = async ({
106 monitor,
107 notification,
108 statusCode,
109 message,
110 incident,
111 cronTimestamp,
112 regions,
113 latency,
114}: NotificationContext) => {
115 const notificationData = slackDataSchema.parse(JSON.parse(notification.data));
116 const { slack: webhookUrl } = notificationData;
117
118 const context = {
119 monitor,
120 notification,
121 statusCode,
122 message,
123 cronTimestamp,
124 latency,
125 regions,
126 };
127
128 const data = buildCommonMessageData(context, { incident });
129 const blocks = buildDegradedBlocks(data);
130
131 await postToWebhook(
132 {
133 attachments: [
134 {
135 color: COLORS.yellow,
136 blocks,
137 },
138 ],
139 },
140 webhookUrl,
141 );
142};
143
144export const sendTestSlackMessage = async (webhookUrl: string) => {
145 await postToWebhook(
146 {
147 attachments: [
148 {
149 color: COLORS.green,
150 blocks: [
151 {
152 type: "header",
153 text: {
154 type: "plain_text",
155 text: "Test Notification",
156 emoji: false,
157 },
158 },
159 {
160 type: "section",
161 text: {
162 type: "mrkdwn",
163 text: "`🧪 Your Slack webhook is configured correctly!`",
164 },
165 },
166 {
167 type: "divider",
168 },
169 {
170 type: "section",
171 fields: [
172 {
173 type: "mrkdwn",
174 text: "*Status*\nWebhook Connected",
175 },
176 {
177 type: "mrkdwn",
178 text: "*Type*\nTest Notification",
179 },
180 ],
181 },
182 {
183 type: "section",
184 text: {
185 type: "mrkdwn",
186 text: "*Next Steps*\nYou will receive notifications here when your monitors trigger fail, recover, or degrades.",
187 },
188 },
189 {
190 type: "actions",
191 elements: [
192 {
193 type: "button",
194 text: {
195 type: "plain_text",
196 text: "View Dashboard",
197 emoji: true,
198 },
199 url: "https://app.openstatus.dev",
200 action_id: "view_dashboard",
201 },
202 ],
203 },
204 ],
205 },
206 ],
207 },
208 webhookUrl,
209 );
210};