Openstatus
www.openstatus.dev
1import type {
2 Incident,
3 Maintenance,
4 StatusReport,
5 StatusReportUpdate,
6} from "@openstatus/db/src/schema";
7
8import { isInBlacklist } from "./blacklist";
9import { classNames, statusDetails } from "./config";
10import type { StatusDetails, StatusVariant } from "./types";
11import { Status } from "./types";
12import { endOfDay, isSameDay, startOfDay } from "./utils";
13
14type Monitor = {
15 count: number;
16 ok: number;
17 day: string;
18};
19type StatusReports = (StatusReport & {
20 statusReportUpdates?: StatusReportUpdate[];
21})[];
22type Incidents = Incident[];
23type Maintenances = Maintenance[];
24
25/**
26 * Tracker Class is supposed to handle the data and calculate from a single monitor.
27 * But we use it to handle the StatusCheck as well (with no data for a single monitor).
28 * We can create Inheritence to handle the StatusCheck and Monitor separately and even
29 * StatusPage with multiple Monitors.
30 */
31export class Tracker {
32 private data: Monitor[] = [];
33 private statusReports: StatusReports = [];
34 private incidents: Incidents = [];
35 private maintenances: Maintenances = [];
36
37 constructor(arg: {
38 data?: Monitor[];
39 statusReports?: StatusReports;
40 incidents?: Incidents;
41 maintenances?: Maintenance[];
42 }) {
43 this.data = arg.data || []; // TODO: use another Class to handle a single Day
44 this.statusReports = arg.statusReports || [];
45 this.incidents = arg.incidents || [];
46 this.maintenances = arg.maintenances || [];
47 }
48
49 private calculateUptime(data: { ok: number; count: number }[]) {
50 const { count, ok } = this.aggregatedData(data);
51 if (count === 0) return 100; // starting with 100% uptime
52 return Math.round((ok / count) * 10_000) / 100; // round to 2 decimal places
53 }
54
55 private aggregatedData(data: { ok: number; count: number }[]) {
56 return data.reduce(
57 (prev, curr) => {
58 prev.ok += curr.ok;
59 prev.count += curr.count;
60 return prev;
61 },
62 { count: 0, ok: 0 },
63 );
64 }
65
66 get isDataMissing() {
67 const { count } = this.aggregatedData(this.data);
68 return count === 0;
69 }
70
71 private calculateUptimeStatus(data: { ok: number; count: number }[]): Status {
72 const uptime = this.calculateUptime(data);
73 if (uptime >= 98) return Status.Operational;
74 if (uptime >= 60) return Status.DegradedPerformance;
75 if (uptime > 30) return Status.PartialOutage;
76 return Status.MajorOutage;
77 }
78
79 private isOngoingIncident() {
80 return this.incidents.some((incident) => !incident.resolvedAt);
81 }
82
83 private isOngoingReport() {
84 const resolved: StatusReport["status"][] = ["monitoring", "resolved"];
85 return this.statusReports.some(
86 (report) => !resolved.includes(report.status),
87 );
88 }
89
90 private isOngoingMaintenance() {
91 return this.maintenances.some((maintenance) => {
92 const now = new Date();
93 return (
94 new Date(maintenance.from).getTime() <= now.getTime() &&
95 new Date(maintenance.to).getTime() >= now.getTime()
96 );
97 });
98 }
99
100 get totalUptime(): number {
101 return this.calculateUptime(this.data);
102 }
103
104 get currentStatus(): Status {
105 if (this.isOngoingMaintenance()) return Status.UnderMaintenance;
106 if (this.isOngoingReport()) return Status.DegradedPerformance;
107 if (this.isOngoingIncident()) return Status.Incident;
108 return this.calculateUptimeStatus(this.data);
109 }
110
111 get currentVariant(): StatusVariant {
112 return statusDetails[this.currentStatus].variant;
113 }
114
115 get currentDetails(): StatusDetails {
116 return statusDetails[this.currentStatus];
117 }
118
119 get currentClassName(): string {
120 return classNames[this.currentVariant];
121 }
122
123 // HACK: this is a temporary solution to get the incidents
124 private getIncidentsByDay(day: Date): Incidents {
125 const incidents = this.incidents?.filter((incident) => {
126 const { startedAt, resolvedAt } = incident;
127 const eod = endOfDay(day);
128 const sod = startOfDay(day);
129
130 if (!startedAt) return false; // not started
131
132 const hasStartedAfterEndOfDay = startedAt.getTime() >= eod.getTime();
133
134 if (hasStartedAfterEndOfDay) return false;
135
136 if (!resolvedAt) return true; // still ongoing
137
138 const hasResolvedBeforeStartOfDay = resolvedAt.getTime() <= sod.getTime();
139
140 if (hasResolvedBeforeStartOfDay) return false;
141
142 const hasStartedBeforeEndOfDay = startedAt.getTime() <= eod.getTime();
143
144 const hasResolvedBeforeEndOfDay = resolvedAt.getTime() <= eod.getTime();
145
146 if (hasStartedBeforeEndOfDay || hasResolvedBeforeEndOfDay) return true;
147
148 return false;
149 });
150
151 return incidents;
152 }
153
154 // HACK: this is a temporary solution to get the status reports
155 private getStatusReportsByDay(props: Monitor): StatusReports {
156 const statusReports = this.statusReports?.filter((report) => {
157 const firstStatusReportUpdate = report?.statusReportUpdates?.sort(
158 (a, b) => a.date.getTime() - b.date.getTime(),
159 )?.[0];
160
161 if (!firstStatusReportUpdate) return false;
162
163 const day = new Date(props.day);
164 return isSameDay(firstStatusReportUpdate.date, day);
165 });
166 return statusReports;
167 }
168
169 private getMaintenancesByDay(day: Date): Maintenances {
170 const maintenances = this.maintenances.filter((maintenance) => {
171 const eod = endOfDay(day);
172 const sod = startOfDay(day);
173 return (
174 maintenance.from.getTime() <= eod.getTime() &&
175 maintenance.to.getTime() >= sod.getTime()
176 );
177 });
178 return maintenances;
179 }
180
181 // TODO: it would be great to create a class to handle a single day
182 // FIXME: will be always generated on each tracker.days call - needs to be in the constructor?
183 get days() {
184 const data = this.data.map((props) => {
185 const day = new Date(props.day);
186 const blacklist = isInBlacklist(day);
187 const incidents = this.getIncidentsByDay(day);
188 const statusReports = this.getStatusReportsByDay(props);
189 const maintenances = this.getMaintenancesByDay(day);
190
191 const isMissingData = props.count === 0;
192
193 /**
194 * 1. Maintenance
195 * 2. Status Reports (Degraded Performance)
196 * 3. Incidents
197 * 4. Uptime Status (Operational, Degraded Performance, Partial Outage, Major Outage)
198 */
199 const status = maintenances.length
200 ? Status.UnderMaintenance
201 : statusReports.length
202 ? Status.DegradedPerformance
203 : incidents.length
204 ? Status.Incident
205 : isMissingData
206 ? Status.Unknown
207 : this.calculateUptimeStatus([props]);
208
209 const variant = statusDetails[status].variant;
210 const label = statusDetails[status].short;
211
212 return {
213 ...props,
214 blacklist,
215 incidents,
216 statusReports,
217 maintenances,
218 status,
219 variant,
220 label: isMissingData ? "Missing" : label,
221 };
222 });
223 return data;
224 }
225
226 get toString() {
227 return statusDetails[this.currentStatus].short;
228 }
229}