Openstatus www.openstatus.dev

feat: using effect-ts to retry notification (#1652)

* trying effect

* ci: apply automated fixes

* check return

* aadd backoff

* ci: apply automated fixes

* fix test

* format

* fix alerting

* ci: apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

authored by

Thibault Le Ouay
autofix-ci[bot]
and committed by
GitHub
b086303a 175ecb24

+533 -489
+1
apps/workflows/package.json
··· 29 29 "@openstatus/utils": "workspace:*", 30 30 "@upstash/qstash": "2.6.2", 31 31 "drizzle-orm": "0.44.4", 32 + "effect": "3.19.12", 32 33 "hono": "4.5.3", 33 34 "limiter": "^3.0.0", 34 35 "zod": "3.25.76"
+71 -30
apps/workflows/src/checker/alerting.ts
··· 8 8 9 9 import { getLogger } from "@logtape/logtape"; 10 10 import type { Region } from "@openstatus/db/src/schema/constants"; 11 + import { Effect, Schedule } from "effect"; 11 12 import { checkerAudit } from "../utils/audit-log"; 12 13 import { providerToFunction } from "./utils"; 13 14 ··· 115 116 } 116 117 switch (notifType) { 117 118 case "alert": 118 - await providerToFunction[notif.notification.provider].sendAlert({ 119 - monitor, 120 - notification: selectNotificationSchema.parse(notif.notification), 121 - statusCode, 122 - message, 123 - incidentId, 124 - cronTimestamp, 125 - region, 126 - latency, 127 - }); 119 + const alertResult = Effect.tryPromise({ 120 + try: () => 121 + providerToFunction[notif.notification.provider].sendAlert({ 122 + monitor, 123 + notification: selectNotificationSchema.parse(notif.notification), 124 + statusCode, 125 + message, 126 + incidentId, 127 + cronTimestamp, 128 + region, 129 + latency, 130 + }), 131 + 132 + catch: (_unknown) => 133 + new Error( 134 + `Failed sending notification via ${notif.notification.provider} for monitor ${monitorId}`, 135 + ), 136 + }).pipe( 137 + Effect.retry({ 138 + times: 3, 139 + schedule: Schedule.exponential("1000 millis"), 140 + }), 141 + ); 142 + await Effect.runPromise(alertResult).catch(console.error); 128 143 break; 129 144 case "recovery": 130 - await providerToFunction[notif.notification.provider].sendRecovery({ 131 - monitor, 132 - notification: selectNotificationSchema.parse(notif.notification), 133 - statusCode, 134 - message, 135 - incidentId, 136 - cronTimestamp, 137 - region, 138 - latency, 139 - }); 145 + const recoveryResult = Effect.tryPromise({ 146 + try: () => 147 + providerToFunction[notif.notification.provider].sendRecovery({ 148 + monitor, 149 + notification: selectNotificationSchema.parse(notif.notification), 150 + statusCode, 151 + message, 152 + incidentId, 153 + cronTimestamp, 154 + region, 155 + latency, 156 + }), 157 + catch: (_unknown) => 158 + new Error( 159 + `Failed sending notification via ${notif.notification.provider} for monitor ${monitorId}`, 160 + ), 161 + }).pipe( 162 + Effect.retry({ 163 + times: 3, 164 + schedule: Schedule.exponential("1000 millis"), 165 + }), 166 + ); 167 + await Effect.runPromise(recoveryResult).catch(console.error); 140 168 break; 141 169 case "degraded": 142 - await providerToFunction[notif.notification.provider].sendDegraded({ 143 - monitor, 144 - notification: selectNotificationSchema.parse(notif.notification), 145 - statusCode, 146 - message, 147 - cronTimestamp, 148 - region, 149 - latency, 150 - }); 170 + const degradedResult = Effect.tryPromise({ 171 + try: () => 172 + providerToFunction[notif.notification.provider].sendDegraded({ 173 + monitor, 174 + notification: selectNotificationSchema.parse(notif.notification), 175 + statusCode, 176 + message, 177 + incidentId, 178 + cronTimestamp, 179 + region, 180 + latency, 181 + }), 182 + catch: (_unknown) => 183 + new Error( 184 + `Failed sending notification via ${notif.notification.provider} for monitor ${monitorId}`, 185 + ), 186 + }).pipe( 187 + Effect.retry({ 188 + times: 3, 189 + schedule: Schedule.exponential("1000 millis"), 190 + }), 191 + ); 192 + await Effect.runPromise(degradedResult).catch(console.error); 151 193 break; 152 194 } 153 195 // ALPHA ··· 162 204 notificationId: notif.notification.id, 163 205 }, 164 206 }); 165 - // 166 207 } 167 208 }; 168 209
+1
packages/emails/src/client.tsx
··· 181 181 throw result.error; 182 182 } catch (err) { 183 183 console.error(`Error sending monitor alert to ${req.to}`, err); 184 + throw err; 184 185 } 185 186 } 186 187
+11 -12
packages/notifications/discord/src/index.test.ts
··· 12 12 let fetchMock: any = undefined; 13 13 14 14 beforeEach(() => { 15 + // @ts-expect-error 15 16 fetchMock = spyOn(global, "fetch").mockImplementation(() => 16 17 Promise.resolve(new Response(null, { status: 200 })), 17 18 ); ··· 150 151 const notification = selectNotificationSchema.parse( 151 152 createMockNotification(), 152 153 ); 153 - 154 - // Should not throw - function catches errors internally 155 - await sendAlert({ 156 - // @ts-expect-error 157 - monitor, 158 - notification, 159 - statusCode: 500, 160 - message: "Error", 161 - cronTimestamp: Date.now(), 162 - }); 163 - 164 - expect(fetchMock).toHaveBeenCalledTimes(1); 154 + expect( 155 + sendAlert({ 156 + // @ts-expect-error 157 + monitor, 158 + notification, 159 + statusCode: 500, 160 + message: "Error", 161 + cronTimestamp: Date.now(), 162 + }), 163 + ).rejects.toThrow(); 165 164 }); 166 165 });
+9 -4
packages/notifications/discord/src/index.ts
··· 3 3 import type { Region } from "@openstatus/db/src/schema/constants"; 4 4 5 5 const postToWebhook = async (content: string, webhookUrl: string) => { 6 - await fetch(webhookUrl, { 6 + const res = await fetch(webhookUrl, { 7 7 method: "POST", 8 8 headers: { 9 9 "Content-Type": "application/json", ··· 15 15 username: "OpenStatus Notifications", 16 16 }), 17 17 }); 18 + if (!res.ok) { 19 + throw new Error( 20 + `Failed to send Discord webhook: ${res.status} ${res.statusText}`, 21 + ); 22 + } 18 23 }; 19 24 20 25 export const sendAlert = async ({ ··· 52 57 ); 53 58 } catch (err) { 54 59 console.error(err); 55 - // Do something 60 + throw err; 56 61 } 57 62 }; 58 63 ··· 88 93 ); 89 94 } catch (err) { 90 95 console.error(err); 91 - // Do something 96 + throw err; 92 97 } 93 98 }; 94 99 ··· 124 129 ); 125 130 } catch (err) { 126 131 console.error(err); 127 - // Do something 132 + throw err; 128 133 } 129 134 }; 130 135
+4 -1
packages/notifications/discord/tsconfig.json
··· 1 1 { 2 2 "extends": "@openstatus/tsconfig/nextjs.json", 3 - "include": ["src", "*.ts"] 3 + "include": ["src", "*.ts"], 4 + "compilerOptions": { 5 + "types": ["bun-types"] 6 + } 4 7 }
+11 -8
packages/notifications/ntfy/src/index.test.ts
··· 7 7 let fetchMock: any = undefined; 8 8 9 9 beforeEach(() => { 10 + // @ts-expect-error 10 11 fetchMock = spyOn(global, "fetch").mockImplementation(() => 11 12 Promise.resolve(new Response(null, { status: 200 })), 12 13 ); ··· 244 245 ); 245 246 246 247 // Should not throw - function catches errors internally 247 - await sendAlert({ 248 - // @ts-expect-error 249 - monitor, 250 - notification, 251 - statusCode: 500, 252 - message: "Error", 253 - cronTimestamp: Date.now(), 254 - }); 248 + await expect( 249 + sendAlert({ 250 + // @ts-expect-error 251 + monitor, 252 + notification, 253 + statusCode: 500, 254 + message: "Error", 255 + cronTimestamp: Date.now(), 256 + }), 257 + ).rejects.toThrow(); 255 258 256 259 expect(fetchMock).toHaveBeenCalledTimes(1); 257 260 });
+28 -33
packages/notifications/ntfy/src/index.ts
··· 35 35 ? `${notificationData.ntfy.serverUrl}/${notificationData.ntfy.topic}` 36 36 : `https://ntfy.sh/${notificationData.ntfy.topic}`; 37 37 38 - try { 39 - await fetch(url, { 40 - method: "post", 41 - body, 42 - headers: { 43 - ...authorization, 44 - }, 45 - }); 46 - } catch (err) { 47 - console.log(err); 48 - // Do something 38 + const res = await fetch(url, { 39 + method: "post", 40 + body, 41 + headers: { 42 + ...authorization, 43 + }, 44 + }); 45 + if (!res.ok) { 46 + throw new Error(`Failed to send alert notification: ${res.statusText}`); 49 47 } 50 48 }; 51 49 ··· 79 77 const url = notificationData.ntfy.serverUrl 80 78 ? `${notificationData.ntfy.serverUrl}/${notificationData.ntfy.topic}` 81 79 : `https://ntfy.sh/${notificationData.ntfy.topic}`; 82 - try { 83 - await fetch(url, { 84 - method: "post", 85 - body, 86 - headers: { 87 - ...authorization, 88 - }, 89 - }); 90 - } catch (err) { 91 - console.log(err); 92 - // Do something 80 + 81 + const res = await fetch(url, { 82 + method: "post", 83 + body, 84 + headers: { 85 + ...authorization, 86 + }, 87 + }); 88 + if (!res.ok) { 89 + throw new Error(`Failed to send recovery notification: ${res.statusText}`); 93 90 } 94 91 }; 95 92 ··· 123 120 ? `${notificationData.ntfy.serverUrl}/${notificationData.ntfy.topic}` 124 121 : `https://ntfy.sh/${notificationData.ntfy.topic}`; 125 122 126 - try { 127 - await fetch(url, { 128 - method: "post", 129 - body, 130 - headers: { 131 - ...authorization, 132 - }, 133 - }); 134 - } catch (err) { 135 - console.log(err); 136 - // Do something 123 + const res = await fetch(url, { 124 + method: "post", 125 + body, 126 + headers: { 127 + ...authorization, 128 + }, 129 + }); 130 + if (!res.ok) { 131 + throw new Error(`Failed to send degraded notification: ${res.statusText}`); 137 132 } 138 133 }; 139 134
+4 -1
packages/notifications/ntfy/tsconfig.json
··· 1 1 { 2 2 "extends": "@openstatus/tsconfig/nextjs.json", 3 - "include": ["src", "*.ts"] 3 + "include": ["src", "*.ts"], 4 + "compilerOptions": { 5 + "types": ["bun-types"] 6 + } 4 7 }
+11 -10
packages/notifications/opsgenie/src/index.test.ts
··· 7 7 let fetchMock: any = undefined; 8 8 9 9 beforeEach(() => { 10 + // @ts-expect-error 10 11 fetchMock = spyOn(global, "fetch").mockImplementation(() => 11 12 Promise.resolve(new Response(null, { status: 200 })), 12 13 ); ··· 130 131 createMockNotification(), 131 132 ); 132 133 133 - // Should not throw - function catches errors internally 134 - await sendAlert({ 135 - // @ts-expect-error 136 - monitor, 137 - notification, 138 - statusCode: 500, 139 - message: "Error", 140 - incidentId: "incident-123", 141 - cronTimestamp: Date.now(), 142 - }); 134 + expect( 135 + sendAlert({ 136 + // @ts-expect-error 137 + monitor, 138 + notification, 139 + statusCode: 500, 140 + message: "Error", 141 + cronTimestamp: Date.now(), 142 + }), 143 + ).rejects.toThrow(); 143 144 144 145 expect(fetchMock).toHaveBeenCalledTimes(1); 145 146 });
+25 -26
packages/notifications/opsgenie/src/index.ts
··· 37 37 opsgenie.region === "eu" 38 38 ? "https://api.eu.opsgenie.com/v2/alerts" 39 39 : "https://api.opsgenie.com/v2/alerts"; 40 - try { 41 - await fetch(url, { 42 - method: "POST", 43 - body: JSON.stringify(event), 44 - headers: { 45 - "Content-Type": "application/json", 46 - Authorization: `GenieKey ${opsgenie.apiKey}`, 47 - }, 48 - }); 49 - } catch (err) { 50 - console.log(err); 51 - // Do something 40 + 41 + const res = await fetch(url, { 42 + method: "POST", 43 + body: JSON.stringify(event), 44 + headers: { 45 + "Content-Type": "application/json", 46 + Authorization: `GenieKey ${opsgenie.apiKey}`, 47 + }, 48 + }); 49 + if (!res.ok) { 50 + throw new Error( 51 + `Failed to send OpsGenie alert: ${res.status} ${res.statusText}`, 52 + ); 52 53 } 53 54 }; 54 55 ··· 86 87 opsgenie.region === "eu" 87 88 ? "https://api.eu.opsgenie.com/v2/alerts" 88 89 : "https://api.opsgenie.com/v2/alerts"; 89 - try { 90 - await fetch(url, { 91 - method: "POST", 92 - body: JSON.stringify(event), 93 - headers: { 94 - "Content-Type": "application/json", 95 - Authorization: `GenieKey ${opsgenie.apiKey}`, 96 - }, 97 - }); 98 - } catch (err) { 99 - console.log(err); 100 - // Do something 101 - 102 - // Do something 90 + const res = await fetch(url, { 91 + method: "POST", 92 + body: JSON.stringify(event), 93 + headers: { 94 + "Content-Type": "application/json", 95 + Authorization: `GenieKey ${opsgenie.apiKey}`, 96 + }, 97 + }); 98 + if (!res.ok) { 99 + throw new Error( 100 + `Failed to send OpsGenie degraded alert: ${res.status} ${res.statusText}`, 101 + ); 103 102 } 104 103 }; 105 104
+4 -1
packages/notifications/opsgenie/tsconfig.json
··· 1 1 { 2 2 "extends": "@openstatus/tsconfig/nextjs.json", 3 - "include": ["src", "*.ts"] 3 + "include": ["src", "*.ts"], 4 + "compilerOptions": { 5 + "types": ["bun-types"] 6 + } 4 7 }
+10 -10
packages/notifications/pagerduty/src/index.test.ts
··· 190 190 createMockNotification(), 191 191 ); 192 192 193 - // Should not throw - function catches errors internally 194 - await sendAlert({ 195 - // @ts-expect-error 196 - monitor, 197 - notification, 198 - statusCode: 500, 199 - message: "Error", 200 - incidentId: "incident-123", 201 - cronTimestamp: Date.now(), 202 - }); 193 + expect( 194 + sendAlert({ 195 + // @ts-expect-error 196 + monitor, 197 + notification, 198 + statusCode: 500, 199 + message: "Error", 200 + cronTimestamp: Date.now(), 201 + }), 202 + ).rejects.toThrow(); 203 203 204 204 expect(fetchMock).toHaveBeenCalledTimes(1); 205 205 });
+61 -64
packages/notifications/pagerduty/src/index.ts
··· 34 34 35 35 const { name } = monitor; 36 36 37 - try { 38 - for await (const integrationKey of notificationData.integration_keys) { 39 - // biome-ignore lint/correctness/noUnusedVariables: <explanation> 40 - const { integration_key, type } = integrationKey; 41 - const event = triggerEventPayloadSchema.parse({ 42 - routing_key: integration_key, 43 - dedup_key: `${monitor.id}}-${incidentId}`, 44 - event_action: "trigger", 45 - payload: { 46 - summary: `${name} is down`, 47 - source: "Open Status", 48 - severity: "error", 49 - timestamp: new Date(cronTimestamp).toISOString(), 50 - custom_details: { 51 - statusCode, 52 - message, 53 - }, 37 + for await (const integrationKey of notificationData.integration_keys) { 38 + // biome-ignore lint/correctness/noUnusedVariables: <explanation> 39 + const { integration_key, type } = integrationKey; 40 + const event = triggerEventPayloadSchema.parse({ 41 + routing_key: integration_key, 42 + dedup_key: `${monitor.id}}-${incidentId}`, 43 + event_action: "trigger", 44 + payload: { 45 + summary: `${name} is down`, 46 + source: "Open Status", 47 + severity: "error", 48 + timestamp: new Date(cronTimestamp).toISOString(), 49 + custom_details: { 50 + statusCode, 51 + message, 54 52 }, 55 - }); 56 - await fetch("https://events.pagerduty.com/v2/enqueue", { 57 - method: "POST", 58 - body: JSON.stringify(event), 59 - }); 53 + }, 54 + }); 55 + const res = await fetch("https://events.pagerduty.com/v2/enqueue", { 56 + method: "POST", 57 + body: JSON.stringify(event), 58 + }); 59 + if (!res.ok) { 60 + console.log(`Failed to send alert notification: ${res.statusText}`); 61 + throw new Error("Failed to send alert notification"); 60 62 } 61 - } catch (err) { 62 - console.log(err); 63 - // Do something 64 63 } 65 64 }; 66 65 ··· 84 83 const notificationData = PagerDutySchema.parse(JSON.parse(data.pagerduty)); 85 84 const { name } = monitor; 86 85 87 - try { 88 - for await (const integrationKey of notificationData.integration_keys) { 89 - // biome-ignore lint/correctness/noUnusedVariables: <explanation> 90 - const { integration_key, type } = integrationKey; 86 + for await (const integrationKey of notificationData.integration_keys) { 87 + // biome-ignore lint/correctness/noUnusedVariables: <explanation> 88 + const { integration_key, type } = integrationKey; 91 89 92 - const event = triggerEventPayloadSchema.parse({ 93 - routing_key: integration_key, 94 - dedup_key: `${monitor.id}}`, 95 - event_action: "trigger", 96 - payload: { 97 - summary: `${name} is degraded`, 98 - source: "Open Status", 99 - severity: "warning", 100 - timestamp: new Date().toISOString(), 101 - custom_details: { 102 - statusCode, 103 - message, 104 - }, 90 + const event = triggerEventPayloadSchema.parse({ 91 + routing_key: integration_key, 92 + dedup_key: `${monitor.id}}`, 93 + event_action: "trigger", 94 + payload: { 95 + summary: `${name} is degraded`, 96 + source: "Open Status", 97 + severity: "warning", 98 + timestamp: new Date().toISOString(), 99 + custom_details: { 100 + statusCode, 101 + message, 105 102 }, 106 - }); 103 + }, 104 + }); 107 105 108 - await fetch("https://events.pagerduty.com/v2/enqueue", { 109 - method: "POST", 110 - body: JSON.stringify(event), 111 - }); 106 + const res = await fetch("https://events.pagerduty.com/v2/enqueue", { 107 + method: "POST", 108 + body: JSON.stringify(event), 109 + }); 110 + if (!res.ok) { 111 + console.log(`Failed to send alert notification: ${res.statusText}`); 112 + throw new Error("Failed to send alert notification"); 112 113 } 113 - } catch (err) { 114 - console.log(err); 115 - // Do something 116 114 } 117 115 }; 118 116 ··· 138 136 139 137 const notificationData = PagerDutySchema.parse(JSON.parse(data.pagerduty)); 140 138 141 - try { 142 - for await (const integrationKey of notificationData.integration_keys) { 143 - const event = resolveEventPayloadSchema.parse({ 144 - routing_key: integrationKey.integration_key, 145 - dedup_key: `${monitor.id}}-${incidentId}`, 146 - event_action: "resolve", 147 - }); 148 - await fetch("https://events.pagerduty.com/v2/enqueue", { 149 - method: "POST", 150 - body: JSON.stringify(event), 151 - }); 139 + for await (const integrationKey of notificationData.integration_keys) { 140 + const event = resolveEventPayloadSchema.parse({ 141 + routing_key: integrationKey.integration_key, 142 + dedup_key: `${monitor.id}}-${incidentId}`, 143 + event_action: "resolve", 144 + }); 145 + const res = await fetch("https://events.pagerduty.com/v2/enqueue", { 146 + method: "POST", 147 + body: JSON.stringify(event), 148 + }); 149 + if (!res.ok) { 150 + console.log(`Failed to send alert notification: ${res.statusText}`); 151 + throw new Error("Failed to send alert notification"); 152 152 } 153 - } catch (err) { 154 - console.log(err); 155 - // Do something 156 153 } 157 154 }; 158 155
+10 -8
packages/notifications/slack/src/index.test.ts
··· 177 177 ); 178 178 179 179 // Should not throw - function catches errors internally 180 - await sendAlert({ 181 - // @ts-expect-error 182 - monitor, 183 - notification, 184 - statusCode: 500, 185 - message: "Error", 186 - cronTimestamp: Date.now(), 187 - }); 180 + expect( 181 + sendAlert({ 182 + // @ts-expect-error 183 + monitor, 184 + notification, 185 + statusCode: 500, 186 + message: "Error", 187 + cronTimestamp: Date.now(), 188 + }), 189 + ).rejects.toThrow(); 188 190 189 191 expect(fetchMock).toHaveBeenCalledTimes(1); 190 192 });
+77 -94
packages/notifications/slack/src/index.ts
··· 4 4 5 5 // biome-ignore lint/suspicious/noExplicitAny: <explanation> 6 6 const postToWebhook = async (body: any, webhookUrl: string) => { 7 - try { 8 - await fetch(webhookUrl, { 9 - method: "POST", 10 - body: JSON.stringify(body), 11 - }); 12 - } catch (e) { 13 - console.log(e); 14 - throw e; 7 + const res = await fetch(webhookUrl, { 8 + method: "POST", 9 + body: JSON.stringify(body), 10 + }); 11 + if (!res.ok) { 12 + throw new Error(`Failed to send Slack notification: ${res.statusText}`); 15 13 } 16 14 }; 17 15 ··· 37 35 const { slack: webhookUrl } = notificationData; // webhook url 38 36 const { name } = monitor; 39 37 40 - try { 41 - await postToWebhook( 42 - { 43 - blocks: [ 44 - { 45 - type: "divider", 46 - }, 47 - { 48 - type: "section", 49 - text: { 50 - type: "mrkdwn", 51 - text: ` 38 + await postToWebhook( 39 + { 40 + blocks: [ 41 + { 42 + type: "divider", 43 + }, 44 + { 45 + type: "section", 46 + text: { 47 + type: "mrkdwn", 48 + text: ` 52 49 *🚨 Alert <${monitor.url}|${name}>*\n\n 53 50 Status Code: ${statusCode || "_empty_"}\n 54 51 Message: ${message || "_empty_"}\n 55 52 Cron Timestamp: ${cronTimestamp} (${new Date(cronTimestamp).toISOString()}) 56 53 `, 57 - }, 58 54 }, 59 - { 60 - type: "context", 61 - elements: [ 62 - { 63 - type: "mrkdwn", 64 - text: "Check your <https://www.openstatus.dev/app|Dashboard>.", 65 - }, 66 - ], 67 - }, 68 - ], 69 - }, 70 - webhookUrl, 71 - ); 72 - } catch (err) { 73 - console.log(err); 74 - // Do something 75 - } 55 + }, 56 + { 57 + type: "context", 58 + elements: [ 59 + { 60 + type: "mrkdwn", 61 + text: "Check your <https://www.openstatus.dev/app|Dashboard>.", 62 + }, 63 + ], 64 + }, 65 + ], 66 + }, 67 + webhookUrl, 68 + ); 76 69 }; 77 70 78 71 export const sendRecovery = async ({ ··· 98 91 const { slack: webhookUrl } = notificationData; // webhook url 99 92 const { name } = monitor; 100 93 101 - try { 102 - await postToWebhook( 103 - { 104 - blocks: [ 105 - { 106 - type: "divider", 94 + await postToWebhook( 95 + { 96 + blocks: [ 97 + { 98 + type: "divider", 99 + }, 100 + { 101 + type: "section", 102 + text: { 103 + type: "mrkdwn", 104 + text: `*✅ Recovered <${monitor.url}/|${name}>*`, 107 105 }, 108 - { 109 - type: "section", 110 - text: { 106 + }, 107 + { 108 + type: "context", 109 + elements: [ 110 + { 111 111 type: "mrkdwn", 112 - text: `*✅ Recovered <${monitor.url}/|${name}>*`, 112 + text: "Check your <https://www.openstatus.dev/app|Dashboard>.", 113 113 }, 114 - }, 115 - { 116 - type: "context", 117 - elements: [ 118 - { 119 - type: "mrkdwn", 120 - text: "Check your <https://www.openstatus.dev/app|Dashboard>.", 121 - }, 122 - ], 123 - }, 124 - ], 125 - }, 126 - webhookUrl, 127 - ); 128 - } catch (err) { 129 - console.log(err); 130 - // Do something 131 - } 114 + ], 115 + }, 116 + ], 117 + }, 118 + webhookUrl, 119 + ); 132 120 }; 133 121 134 122 export const sendDegraded = async ({ ··· 151 139 const { slack: webhookUrl } = notificationData; // webhook url 152 140 const { name } = monitor; 153 141 154 - try { 155 - await postToWebhook( 156 - { 157 - blocks: [ 158 - { 159 - type: "divider", 142 + await postToWebhook( 143 + { 144 + blocks: [ 145 + { 146 + type: "divider", 147 + }, 148 + { 149 + type: "section", 150 + text: { 151 + type: "mrkdwn", 152 + text: `*⚠️ Degraded <${monitor.url}/|${name}>*`, 160 153 }, 161 - { 162 - type: "section", 163 - text: { 154 + }, 155 + { 156 + type: "context", 157 + elements: [ 158 + { 164 159 type: "mrkdwn", 165 - text: `*⚠️ Degraded <${monitor.url}/|${name}>*`, 160 + text: "Check your <https://www.openstatus.dev/app|Dashboard>.", 166 161 }, 167 - }, 168 - { 169 - type: "context", 170 - elements: [ 171 - { 172 - type: "mrkdwn", 173 - text: "Check your <https://www.openstatus.dev/app|Dashboard>.", 174 - }, 175 - ], 176 - }, 177 - ], 178 - }, 179 - webhookUrl, 180 - ); 181 - } catch (err) { 182 - console.log(err); 183 - // Do something 184 - } 162 + ], 163 + }, 164 + ], 165 + }, 166 + webhookUrl, 167 + ); 185 168 }; 186 169 187 170 export const sendTestSlackMessage = async (webhookUrl: string) => {
+21 -18
packages/notifications/telegram/src/index.test.ts
··· 9 9 10 10 beforeEach(() => { 11 11 process.env.TELEGRAM_BOT_TOKEN = "test-bot-token-123"; 12 + // @ts-expect-error 12 13 fetchMock = spyOn(global, "fetch").mockImplementation(() => 13 14 Promise.resolve(new Response(null, { status: 200 })), 14 15 ); ··· 172 173 createMockNotification(), 173 174 ); 174 175 175 - // Should not throw - function catches errors internally 176 - await sendAlert({ 177 - // @ts-expect-error 178 - monitor, 179 - notification, 180 - statusCode: 500, 181 - message: "Error", 182 - cronTimestamp: Date.now(), 183 - }); 176 + expect( 177 + sendAlert({ 178 + // @ts-expect-error 179 + monitor, 180 + notification, 181 + statusCode: 500, 182 + message: "Error", 183 + cronTimestamp: Date.now(), 184 + }), 185 + ).rejects.toThrow(); 184 186 185 187 expect(fetchMock).toHaveBeenCalledTimes(1); 186 188 }); ··· 193 195 createMockNotification(), 194 196 ); 195 197 196 - await sendAlert({ 197 - // @ts-expect-error 198 - monitor, 199 - notification, 200 - statusCode: 500, 201 - message: "Error", 202 - cronTimestamp: Date.now(), 203 - }); 204 - 198 + expect( 199 + sendAlert({ 200 + // @ts-expect-error 201 + monitor, 202 + notification, 203 + statusCode: 500, 204 + message: "Error", 205 + cronTimestamp: Date.now(), 206 + }), 207 + ).rejects.toThrow(); 205 208 // Should not call fetch when token is missing 206 209 expect(fetchMock).not.toHaveBeenCalled(); 207 210 });
+17 -28
packages/notifications/telegram/src/index.ts
··· 29 29 statusCode ? `status code ${statusCode}` : `error: ${message}` 30 30 }`; 31 31 32 - try { 33 - await sendMessage({ 34 - chatId: notificationData.telegram.chatId, 35 - message: body, 36 - }); 37 - } catch (err) { 38 - console.log(err); 39 - // Do something 40 - } 32 + await sendMessage({ 33 + chatId: notificationData.telegram.chatId, 34 + message: body, 35 + }); 41 36 }; 42 37 43 38 export const sendRecovery = async ({ ··· 65 60 const { name } = monitor; 66 61 67 62 const body = `Your monitor ${name} / ${monitor.url} is up again`; 68 - try { 69 - await sendMessage({ 70 - chatId: notificationData.telegram.chatId, 71 - message: body, 72 - }); 73 - } catch (err) { 74 - console.log(err); 75 - // Do something 76 - } 63 + await sendMessage({ 64 + chatId: notificationData.telegram.chatId, 65 + message: body, 66 + }); 77 67 }; 78 68 79 69 export const sendDegraded = async ({ ··· 100 90 101 91 const body = `Your monitor ${name} / ${monitor.url} is degraded `; 102 92 103 - try { 104 - await sendMessage({ 105 - chatId: notificationData.telegram.chatId, 106 - message: body, 107 - }); 108 - } catch (err) { 109 - console.log(err); 110 - // Do something 111 - } 93 + await sendMessage({ 94 + chatId: notificationData.telegram.chatId, 95 + message: body, 96 + }); 112 97 }; 113 98 114 99 export const sendTest = async ({ chatId }: { chatId: string }) => { ··· 134 119 if (!process.env.TELEGRAM_BOT_TOKEN) { 135 120 throw new Error("TELEGRAM_BOT_TOKEN is not set"); 136 121 } 137 - return fetch( 122 + const res = await fetch( 138 123 `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage?chat_id=${chatId}&text=${message}`, 139 124 ); 125 + if (!res.ok) { 126 + throw new Error(`Failed to send telegram message: ${res.statusText}`); 127 + } 128 + return res; 140 129 }
+4 -1
packages/notifications/telegram/tsconfig.json
··· 1 1 { 2 2 "extends": "@openstatus/tsconfig/nextjs.json", 3 - "include": ["src", "*.ts"] 3 + "include": ["src", "*.ts"], 4 + "compilerOptions": { 5 + "types": ["bun-types"] 6 + } 4 7 }
+11 -9
packages/notifications/twillio-sms/src/index.test.ts
··· 13 13 beforeEach(() => { 14 14 process.env.TWILLIO_ACCOUNT_ID = "test-account-id"; 15 15 process.env.TWILLIO_AUTH_TOKEN = "test-auth-token"; 16 + // @ts-expect-error 16 17 fetchMock = spyOn(global, "fetch").mockImplementation(() => 17 18 Promise.resolve(new Response(null, { status: 200 })), 18 19 ); ··· 162 163 createMockNotification(), 163 164 ); 164 165 165 - // Should not throw - function catches errors internally 166 - await sendAlert({ 167 - // @ts-expect-error 168 - monitor, 169 - notification, 170 - statusCode: 500, 171 - message: "Error", 172 - cronTimestamp: Date.now(), 173 - }); 166 + expect( 167 + sendAlert({ 168 + // @ts-expect-error 169 + monitor, 170 + notification, 171 + statusCode: 500, 172 + message: "Error", 173 + cronTimestamp: Date.now(), 174 + }), 175 + ).rejects.toThrow(); 174 176 175 177 expect(fetchMock).toHaveBeenCalledTimes(1); 176 178 });
+39 -45
packages/notifications/twillio-sms/src/index.ts
··· 34 34 }`, 35 35 ); 36 36 37 - try { 38 - await fetch( 39 - `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, 40 - { 41 - method: "post", 42 - body, 43 - headers: { 44 - Authorization: `Basic ${btoa( 45 - `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, 46 - )}`, 47 - }, 37 + const res = await fetch( 38 + `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, 39 + { 40 + method: "post", 41 + body, 42 + headers: { 43 + Authorization: `Basic ${btoa( 44 + `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, 45 + )}`, 48 46 }, 49 - ); 50 - } catch (err) { 51 - console.log(err); 52 - // Do something 47 + }, 48 + ); 49 + if (!res.ok) { 50 + throw new Error(`Failed to send SMS: ${res.statusText}`); 53 51 } 54 52 }; 55 53 ··· 80 78 body.set("From", "+14807252613"); 81 79 body.set("Body", `Your monitor ${name} / ${monitor.url} is up again`); 82 80 83 - try { 84 - await fetch( 85 - `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, 86 - { 87 - method: "post", 88 - body, 89 - headers: { 90 - Authorization: `Basic ${btoa( 91 - `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, 92 - )}`, 93 - }, 81 + const res = await fetch( 82 + `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, 83 + { 84 + method: "post", 85 + body, 86 + headers: { 87 + Authorization: `Basic ${btoa( 88 + `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, 89 + )}`, 94 90 }, 95 - ); 96 - } catch (err) { 97 - console.log(err); 98 - // Do something 91 + }, 92 + ); 93 + if (!res.ok) { 94 + throw new Error(`Failed to send SMS: ${res.statusText}`); 99 95 } 100 96 }; 101 97 ··· 124 120 body.set("From", "+14807252613"); 125 121 body.set("Body", `Your monitor ${name} / ${monitor.url} is degraded `); 126 122 127 - try { 128 - await fetch( 129 - `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, 130 - { 131 - method: "post", 132 - body, 133 - headers: { 134 - Authorization: `Basic ${btoa( 135 - `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, 136 - )}`, 137 - }, 123 + const res = await fetch( 124 + `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, 125 + { 126 + method: "post", 127 + body, 128 + headers: { 129 + Authorization: `Basic ${btoa( 130 + `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, 131 + )}`, 138 132 }, 139 - ); 140 - } catch (err) { 141 - console.log(err); 142 - // Do something 133 + }, 134 + ); 135 + if (!res.ok) { 136 + throw new Error(`Failed to send SMS: ${res.statusText}`); 143 137 } 144 138 };
+39 -45
packages/notifications/twillio-whatsapp/src/index.ts
··· 31 31 body.set("ContentSid", "HX8282106bfaecb7939e69f9c5564babe5"); 32 32 body.set("ContentVariables", contentVariables); 33 33 34 - try { 35 - await fetch( 36 - `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, 37 - { 38 - method: "post", 39 - body, 40 - headers: { 41 - Authorization: `Basic ${btoa( 42 - `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, 43 - )}`, 44 - }, 34 + const res = await fetch( 35 + `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, 36 + { 37 + method: "post", 38 + body, 39 + headers: { 40 + Authorization: `Basic ${btoa( 41 + `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, 42 + )}`, 45 43 }, 46 - ); 47 - } catch (err) { 48 - console.log(err); 49 - // Do something 44 + }, 45 + ); 46 + if (!res.ok) { 47 + throw new Error(`Failed to send WhatsApp message: ${res.statusText}`); 50 48 } 51 49 }; 52 50 ··· 80 78 body.set("ContentSid", "HX8fdeb4201bed18ac8838b3c0135bbf28"); 81 79 body.set("ContentVariables", contentVariables); 82 80 83 - try { 84 - await fetch( 85 - `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, 86 - { 87 - method: "post", 88 - body, 89 - headers: { 90 - Authorization: `Basic ${btoa( 91 - `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, 92 - )}`, 93 - }, 81 + const res = await fetch( 82 + `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, 83 + { 84 + method: "post", 85 + body, 86 + headers: { 87 + Authorization: `Basic ${btoa( 88 + `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, 89 + )}`, 94 90 }, 95 - ); 96 - } catch (err) { 97 - console.log(err); 98 - // Do something 91 + }, 92 + ); 93 + if (!res.ok) { 94 + throw new Error(`Failed to send SMS: ${res.statusText}`); 99 95 } 100 96 }; 101 97 ··· 127 123 body.set("ContentSid", "HX35589f2e7ac8b8be63f4bd62a60e435f"); 128 124 body.set("ContentVariables", contentVariables); 129 125 130 - try { 131 - await fetch( 132 - `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, 133 - { 134 - method: "post", 135 - body, 136 - headers: { 137 - Authorization: `Basic ${btoa( 138 - `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, 139 - )}`, 140 - }, 126 + const res = await fetch( 127 + `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, 128 + { 129 + method: "post", 130 + body, 131 + headers: { 132 + Authorization: `Basic ${btoa( 133 + `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, 134 + )}`, 141 135 }, 142 - ); 143 - } catch (err) { 144 - console.log(err); 145 - // Do something 136 + }, 137 + ); 138 + if (!res.ok) { 139 + throw new Error(`Failed to send SMS: ${res.statusText}`); 146 140 } 147 141 }; 148 142
+33 -39
packages/notifications/webhook/src/index.ts
··· 34 34 errorMessage: message, 35 35 }); 36 36 37 - try { 38 - await fetch(notificationData.webhook.endpoint, { 39 - method: "post", 40 - body: JSON.stringify(body), 41 - headers: notificationData.webhook.headers 42 - ? transformHeaders(notificationData.webhook.headers) 43 - : { 44 - "Content-Type": "application/json", 45 - }, 46 - }); 47 - } catch (err) { 48 - console.log(err); 49 - // Do something 37 + const res = await fetch(notificationData.webhook.endpoint, { 38 + method: "post", 39 + body: JSON.stringify(body), 40 + headers: notificationData.webhook.headers 41 + ? transformHeaders(notificationData.webhook.headers) 42 + : { 43 + "Content-Type": "application/json", 44 + }, 45 + }); 46 + if (!res.ok) { 47 + throw new Error(`Failed to send webhook notification: ${res.statusText}`); 50 48 } 51 49 }; 52 50 ··· 80 78 errorMessage: message, 81 79 }); 82 80 const url = notificationData.webhook.endpoint; 83 - try { 84 - await fetch(url, { 85 - method: "post", 86 - body: JSON.stringify(body), 87 - headers: notificationData.webhook.headers 88 - ? transformHeaders(notificationData.webhook.headers) 89 - : { 90 - "Content-Type": "application/json", 91 - }, 92 - }); 93 - } catch (err) { 94 - console.log(err); 95 - // Do something 81 + const res = await fetch(url, { 82 + method: "post", 83 + body: JSON.stringify(body), 84 + headers: notificationData.webhook.headers 85 + ? transformHeaders(notificationData.webhook.headers) 86 + : { 87 + "Content-Type": "application/json", 88 + }, 89 + }); 90 + if (!res.ok) { 91 + throw new Error(`Failed to send SMS: ${res.statusText}`); 96 92 } 97 93 }; 98 94 ··· 124 120 errorMessage: message, 125 121 }); 126 122 127 - try { 128 - await fetch(notificationData.webhook.endpoint, { 129 - method: "post", 130 - body: JSON.stringify(body), 131 - headers: notificationData.webhook.headers 132 - ? transformHeaders(notificationData.webhook.headers) 133 - : { 134 - "Content-Type": "application/json", 135 - }, 136 - }); 137 - } catch (err) { 138 - console.log(err); 139 - // Do something 123 + const res = await fetch(notificationData.webhook.endpoint, { 124 + method: "post", 125 + body: JSON.stringify(body), 126 + headers: notificationData.webhook.headers 127 + ? transformHeaders(notificationData.webhook.headers) 128 + : { 129 + "Content-Type": "application/json", 130 + }, 131 + }); 132 + if (!res.ok) { 133 + throw new Error(`Failed to send SMS: ${res.statusText}`); 140 134 } 141 135 }; 142 136
+31 -2
pnpm-lock.yaml
··· 1118 1118 drizzle-orm: 1119 1119 specifier: 0.44.4 1120 1120 version: 0.44.4(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(bun-types@1.3.4) 1121 + effect: 1122 + specifier: 3.19.12 1123 + version: 3.19.12 1121 1124 hono: 1122 1125 specifier: 4.5.3 1123 1126 version: 4.5.3 ··· 6286 6289 '@socket.io/component-emitter@3.1.2': 6287 6290 resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} 6288 6291 6292 + '@standard-schema/spec@1.1.0': 6293 + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} 6294 + 6289 6295 '@standard-schema/utils@0.3.0': 6290 6296 resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} 6291 6297 ··· 7946 7952 7947 7953 ee-first@1.1.1: 7948 7954 resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} 7955 + 7956 + effect@3.19.12: 7957 + resolution: {integrity: sha512-7F9RGTrCTC3D7nh9Zw+3VlJWwZgo5k33KA+476BAaD0rKIXKZsY/jQ+ipyhR/Avo239Fi6GqAVFs1mqM1IJ7yg==} 7949 7958 7950 7959 electron-to-chromium@1.5.260: 7951 7960 resolution: {integrity: sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA==} ··· 8170 8179 resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} 8171 8180 engines: {node: '>=4'} 8172 8181 8182 + fast-check@3.23.2: 8183 + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} 8184 + engines: {node: '>=8.0.0'} 8185 + 8173 8186 fast-deep-equal@2.0.1: 8174 8187 resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} 8175 8188 ··· 10146 10159 punycode@2.3.1: 10147 10160 resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 10148 10161 engines: {node: '>=6'} 10162 + 10163 + pure-rand@6.1.0: 10164 + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} 10149 10165 10150 10166 qs@6.14.0: 10151 10167 resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} ··· 16210 16226 json5: 2.2.3 16211 16227 log-symbols: 4.1.0 16212 16228 module-punycode: punycode@2.3.1 16213 - next: 15.5.2(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 16229 + next: 15.5.2(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 16214 16230 node-html-parser: 7.0.1 16215 16231 ora: 5.4.1 16216 16232 pretty-bytes: 6.1.1 ··· 17008 17024 p-map: 4.0.0 17009 17025 17010 17026 '@socket.io/component-emitter@3.1.2': {} 17027 + 17028 + '@standard-schema/spec@1.1.0': {} 17011 17029 17012 17030 '@standard-schema/utils@0.3.0': {} 17013 17031 ··· 18737 18755 semver: 7.7.3 18738 18756 18739 18757 ee-first@1.1.1: {} 18758 + 18759 + effect@3.19.12: 18760 + dependencies: 18761 + '@standard-schema/spec': 1.1.0 18762 + fast-check: 3.23.2 18740 18763 18741 18764 electron-to-chromium@1.5.260: {} 18742 18765 ··· 19084 19107 iconv-lite: 0.4.24 19085 19108 tmp: 0.0.33 19086 19109 19110 + fast-check@3.23.2: 19111 + dependencies: 19112 + pure-rand: 6.1.0 19113 + 19087 19114 fast-deep-equal@2.0.1: {} 19088 19115 19089 19116 fast-deep-equal@3.1.3: {} ··· 20936 20963 react: 19.2.2 20937 20964 react-dom: 19.2.2(react@19.2.2) 20938 20965 20939 - next@15.5.2(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.2.2))(react@19.0.0): 20966 + next@15.5.2(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): 20940 20967 dependencies: 20941 20968 '@next/env': 15.5.2 20942 20969 '@swc/helpers': 0.5.15 ··· 21564 21591 proxy-from-env@1.1.0: {} 21565 21592 21566 21593 punycode@2.3.1: {} 21594 + 21595 + pure-rand@6.1.0: {} 21567 21596 21568 21597 qs@6.14.0: 21569 21598 dependencies: