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.
9.8 kB ยท 393 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394import type { FormattedMessageData } from "@openstatus/notification-base";
/** * Slack Block types for rich message formatting * Reference: https://docs.slack.dev/messaging/formatting-message-text */
interface SlackTextObject { type: "plain_text" | "mrkdwn"; text: string; emoji?: boolean;}
interface SlackHeaderBlock { type: "header"; text: SlackTextObject;}
interface SlackSectionBlock { type: "section"; text?: SlackTextObject; fields?: SlackTextObject[]; accessory?: SlackButtonElement;}
interface SlackDividerBlock { type: "divider";}
interface SlackActionsBlock { type: "actions"; elements: SlackButtonElement[];}
interface SlackButtonElement { type: "button"; text: SlackTextObject; url?: string; action_id?: string;}
type SlackBlock = | SlackHeaderBlock | SlackSectionBlock | SlackDividerBlock | SlackActionsBlock;
/** * Escapes special characters for Slack mrkdwn format * Reference: https://api.slack.com/reference/surfaces/formatting#escaping * * @param text - Text to escape * @returns Escaped text safe for Slack mrkdwn * * @example * escapeSlackText("Hello & <world>") // "Hello & <world>" */export function escapeSlackText(text: string): string { return text .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">");}
/** * Builds Slack blocks for alert notifications * * Layout: * - Header: "{monitor.name} is failing" * - Section: "METHOD URL" in code format (e.g., `GET https://api.example.com`) * - Divider * - Section: 4 fields in 2x2 grid (Status, Regions, Latency, Cron Timestamp) * - Section: Error message in code block * - Actions: Dashboard button * * @param data - Formatted message data from buildCommonMessageData * @returns Array of Slack blocks * * @example * const blocks = buildAlertBlocks({ * monitorName: "API Health", * monitorUrl: "https://api.example.com", * monitorMethod: "GET", * monitorJobType: "http", * statusCodeFormatted: "503 Service Unavailable", * errorMessage: "Connection timeout", * timestampFormatted: "Jan 22, 2026 at 14:30 UTC", * regionsDisplay: "iad, fra, syd", * latencyDisplay: "1,234 ms", * dashboardUrl: "https://app.openstatus.dev/monitors/123" * }); */export function buildAlertBlocks(data: FormattedMessageData): SlackBlock[] { const escapedError = escapeSlackText(data.errorMessage);
// Format description as "METHOD URL" or just "URL" for non-HTTP const description = data.monitorMethod && data.monitorJobType === "http" ? `${data.monitorMethod} <${data.monitorUrl}|${data.monitorUrl}>` : `<${data.monitorUrl}|${data.monitorUrl}>`;
return [ { type: "header", text: { type: "plain_text", text: buildTitle(data.monitorName, " is failing"), emoji: false, }, }, { type: "section", text: { type: "mrkdwn", text: `\`${description}\``, }, }, { type: "divider", }, { type: "section", fields: [ { type: "mrkdwn", text: `*Status*\n${data.statusCodeFormatted}`, }, { type: "mrkdwn", text: `*Regions*\n${data.regionsDisplay}`, }, { type: "mrkdwn", text: `*Latency*\n${data.latencyDisplay}`, }, { type: "mrkdwn", text: `*Cron Timestamp*\n${data.timestampFormatted}`, }, ], }, { type: "section", text: { type: "mrkdwn", text: `*Error*\n\`\`\`${escapedError}\`\`\``, }, }, { type: "actions", elements: [ { type: "button", text: { type: "plain_text", text: "View Dashboard", emoji: true, }, url: data.dashboardUrl, action_id: "view_dashboard", }, ], }, ];}
/** * Builds Slack blocks for recovery notifications * * Layout: * - Header: "{monitor.name} is recovered" * - Section: "METHOD URL" in code format (e.g., `GET https://api.example.com`) * - Section: Downtime duration (optional, only if data.incidentDuration exists) * - Divider * - Section: 4 fields in 2x2 grid (Status, Regions, Latency, Cron Timestamp) * - Actions: Dashboard button * * @param data - Formatted message data from buildCommonMessageData * @returns Array of Slack blocks * * @example * const blocks = buildRecoveryBlocks({ * monitorName: "API Health", * monitorUrl: "https://api.example.com", * monitorMethod: "GET", * monitorJobType: "http", * statusCodeFormatted: "200 OK", * errorMessage: "", * timestampFormatted: "Jan 22, 2026 at 14:35 UTC", * regionsDisplay: "iad, fra, syd", * latencyDisplay: "156 ms", * dashboardUrl: "https://app.openstatus.dev/monitors/123", * incidentDuration: "5m 30s" * }); */export function buildRecoveryBlocks(data: FormattedMessageData): SlackBlock[] { // Format description as "METHOD URL" or just "URL" for non-HTTP const description = data.monitorMethod && data.monitorJobType === "http" ? `${data.monitorMethod} <${data.monitorUrl}|${data.monitorUrl}>` : `<${data.monitorUrl}|${data.monitorUrl}>`;
const blocks: SlackBlock[] = [ { type: "header", text: { type: "plain_text", text: buildTitle(data.monitorName, " is recovered"), emoji: false, }, }, { type: "section", text: { type: "mrkdwn", text: `\`${description}\``, }, }, ];
// Only include downtime if incident duration is available if (data.incidentDuration) { blocks.push({ type: "section", text: { type: "mrkdwn", text: `โฑ๏ธ *Downtime:* ${data.incidentDuration}`, }, }); }
blocks.push( { type: "divider", }, { type: "section", fields: [ { type: "mrkdwn", text: `*Status*\n${data.statusCodeFormatted}`, }, { type: "mrkdwn", text: `*Regions*\n${data.regionsDisplay}`, }, { type: "mrkdwn", text: `*Latency*\n${data.latencyDisplay}`, }, { type: "mrkdwn", text: `*Cron Timestamp*\n${data.timestampFormatted}`, }, ], }, { type: "actions", elements: [ { type: "button", text: { type: "plain_text", text: "View Dashboard", emoji: true, }, url: data.dashboardUrl, action_id: "view_dashboard", }, ], }, );
return blocks;}
/** * Builds Slack blocks for degraded notifications * * Layout: * - Header: "{monitor.name} is degraded" * - Section: "METHOD URL" in code format (e.g., `GET https://api.example.com`) * - Section: Previous incident duration (optional, only if data.incidentDuration exists) * - Divider * - Section: 4 fields in 2x2 grid (Status, Regions, Latency, Cron Timestamp) * - Actions: Dashboard button * * @param data - Formatted message data from buildCommonMessageData * @returns Array of Slack blocks * * @example * const blocks = buildDegradedBlocks({ * monitorName: "API Health", * monitorUrl: "https://api.example.com", * monitorMethod: "GET", * monitorJobType: "http", * statusCodeFormatted: "504 Gateway Timeout", * errorMessage: "Slow response", * timestampFormatted: "Jan 22, 2026 at 14:40 UTC", * regionsDisplay: "iad, fra, syd", * latencyDisplay: "5,234 ms", * dashboardUrl: "https://app.openstatus.dev/monitors/123", * incidentDuration: "2h 15m" * }); */export function buildDegradedBlocks(data: FormattedMessageData): SlackBlock[] { // Format description as "METHOD URL" or just "URL" for non-HTTP const description = data.monitorMethod && data.monitorJobType === "http" ? `${data.monitorMethod} <${data.monitorUrl}|${data.monitorUrl}>` : `<${data.monitorUrl}|${data.monitorUrl}>`;
const blocks: SlackBlock[] = [ { type: "header", text: { type: "plain_text", text: buildTitle(data.monitorName, " is degraded"), emoji: false, }, }, { type: "section", text: { type: "mrkdwn", text: `\`${description}\``, }, }, ];
// Only include previous incident duration if available if (data.incidentDuration) { blocks.push({ type: "section", text: { type: "mrkdwn", text: `โฑ๏ธ *Previous Incident Duration:* ${data.incidentDuration}`, }, }); }
blocks.push( { type: "divider", }, { type: "section", fields: [ { type: "mrkdwn", text: `*Status*\n${data.statusCodeFormatted}`, }, { type: "mrkdwn", text: `*Regions*\n${data.regionsDisplay}`, }, { type: "mrkdwn", text: `*Latency*\n${data.latencyDisplay}`, }, { type: "mrkdwn", text: `*Cron Timestamp*\n${data.timestampFormatted}`, }, ], }, { type: "actions", elements: [ { type: "button", text: { type: "plain_text", text: "View Dashboard", emoji: true, }, url: data.dashboardUrl, action_id: "view_dashboard", }, ], }, );
return blocks;}
function buildTitle(name: string, suffix: string): string { const shortenedName = escapeSlackText(name) .slice(0, 150 - suffix.length) // Keep escape sequences and surrogate pairs complete at the title limit. .replace(/&[^;]*$|[\uD800-\uDBFF]$/g, ""); return `${shortenedName}${suffix}`;}