Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
17 kB ยท 523 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524/** @jsxRuntime automatic @jsxImportSource react */
import { statusLabel } from "@openstatus/utils";import { type Duration, Effect, Schedule } from "effect";import { render } from "react-email";import { Resend } from "resend";
import FollowUpEmail from "../emails/followup";import MonitorAlertEmail, { monitorAlertSubject,} from "../emails/monitor-alert";import type { MonitorAlertProps } from "../emails/monitor-alert";import PageSubscriptionEmail from "../emails/page-subscription";import type { PageSubscriptionProps } from "../emails/page-subscription";import PrivateLocationAlertEmail, { privateLocationAlertSubject,} from "../emails/private-location-alert";import type { PrivateLocationAlertProps } from "../emails/private-location-alert";import SlackFeedbackEmail from "../emails/slack-feedback";import StatusPageMagicLinkEmail from "../emails/status-page-magic-link";import type { StatusPageMagicLinkProps } from "../emails/status-page-magic-link";import StatusReportEmail from "../emails/status-report";import type { StatusReportProps } from "../emails/status-report";import TeamInvitationEmail from "../emails/team-invitation";import type { TeamInvitationProps } from "../emails/team-invitation";import { env } from "./env";
export function statusReportSubject(req: { status: StatusReportProps["status"]; reportTitle: string;}): string { if (req.status === "resolved") return `RESOLVED: ${req.reportTitle}`; if (req.status === "maintenance") return `${statusLabel("maintenance")}: ${req.reportTitle}`; return req.reportTitle;}
const SYSTEM_FROM = "openstatus <notifications@notifications.openstatus.dev>";const SUPPORT_EMAIL = "ping@openstatus.dev";
// Deterministic Resend rejections: retrying the identical request can never// succeed (e.g. 409 invalid_idempotent_request when a key is reused with a// different body), it only burns the backoff and re-logs the error.const NON_RETRYABLE_RESEND_ERRORS = new Set<string>([ "invalid_idempotent_request", "invalid_idempotency_key", "validation_error",]);
function isRetryableSendError(error: { name: string }): boolean { return !NON_RETRYABLE_RESEND_ERRORS.has(error.name);}
// split an array into chunks of a given size.function chunk<T>(array: T[], size: number): T[][] { const result: T[][] = []; for (let i = 0; i < array.length; i += size) { result.push(array.slice(i, i + size)); } return result;}
export class EmailClient { public readonly client: Resend; // Base delay for the per-batch send retry. Overridable so tests can run the // retry path without the real ~1s exponential sleep. private readonly retryBackoff: Duration.Input;
constructor(opts: { apiKey: string; retryBackoff?: Duration.Input }) { this.client = new Resend(opts.apiKey); this.retryBackoff = opts.retryBackoff ?? "1000 millis"; }
public async sendFollowUp(req: { to: string }) { if (env.NODE_ENV === "development") { console.log(`Sending follow up email to ${req.to}`); return; }
try { const html = await render(<FollowUpEmail />); const result = await this.client.emails.send({ from: "Thibault Le Ouay Ducasse <welcome@openstatus.dev>", replyTo: "Thibault Le Ouay Ducasse <thibault@openstatus.dev>", subject: "How's it going with openstatus?", to: req.to, html, });
if (!result.error) { console.log(`Sent follow up email to ${req.to}`); return; }
throw result.error; } catch (err) { console.error(`Error sending follow up email to ${req.to}: ${err}`); } }
public async sendFollowUpBatched(req: { to: string[] }) { if (env.NODE_ENV === "development") { console.log(`Sending follow up emails to ${req.to.join(", ")}`); return; }
const html = await render(<FollowUpEmail />); const result = await this.client.batch.send( req.to.map((subscriber) => ({ from: "Thibault Le Ouay Ducasse <thibault@openstatus.dev>", subject: "How's it going with openstatus?", to: subscriber, html, })), );
if (result.error) { // We only throw the error if we are rate limited if (result.error?.name === "rate_limit_exceeded") { throw result.error; } // Otherwise let's log the error and continue console.error( `Error sending follow up email to ${req.to}: ${result.error}`, ); return; }
console.log(`Sent follow up emails to ${req.to}`); }
public async sendSlackFeedback(req: { to: string }) { if (env.NODE_ENV === "development") { console.log(`Sending slack feedback email to ${req.to}`); return; }
try { const html = await render(<SlackFeedbackEmail />); const result = await this.client.emails.send({ from: "Thibault Le Ouay Ducasse <thibault@openstatus.dev>", replyTo: "Thibault Le Ouay Ducasse <thibault@openstatus.dev>", subject: "How's the Slack app working for you?", to: req.to, html, });
if (!result.error) { console.log(`Sent slack feedback email to ${req.to}`); return; }
throw result.error; } catch (err) { console.error(`Error sending slack feedback email to ${req.to}: ${err}`); } }
public async sendSlackFeedbackBatched(req: { to: string[] }) { if (env.NODE_ENV === "development") { console.log(`Sending slack feedback emails to ${req.to.join(", ")}`); return; }
const html = await render(<SlackFeedbackEmail />); const result = await this.client.batch.send( req.to.map((subscriber) => ({ from: "Thibault Le Ouay Ducasse <thibault@openstatus.dev>", subject: "How's the Slack app working for you?", to: subscriber, html, })), );
if (result.error) { if (result.error?.name === "rate_limit_exceeded") { throw result.error; } console.error( `Error sending slack feedback email to ${req.to}: ${result.error}`, ); return; }
console.log(`Sent slack feedback emails to ${req.to}`); }
public async sendStatusReportUpdate( req: Omit< StatusReportProps, "unsubscribeUrl" | "manageUrl" | "statusPageUrl" > & { subscribers: Array<{ email: string; token: string }>; pageSlug: string; customDomain?: string | null; // Base key for Resend idempotency. The per-batch retry below would // otherwise re-send the whole chunk if a request succeeds server-side // but the response is lost. Must be stable across retries. idempotencyKey?: string; }, ) { const statusPageBaseUrl = req.customDomain ? `https://${req.customDomain}` : `https://${req.pageSlug}.openstatus.dev`;
if (env.NODE_ENV === "development") { console.log( `Sending status report update emails to ${req.subscribers .map((s) => s.email) .join(", ")}`, ); return; }
const chunks = chunk(req.subscribers, 100); for (let i = 0; i < chunks.length; i++) { const recipients = chunks[i]; // suffix the chunk index so a multi-batch send doesn't collide its // own chunks on a single shared key const batchKey = req.idempotencyKey ? `${req.idempotencyKey}:${i}` : undefined; const sendEmail = Effect.tryPromise({ try: () => this.client.batch.send( recipients.map((subscriber) => { const unsubscribeUrl = `${statusPageBaseUrl}/unsubscribe/${subscriber.token}`; const manageUrl = `${statusPageBaseUrl}/manage/${subscriber.token}`; return { from: `${req.pageTitle} <notifications@notifications.openstatus.dev>`, subject: statusReportSubject(req), to: subscriber.email, react: ( <StatusReportEmail {...req} statusPageUrl={statusPageBaseUrl} unsubscribeUrl={unsubscribeUrl} manageUrl={manageUrl} /> ), }; }), batchKey ? { idempotencyKey: batchKey } : undefined, ), catch: (_unknown) => new Error( `Error sending status report update batch to ${recipients.map( (r) => r.email, )}`, ), }).pipe( Effect.andThen((result) => result.error ? Effect.fail(result.error) : Effect.succeed(result), ), Effect.retry({ times: 3, schedule: Schedule.exponential(this.retryBackoff), while: isRetryableSendError, }), ); await Effect.runPromise(sendEmail).catch(console.error); }
console.log( `Sent status report update email to ${req.subscribers.length} subscribers`, ); }
public async sendTeamInvitation(req: TeamInvitationProps & { to: string }) { if (env.NODE_ENV === "development") { const inviteUrl = `${req.baseUrl ?? "http://localhost:3000/invite"}?token=${req.token}`; console.log(`Sending team invitation email to ${req.to}`); console.log(`>>> Team Invitation Link: ${inviteUrl}`); return; }
try { const html = await render(<TeamInvitationEmail {...req} />); const result = await this.client.emails.send({ from: `${ req.workspaceName || "openstatus" } <notifications@notifications.openstatus.dev>`, replyTo: SUPPORT_EMAIL, subject: `You've been invited to join ${ req.workspaceName || "openstatus" }`, to: req.to, html, });
if (!result.error) { console.log(`Sent team invitation email to ${req.to}`); return; }
throw result.error; } catch (err) { console.error(`Error sending team invitation email to ${req.to}`, err); } }
public async sendMonitorAlert(req: MonitorAlertProps & { to: string }) { if (env.NODE_ENV === "development") { console.log(`Sending monitor alert email to ${req.to}`); return; }
try { const { to: _to, ...props } = req; const html = await render(<MonitorAlertEmail {...props} />); const result = await this.client.emails.send({ from: SYSTEM_FROM, replyTo: SUPPORT_EMAIL, subject: monitorAlertSubject(props), to: req.to, html, });
if (!result.error) { console.log(`Sent monitor alert email to ${req.to}`); return; }
throw result.error; } catch (err) { console.error(`Error sending monitor alert to ${req.to}`, err); throw err; } }
public async sendPageSubscription( req: PageSubscriptionProps & { to: string }, ) { if (env.NODE_ENV === "development") { console.log(`Sending page subscription email to ${req.to}`); return; }
try { const html = await render(<PageSubscriptionEmail {...req} />); const result = await this.client.emails.send({ from: "Status Page <notifications@notifications.openstatus.dev>", subject: `Confirm your subscription to ${req.page}`, to: req.to, html, });
if (!result.error) { console.log(`Sent page subscription email to ${req.to}`); return; }
throw result.error; } catch (err) { console.error(`Error sending page subscription to ${req.to}`, err); } }
public async sendStatusPageMagicLink( req: StatusPageMagicLinkProps & { to: string }, ) { if (env.NODE_ENV === "development") { console.log(`Sending status page magic link email to ${req.to}`); console.log(`>>> Magic Link: ${req.link}`); return; }
try { const html = await render(<StatusPageMagicLinkEmail {...req} />); const result = await this.client.emails.send({ from: "Status Page <notifications@notifications.openstatus.dev>", subject: `Authenticate to ${req.page}`, to: req.to, html, });
if (!result.error) { console.log(`Sent status page magic link email to ${req.to}`); return; }
throw result.error; } catch (err) { console.error(`Error sending status page magic link to ${req.to}`, err); } }
public async sendMaintenanceNotification(req: { subscribers: Array<{ email: string; token: string }>; pageTitle: string; pageSlug: string; customDomain?: string | null; maintenanceTitle: string; message: string; from: string; to: string; pageComponents: string[]; idempotencyKey?: string; }) { const statusPageBaseUrl = req.customDomain ? `https://${req.customDomain}` : `https://${req.pageSlug}.openstatus.dev`;
if (env.NODE_ENV === "development") { console.log( `Sending maintenance notification emails to ${req.subscribers .map((s) => s.email) .join(", ")}`, ); return; }
const chunks = chunk(req.subscribers, 100); for (let i = 0; i < chunks.length; i++) { const recipients = chunks[i]; const batchKey = req.idempotencyKey ? `${req.idempotencyKey}:${i}` : undefined; const sendEmail = Effect.tryPromise({ try: () => this.client.batch.send( recipients.map((subscriber) => { const unsubscribeUrl = `${statusPageBaseUrl}/unsubscribe/${subscriber.token}`; const manageUrl = `${statusPageBaseUrl}/manage/${subscriber.token}`; return { from: `${req.pageTitle} <notifications@notifications.openstatus.dev>`, subject: `Scheduled Maintenance: ${req.maintenanceTitle}`, to: subscriber.email, react: ( <StatusReportEmail pageTitle={req.pageTitle} reportTitle={req.maintenanceTitle} status="maintenance" date={`${req.from} - ${req.to}`} message={req.message} pageComponents={req.pageComponents} statusPageUrl={statusPageBaseUrl} unsubscribeUrl={unsubscribeUrl} manageUrl={manageUrl} /> ), }; }), batchKey ? { idempotencyKey: batchKey } : undefined, ), catch: (_unknown) => new Error( `Error sending maintenance notification batch to ${recipients.map( (r) => r.email, )}`, ), }).pipe( Effect.andThen((result) => result.error ? Effect.fail(result.error) : Effect.succeed(result), ), Effect.retry({ times: 3, schedule: Schedule.exponential(this.retryBackoff), while: isRetryableSendError, }), ); await Effect.runPromise(sendEmail).catch(console.error); }
console.log( `Sent maintenance notification email to ${req.subscribers.length} subscribers`, ); }
public async sendPrivateLocationAlert( req: Omit<PrivateLocationAlertProps, "lastSeenAt"> & { to: string[]; lastSeenAt: Date; }, ) { if (req.to.length === 0) return;
const subject = privateLocationAlertSubject(req);
if (env.NODE_ENV === "development") { console.log( `Sending private location ${req.status} email to ${req.to.join(", ")}`, ); return; }
try { const html = await render( <PrivateLocationAlertEmail locationName={req.locationName} status={req.status} lastSeenAt={req.lastSeenAt.toISOString()} monitorCount={req.monitorCount} />, ); const result = await this.client.batch.send( req.to.map((to) => ({ from: SYSTEM_FROM, replyTo: SUPPORT_EMAIL, subject, to, html, })), );
if (result.error) { if (result.error?.name === "rate_limit_exceeded") { throw result.error; } console.error( `Error sending private location alert to ${req.to}: ${result.error}`, ); return; }
console.log(`Sent private location ${req.status} email to ${req.to}`); } catch (err) { console.error( `Error sending private location alert to ${req.to}: ${err}`, ); } }}