Openstatus www.openstatus.dev

๐Ÿ’Œ improve email (#440)

* ๐Ÿ’Œ improve email

* ๐Ÿ’Œ improve email

* ๐Ÿ’Œ improve email

* ๐Ÿ’Œ improve email

* ๐Ÿ’Œ improve email

* ๐Ÿ’Œ improve email

authored by

Thibault Le Ouay and committed by
GitHub
c44918de d93391c7

+128 -66
+1
apps/server/package.json
··· 20 20 "@openstatus/plans": "workspace:*", 21 21 "@openstatus/tinybird": "workspace:*", 22 22 "@openstatus/upstash": "workspace:*", 23 + "@openstatus/utils": "workspace:*", 23 24 "@t3-oss/env-core": "0.7.1", 24 25 "@unkey/api": "0.11.0", 25 26 "hono": "3.9.1",
+2 -2
apps/server/src/checker/alerting.test.ts
··· 2 2 3 3 import { triggerAlerting } from "./alerting"; 4 4 5 - test("should send email notification", async () => { 5 + test.todo("should send email notification", async () => { 6 6 const fn = mock(() => {}); 7 7 mock.module("./utils.ts", () => { 8 8 return { ··· 11 11 }, 12 12 }; 13 13 }); 14 - await triggerAlerting({ monitorId: "1" }); 14 + await triggerAlerting({ monitorId: "1", region: "ams", statusCode: 400 }); 15 15 expect(fn).toHaveBeenCalled(); 16 16 });
+13 -14
apps/server/src/checker/alerting.ts
··· 4 4 selectMonitorSchema, 5 5 selectNotificationSchema, 6 6 } from "@openstatus/db/src/schema"; 7 + import type { flyRegions } from "@openstatus/utils"; 8 + import { flyRegionsDict } from "@openstatus/utils"; 7 9 8 - import { publishPingRetryPolicy } from "./checker"; 9 - import type { Payload } from "./schema"; 10 10 import { providerToFunction } from "./utils"; 11 11 12 - export async function catchTooManyRetry(payload: Payload) { 13 - await publishPingRetryPolicy({ payload, latency: -1, statusCode: 500 }); 14 - if (payload?.status !== "error") { 15 - await triggerAlerting({ monitorId: payload.monitorId }); 16 - await updateMonitorStatus({ 17 - monitorId: payload.monitorId, 18 - status: "error", 19 - }); 20 - } 21 - } 22 - 23 - export const triggerAlerting = async ({ monitorId }: { monitorId: string }) => { 12 + export const triggerAlerting = async ({ 13 + monitorId, 14 + region, 15 + statusCode, 16 + }: { 17 + monitorId: string; 18 + region: keyof typeof flyRegionsDict; 19 + statusCode: number; 20 + }) => { 24 21 console.log(`triggerAlerting for ${monitorId}`); 25 22 const notifications = await db 26 23 .select() ··· 40 37 await providerToFunction[notif.notification.provider]({ 41 38 monitor, 42 39 notification: selectNotificationSchema.parse(notif.notification), 40 + region: flyRegionsDict[region].location, 41 + statusCode, 43 42 }); 44 43 } 45 44 };
+6 -1
apps/server/src/checker/checker.ts
··· 1 + import { env } from "../env"; 1 2 import { triggerAlerting, updateMonitorStatus } from "./alerting"; 2 3 import type { PublishPingType } from "./ping"; 3 4 import { pingEndpoint, publishPing } from "./ping"; ··· 78 79 monitorId: data.monitorId, 79 80 status: "error", 80 81 }); 81 - await triggerAlerting({ monitorId: data.monitorId }); 82 + await triggerAlerting({ 83 + monitorId: data.monitorId, 84 + region: env.FLY_REGION, 85 + statusCode: res?.status || 0, 86 + }); 82 87 } 83 88 } 84 89 }
-1
apps/server/src/checker/index.ts
··· 34 34 `catchTooManyRetry for ${JSON.stringify(result.data)} 35 35 )}`, 36 36 ); 37 - // catchTooManyRetry(result.data); 38 37 return c.text("Ok", 200); // finish the task 39 38 } 40 39
+5
apps/server/src/checker/utils.ts
··· 6 6 import { sendDiscordMessage } from "@openstatus/notification-discord"; 7 7 import { send as sendEmail } from "@openstatus/notification-emails"; 8 8 import { sendSlackMessage } from "@openstatus/notification-slack"; 9 + import type { flyRegionsDict } from "@openstatus/utils"; 9 10 10 11 type SendNotification = ({ 11 12 monitor, 12 13 notification, 14 + region, 15 + statusCode, 13 16 }: { 14 17 monitor: Monitor; 15 18 notification: Notification; 19 + region: keyof typeof flyRegionsDict; 20 + statusCode: number; 16 21 }) => Promise<void>; 17 22 18 23 export const providerToFunction = {
+3 -1
apps/server/src/env.ts
··· 1 1 import { createEnv } from "@t3-oss/env-core"; 2 2 import { z } from "zod"; 3 3 4 + import { flyRegions } from "@openstatus/utils"; 5 + 4 6 export const env = createEnv({ 5 7 server: { 6 8 UNKEY_API_ID: z.string().min(1), ··· 8 10 TINY_BIRD_API_KEY: z.string().min(1), 9 11 UPSTASH_REDIS_REST_URL: z.string().min(1), 10 12 UPSTASH_REDIS_REST_TOKEN: z.string().min(1), 11 - FLY_REGION: z.string(), 13 + FLY_REGION: z.enum(flyRegions), 12 14 CRON_SECRET: z.string(), 13 15 }, 14 16
+2 -1
apps/web/package.json
··· 18 18 "@openstatus/api": "workspace:*", 19 19 "@openstatus/db": "workspace:*", 20 20 "@openstatus/emails": "workspace:*", 21 - "@openstatus/notification-emails": "workspace:*", 22 21 "@openstatus/notification-discord": "workspace:*", 22 + "@openstatus/notification-emails": "workspace:*", 23 23 "@openstatus/notification-slack": "workspace:*", 24 24 "@openstatus/plans": "workspace:*", 25 25 "@openstatus/react": "workspace:*", 26 26 "@openstatus/tinybird": "workspace:*", 27 27 "@openstatus/ui": "workspace:*", 28 28 "@openstatus/upstash": "workspace:*", 29 + "@openstatus/utils": "workspace:*", 29 30 "@openstatus/vercel": "workspace:*", 30 31 "@sentry/integrations": "7.73.0", 31 32 "@sentry/nextjs": "7.73.0",
+4 -5
apps/web/src/app/api/checker/regions/_checker.ts
··· 16 16 import { env } from "@/env"; 17 17 import type { Payload } from "../schema"; 18 18 import { payloadSchema } from "../schema"; 19 - import { providerToFunction } from "../utils"; 20 19 21 20 export const monitorSchema = tbIngestPingResponse.pick({ 22 21 url: true, ··· 147 146 .where(eq(schema.monitor.id, Number(monitorId))) 148 147 .all(); 149 148 for (const notif of notifications) { 150 - await providerToFunction[notif.notification.provider]({ 151 - monitor: selectMonitorSchema.parse(notif.monitor), 152 - notification: selectNotificationSchema.parse(notif.notification), 153 - }); 149 + // await providerToFunction[notif.notification.provider]({ 150 + // monitor: selectMonitorSchema.parse(notif.monitor), 151 + // notification: selectNotificationSchema.parse(notif.notification), 152 + // }); 154 153 } 155 154 }; 156 155
+12 -12
apps/web/src/app/api/checker/utils.ts
··· 7 7 import { send as sendEmail } from "@openstatus/notification-emails"; 8 8 import { sendSlackMessage } from "@openstatus/notification-slack"; 9 9 10 - type sendNotificationType = ({ 11 - monitor, 12 - notification, 13 - }: { 14 - monitor: Monitor; 15 - notification: Notification; 16 - }) => Promise<void>; 10 + // type sendNotificationType = ({ 11 + // monitor, 12 + // notification, 13 + // }: { 14 + // monitor: Monitor; 15 + // notification: Notification; 16 + // }) => Promise<void>; 17 17 18 - export const providerToFunction = { 19 - email: sendEmail, 20 - slack: sendSlackMessage, 21 - discord: sendDiscordMessage, 22 - } satisfies Record<NotificationProvider, sendNotificationType>; 18 + // export const providerToFunction = { 19 + // email: sendEmail, 20 + // slack: sendSlackMessage, 21 + // discord: sendDiscordMessage, 22 + // } satisfies Record<NotificationProvider, sendNotificationType>;
-1
apps/web/src/app/layout.tsx
··· 3 3 import type { Metadata } from "next"; 4 4 import { Inter } from "next/font/google"; 5 5 import LocalFont from "next/font/local"; 6 - import { ClerkProvider } from "@clerk/nextjs"; 7 6 import PlausibleProvider from "next-plausible"; 8 7 9 8 import { Toaster } from "@openstatus/ui";
-2
apps/web/src/components/content/timeline.tsx
··· 1 - import Image from "next/image"; 2 - 3 1 import { formatDate } from "@/lib/utils"; 4 2 5 3 interface TimelineProps {
-1
apps/web/src/components/dashboard/container.tsx
··· 2 2 Card, 3 3 CardContent, 4 4 CardDescription, 5 - CardFooter, 6 5 CardHeader, 7 6 CardTitle, 8 7 Skeleton,
+1 -1
apps/web/src/components/data-table/columns.tsx
··· 4 4 import { format } from "date-fns"; 5 5 6 6 import type { Ping } from "@openstatus/tinybird"; 7 + import { regionsDict } from "@openstatus/utils"; 7 8 8 - import { regionsDict } from "@/data/regions-dictionary"; 9 9 import { DataTableColumnHeader } from "./data-table-column-header"; 10 10 import { DataTableStatusBadge } from "./data-table-status-badge"; 11 11
+1 -3
apps/web/src/components/data-table/data-table-toolbar.tsx
··· 4 4 import { X } from "lucide-react"; 5 5 6 6 import { Button } from "@openstatus/ui"; 7 + import { flyRegionsDict } from "@openstatus/utils"; 7 8 8 9 import { codesDict } from "@/data/code-dictionary"; 9 - import { flyRegionsDict } from "@/data/regions-dictionary"; 10 - import { DataTableDateRangePicker } from "./data-table-date-ranger-picker"; 11 10 import { DataTableFacetedFilter } from "./data-table-faceted-filter"; 12 - import { DataTableFilterInput } from "./data-table-filter-input"; 13 11 14 12 interface DataTableToolbarProps<TData> { 15 13 table: Table<TData>;
-1
apps/web/src/components/data-table/status-page/columns.tsx
··· 7 7 import type { Page } from "@openstatus/db/src/schema"; 8 8 import { Badge } from "@openstatus/ui"; 9 9 10 - import { formatDate } from "@/lib/utils"; 11 10 import { DataTableRowActions } from "./data-table-row-actions"; 12 11 13 12 // TODO: add total number of monitors
-6
apps/web/src/components/domains/domain-configuration.tsx
··· 1 1 "use client"; 2 2 3 - import { useEffect, useState, useTransition } from "react"; 4 - 5 - import type { 6 - DomainResponse, 7 - DomainVerificationStatusProps, 8 - } from "@openstatus/api/src/router/domain"; 9 3 import { Tabs, TabsContent, TabsList, TabsTrigger } from "@openstatus/ui"; 10 4 11 5 import { useDomainStatus } from "@/hooks/use-domain-status";
+1 -1
apps/web/src/components/forms/monitor-form.tsx
··· 60 60 TooltipProvider, 61 61 TooltipTrigger, 62 62 } from "@openstatus/ui"; 63 + import { flyRegionsDict } from "@openstatus/utils"; 63 64 64 65 import { LoadingAnimation } from "@/components/loading-animation"; 65 66 import { FailedPingAlertConfirmation } from "@/components/modals/failed-ping-alert-confirmation"; 66 - import { flyRegionsDict } from "@/data/regions-dictionary"; 67 67 import { useToastAction } from "@/hooks/use-toast-action"; 68 68 import useUpdateSearchParams from "@/hooks/use-update-search-params"; 69 69 import { cn } from "@/lib/utils";
+1 -1
apps/web/src/components/incidents/affected-monitors.tsx
··· 11 11 return ( 12 12 <ul role="list" className="flex gap-2"> 13 13 {monitors.length > 0 ? ( 14 - monitors.map(({ name, url }, i) => ( 14 + monitors.map(({ name }, i) => ( 15 15 <li key={i} className="text-xs"> 16 16 <Badge variant="secondary">{name}</Badge> 17 17 </li>
+1 -3
apps/web/src/components/marketing/cards.tsx
··· 1 - import Link from "next/link"; 2 - 3 - import { Badge, Button } from "@openstatus/ui"; 1 + import { Badge } from "@openstatus/ui"; 4 2 5 3 import type { Feature, SpecialFeature } from "@/config/features"; 6 4 import { Shell } from "../dashboard/shell";
+2 -1
apps/web/src/components/marketing/stats.tsx
··· 1 1 import { Shell } from "@/components/dashboard/shell"; 2 2 import { getHomeStatsData } from "@/lib/tb"; 3 3 import { numberFormatter } from "@/lib/utils"; 4 - import { api } from "@/trpc/server"; 4 + 5 + // import { api } from "@/trpc/server"; 5 6 6 7 export async function Stats() { 7 8 const tbTotalStats = await getHomeStatsData({});
+1 -5
apps/web/src/components/modals/notification-dialog.tsx
··· 14 14 15 15 import { NotificationForm } from "@/components/forms/notification-form"; 16 16 17 - export const NotificationDialog = ({ 18 - workspaceSlug, 19 - }: { 20 - workspaceSlug: string; 21 - }) => { 17 + export const NotificationDialog = ({}: { workspaceSlug: string }) => { 22 18 const [open, setOpen] = useState(false); 23 19 return ( 24 20 <Dialog open={open} onOpenChange={(val) => setOpen(val)}>
+23
apps/web/src/data/regions-dictionary.ts packages/utils/index.ts
··· 137 137 }, 138 138 } as const; 139 139 140 + export const vercelRegions = [ 141 + "arn1", 142 + "bom1", 143 + "cdg1", 144 + "cle1", 145 + "cpt1", 146 + "dub1", 147 + "fra1", 148 + "gru1", 149 + "hkg1", 150 + "hnd1", 151 + "iad1", 152 + "icn1", 153 + "kix1", 154 + "lhr1", 155 + "pdx1", 156 + "sfo1", 157 + "sin1", 158 + "syd1", 159 + ] as const; 160 + 161 + export const flyRegions = ["ams", "iad", "hkg", "jnb", "syd", "gru"] as const; 162 + 140 163 export const regionsDict = { ...vercelRegionsDict, ...flyRegionsDict } as const;
+5 -1
packages/notifications/discord/src/index.ts
··· 18 18 export const sendDiscordMessage = async ({ 19 19 monitor, 20 20 notification, 21 + region, 22 + statusCode, 21 23 }: { 22 24 monitor: Monitor; 23 25 notification: Notification; 26 + statusCode: number; 27 + region: string; 24 28 }) => { 25 29 const notificationData = JSON.parse(notification.data); 26 30 const { discord: webhookUrl } = notificationData; // webhook url ··· 30 34 await postToWebhook( 31 35 `Your monitor ${name} is down ๐Ÿšจ 32 36 33 - Your monitor with url ${monitor.url} is down.`, 37 + Your monitor with url ${monitor.url} is down in ${region} with status code ${statusCode}.`, 34 38 webhookUrl, 35 39 ); 36 40 } catch (err) {
+5 -1
packages/notifications/email/src/index.ts
··· 6 6 export const send = async ({ 7 7 monitor, 8 8 notification, 9 + region, 10 + statusCode, 9 11 }: { 10 12 monitor: Monitor; 11 13 notification: Notification; 14 + statusCode: number; 15 + region: string; 12 16 }) => { 13 17 // FIXME: 14 18 const config = EmailConfigurationSchema.parse(JSON.parse(notification.data)); ··· 25 29 from: "Notifications <ping@openstatus.dev>", 26 30 27 31 subject: `Your monitor ${monitor.name} is down ๐Ÿšจ`, 28 - html: `Hey, <br/> Your monitor ${monitor.name} is down. <br/> <br/> OpenStatus`, 32 + html: `<p>Hi,<br><br>Your monitor ${monitor.name} is down in ${region}. </p><p>URL : ${monitor.url}</p><p>Status Code: ${statusCode}</p><p>OpenStatus ๐Ÿ“ </p>`, 29 33 }), 30 34 }); 31 35
+5 -1
packages/notifications/slack/src/index.ts
··· 15 15 export const sendSlackMessage = async ({ 16 16 monitor, 17 17 notification, 18 + region, 19 + statusCode, 18 20 }: { 19 21 monitor: Monitor; 20 22 notification: Notification; 23 + statusCode: number; 24 + region: string; 21 25 }) => { 22 26 const notificationData = JSON.parse(notification.data); 23 27 const { slack: webhookUrl } = notificationData; // webhook url ··· 31 35 type: "section", 32 36 text: { 33 37 type: "mrkdwn", 34 - text: `Your monitor <${monitor.url}/|${name}> is down ๐Ÿšจ \n\n Powered by <https://www.openstatus.dev/|OpenStatus>.`, 38 + text: `Your monitor <${monitor.url}/|${name}> is down in ${region} with status code ${statusCode} ๐Ÿšจ \n\n Powered by <https://www.openstatus.dev/|OpenStatus>.`, 35 39 }, 36 40 accessory: { 37 41 type: "button",
+15
packages/utils/package.json
··· 1 + { 2 + "name": "@openstatus/utils", 3 + "version": "1.0.0", 4 + "description": "", 5 + "main": "index.ts", 6 + "scripts": {}, 7 + "dependencies": {}, 8 + "devDependencies": { 9 + "@openstatus/tsconfig": "workspace:*", 10 + "typescript": "5.2.2" 11 + }, 12 + "keywords": [], 13 + "author": "", 14 + "license": "ISC" 15 + }
+4
packages/utils/tsconfig.json
··· 1 + { 2 + "extends": "@openstatus/tsconfig/base.json", 3 + "include": ["src", "*.ts"] 4 + }
+15
pnpm-lock.yaml
··· 64 64 '@openstatus/upstash': 65 65 specifier: workspace:* 66 66 version: link:../../packages/upstash 67 + '@openstatus/utils': 68 + specifier: workspace:* 69 + version: link:../../packages/utils 67 70 '@t3-oss/env-core': 68 71 specifier: 0.7.1 69 72 version: 0.7.1(typescript@5.2.2)(zod@3.22.2) ··· 143 146 '@openstatus/upstash': 144 147 specifier: workspace:* 145 148 version: link:../../packages/upstash 149 + '@openstatus/utils': 150 + specifier: workspace:* 151 + version: link:../../packages/utils 146 152 '@openstatus/vercel': 147 153 specifier: workspace:* 148 154 version: link:../../packages/integrations/vercel ··· 859 865 tsup: 860 866 specifier: 7.2.0 861 867 version: 7.2.0(postcss@8.4.31)(typescript@5.2.2) 868 + typescript: 869 + specifier: 5.2.2 870 + version: 5.2.2 871 + 872 + packages/utils: 873 + devDependencies: 874 + '@openstatus/tsconfig': 875 + specifier: workspace:* 876 + version: link:../tsconfig 862 877 typescript: 863 878 specifier: 5.2.2 864 879 version: 5.2.2