Openstatus
www.openstatus.dev
1import type { Incident } from "@openstatus/db/src/schema";
2import { getRegionInfo } from "@openstatus/regions";
3import type { FormattedMessageData, NotificationContext } from "../types";
4import { getIncidentDuration } from "./incident";
5import { formatTimestamp } from "./timestamp";
6
7/**
8 * Common HTTP status descriptions
9 */
10const statusDescriptions: Record<number, string> = {
11 200: "OK",
12 201: "Created",
13 204: "No Content",
14 301: "Moved Permanently",
15 302: "Found",
16 304: "Not Modified",
17 400: "Bad Request",
18 401: "Unauthorized",
19 403: "Forbidden",
20 404: "Not Found",
21 405: "Method Not Allowed",
22 408: "Request Timeout",
23 429: "Too Many Requests",
24 500: "Internal Server Error",
25 502: "Bad Gateway",
26 503: "Service Unavailable",
27 504: "Gateway Timeout",
28};
29
30/**
31 * Format status code for display with human-readable description
32 *
33 * @example
34 * formatStatusCode(503) // "503 Service Unavailable"
35 * formatStatusCode(404) // "404 Not Found"
36 * formatStatusCode(418) // "418" (no description available)
37 * formatStatusCode(undefined) // "Unknown"
38 *
39 * @param statusCode - HTTP status code
40 * @returns Formatted status code string
41 */
42export function formatStatusCode(statusCode?: number): string {
43 if (!statusCode) {
44 return "Unknown";
45 }
46
47 const description = statusDescriptions[statusCode];
48 return description ? `${statusCode} ${description}` : `${statusCode}`;
49}
50
51/**
52 * Build common formatted message data from notification context
53 *
54 * Centralizes formatting logic used by all providers to ensure consistency.
55 *
56 * @example
57 * const data = buildCommonMessageData(context);
58 * // Returns: {
59 * // monitorName: "My API",
60 * // monitorUrl: "https://api.example.com",
61 * // monitorMethod: "GET",
62 * // monitorJobType: "http",
63 * // statusCodeFormatted: "503 Service Unavailable",
64 * // errorMessage: "Connection timeout",
65 * // timestampFormatted: "Jan 22, 2026 at 14:30 UTC",
66 * // regionsDisplay: "ams, fra, syd",
67 * // latencyDisplay: "2,450ms",
68 * // dashboardUrl: "https://app.openstatus.dev/monitors/123",
69 * // incidentDuration: undefined
70 * // }
71 *
72 * @param context - Notification context with monitor and event data
73 * @param options - Optional configuration
74 * @param options.incident - Include incident data for duration calculation
75 * @returns Formatted message data ready for rendering
76 */
77export function buildCommonMessageData(
78 context: NotificationContext,
79 options?: {
80 incident?: Incident;
81 },
82): FormattedMessageData {
83 const { monitor, statusCode, message, cronTimestamp, regions, latency } =
84 context;
85
86 // Format multiple regions as comma-separated list
87 let regionsDisplay = "Unknown";
88 if (regions && regions.length > 0) {
89 if (regions.length === 1) {
90 // Single region: show code and location
91 const regionInfo = getRegionInfo(regions[0]);
92 regionsDisplay = regionInfo
93 ? `${regionInfo.code} (${regionInfo.location})`
94 : regions[0];
95 } else {
96 // Multiple regions: show comma-separated codes
97 regionsDisplay = regions.join(", ");
98 }
99 }
100
101 // Calculate incident duration only if incident is resolved
102 let incidentDuration: string | undefined;
103 if (options?.incident?.resolvedAt) {
104 const duration = getIncidentDuration(options.incident);
105 incidentDuration = duration ?? undefined;
106 }
107
108 return {
109 monitorName: monitor.name,
110 monitorUrl: monitor.url,
111 monitorMethod: monitor.method ?? undefined,
112 monitorJobType: monitor.jobType,
113 statusCodeFormatted: formatStatusCode(statusCode),
114 errorMessage: message || "No error message available",
115 timestampFormatted: formatTimestamp(cronTimestamp),
116 regionsDisplay,
117 latencyDisplay:
118 typeof latency === "number" ? `${latency.toLocaleString()}ms` : "N/A",
119 dashboardUrl: `https://app.openstatus.dev/monitors/${monitor.id}`,
120 incidentDuration,
121 };
122}