···11+TODO: Update the different component/files to use the package as source of
22+truth!
33+44+- [x] public/status
55+- [ ] package/react dev deps
66+- [x] `status-check` on status page
77+- [x] tracker `bar` on status page
88+- [x] og image api `status-check`
99+- [x] monitor (overview) dasboard
···11+import { isSameDay } from "./utils";
22+33+/**
44+ * Blacklist dates where we had issues with data collection
55+ */
66+export const blacklistDates: Record<string, string> = {
77+ "Fri Aug 25 2023":
88+ "OpenStatus faced issues between 24.08. and 27.08., preventing data collection.",
99+ "Sat Aug 26 2023":
1010+ "OpenStatus faced issues between 24.08. and 27.08., preventing data collection.",
1111+ "Wed Oct 18 2023":
1212+ "OpenStatus migrated from Vercel to Fly to improve the performance of the checker.",
1313+};
1414+1515+export function isInBlacklist(day: Date) {
1616+ const el = Object.keys(blacklistDates).find((date) =>
1717+ isSameDay(new Date(date), day),
1818+ );
1919+ return el ? blacklistDates[el] : undefined;
2020+}
···11+import type {
22+ Incident,
33+ StatusReport,
44+ StatusReportUpdate,
55+} from "@openstatus/db/src/schema";
66+import type { Monitor } from "@openstatus/tinybird";
77+88+import { isInBlacklist } from "./blacklist";
99+import { classNames, statusDetails } from "./config";
1010+import type { StatusDetails, StatusVariant } from "./types";
1111+import { Status } from "./types";
1212+import { endOfDay, isSameDay, startOfDay } from "./utils";
1313+1414+type Monitors = Monitor[];
1515+type StatusReports = (StatusReport & {
1616+ statusReportUpdates?: StatusReportUpdate[];
1717+})[];
1818+type Incidents = Incident[];
1919+2020+/**
2121+ * Tracker Class is supposed to handle the data and calculate from a single monitor.
2222+ * But we use it to handle the StatusCheck as well (with no data for a single monitor).
2323+ * We can create Inheritence to handle the StatusCheck and Monitor separately and even
2424+ * StatusPage with multiple Monitors.
2525+ */
2626+export class Tracker {
2727+ private data: Monitors = [];
2828+ private statusReports: StatusReports = [];
2929+ private incidents: Incidents = [];
3030+3131+ constructor(arg: {
3232+ data?: Monitors;
3333+ statusReports?: StatusReports;
3434+ incidents?: Incidents;
3535+ }) {
3636+ this.data = arg.data || []; // TODO: use another Class to handle a single Day
3737+ this.statusReports = arg.statusReports || [];
3838+ this.incidents = arg.incidents || [];
3939+ }
4040+4141+ private calculateUptime(data: { ok: number; count: number }[]) {
4242+ const { count, ok } = this.aggregatedData(data);
4343+ if (count === 0) return 100; // starting with 100% uptime
4444+ return Math.round((ok / count) * 10_000) / 100; // round to 2 decimal places
4545+ }
4646+4747+ private aggregatedData(data: { ok: number; count: number }[]) {
4848+ return data.reduce(
4949+ (prev, curr) => {
5050+ prev.ok += curr.ok;
5151+ prev.count += curr.count;
5252+ return prev;
5353+ },
5454+ { count: 0, ok: 0 },
5555+ );
5656+ }
5757+5858+ get isDataMissing() {
5959+ const { count } = this.aggregatedData(this.data);
6060+ return count === 0;
6161+ }
6262+6363+ private calculateUptimeStatus(data: { ok: number; count: number }[]): Status {
6464+ const uptime = this.calculateUptime(data);
6565+ if (uptime >= 99.8) return Status.Operational;
6666+ if (uptime >= 95) return Status.DegradedPerformance;
6767+ if (uptime > 50) return Status.PartialOutage;
6868+ return Status.MajorOutage;
6969+ }
7070+7171+ private isOngoingIncident() {
7272+ return this.incidents.some((incident) => !incident.resolvedAt);
7373+ }
7474+7575+ private isOngoingReport() {
7676+ const resolved: StatusReport["status"][] = ["monitoring", "resolved"];
7777+ return this.statusReports.some(
7878+ (report) => !resolved.includes(report.status),
7979+ );
8080+ }
8181+8282+ get totalUptime(): number {
8383+ return this.calculateUptime(this.data);
8484+ }
8585+8686+ get currentStatus(): Status {
8787+ if (this.isOngoingReport()) return Status.DegradedPerformance;
8888+ if (this.isOngoingIncident()) return Status.Incident;
8989+ return this.calculateUptimeStatus(this.data);
9090+ }
9191+9292+ get currentVariant(): StatusVariant {
9393+ return statusDetails[this.currentStatus].variant;
9494+ }
9595+9696+ get currentDetails(): StatusDetails {
9797+ return statusDetails[this.currentStatus];
9898+ }
9999+100100+ get currentClassName(): string {
101101+ return classNames[this.currentVariant];
102102+ }
103103+104104+ // HACK: this is a temporary solution to get the incidents
105105+ private getIncidentsByDay(day: Date): Incidents {
106106+ const incidents = this.incidents?.filter((incident) => {
107107+ const { startedAt, resolvedAt } = incident;
108108+ const eod = endOfDay(day);
109109+ const sod = startOfDay(day);
110110+111111+ if (!startedAt) return false; // not started
112112+113113+ const hasStartedAfterEndOfDay = startedAt.getTime() >= eod.getTime();
114114+115115+ if (hasStartedAfterEndOfDay) return false;
116116+117117+ if (!resolvedAt) return true; // still ongoing
118118+119119+ const hasResolvedBeforeStartOfDay = resolvedAt.getTime() <= sod.getTime();
120120+121121+ if (hasResolvedBeforeStartOfDay) return false;
122122+123123+ const hasStartedBeforeEndOfDay = startedAt.getTime() <= eod.getTime();
124124+125125+ const hasResolvedBeforeEndOfDay = resolvedAt.getTime() <= eod.getTime();
126126+127127+ if (hasStartedBeforeEndOfDay || hasResolvedBeforeEndOfDay) return true;
128128+129129+ return false;
130130+ });
131131+132132+ return incidents;
133133+ }
134134+135135+ // HACK: this is a temporary solution to get the status reports
136136+ private getStatusReportsByDay(props: Monitor): StatusReports {
137137+ const statusReports = this.statusReports?.filter((report) => {
138138+ const firstStatusReportUpdate = report?.statusReportUpdates?.sort(
139139+ (a, b) => a.date.getTime() - b.date.getTime(),
140140+ )?.[0];
141141+142142+ if (!firstStatusReportUpdate) return false;
143143+144144+ const day = new Date(props.day);
145145+ return isSameDay(firstStatusReportUpdate.date, day);
146146+ });
147147+ return statusReports;
148148+ }
149149+150150+ // TODO: it would be great to create a class to handle a single day
151151+ // FIXME: will be always generated on each tracker.days call - needs to be in the constructor?
152152+ get days() {
153153+ const data = this.data.map((props) => {
154154+ const day = new Date(props.day);
155155+ const blacklist = isInBlacklist(day);
156156+ const incidents = this.getIncidentsByDay(day);
157157+ const statusReports = this.getStatusReportsByDay(props);
158158+159159+ const isMissingData = props.count === 0;
160160+161161+ // FIXME:
162162+ const status = incidents.length
163163+ ? Status.Incident
164164+ : isMissingData
165165+ ? Status.Unknown
166166+ : this.calculateUptimeStatus([props]);
167167+168168+ const variant = statusDetails[status].variant;
169169+ const label = statusDetails[status].short;
170170+171171+ return {
172172+ ...props,
173173+ blacklist,
174174+ incidents,
175175+ statusReports,
176176+ status,
177177+ variant,
178178+ label: isMissingData ? "Missing" : label,
179179+ };
180180+ });
181181+ return data;
182182+ }
183183+184184+ get toString() {
185185+ return statusDetails[this.currentStatus].short;
186186+ }
187187+}
+33
packages/tracker/src/types.ts
···11+import type { Incident, StatusReport } from "@openstatus/db/src/schema";
22+33+// DO NOT CHANGE!
44+export enum Status {
55+ Operational = "operational",
66+ DegradedPerformance = "degraded_performance",
77+ PartialOutage = "partial_outage",
88+ MajorOutage = "major_outage",
99+ UnderMaintenance = "under_maintenance", // not used
1010+ Unknown = "unknown",
1111+ Incident = "incident",
1212+}
1313+1414+export type StatusVariant = "up" | "degraded" | "down" | "empty" | "incident";
1515+1616+export type StatusDetails = {
1717+ long: string;
1818+ short: string;
1919+ variant: StatusVariant;
2020+};
2121+2222+/**
2323+ * Data used for the `Bar` component within the `Tracker` component.
2424+ */
2525+export type TrackerData = {
2626+ ok: number;
2727+ count: number;
2828+ date: Date;
2929+ incidents: Incident[];
3030+ statusReports: StatusReport[];
3131+ status: Status;
3232+ variant: StatusVariant;
3333+};
+32
packages/tracker/src/utils.ts
···11+export function endOfDay(date: Date): Date {
22+ // Create a new Date object to avoid mutating the original date
33+ const newDate = new Date(date);
44+55+ // Set hours, minutes, seconds, and milliseconds to end of day
66+ newDate.setHours(23, 59, 59, 999);
77+88+ return newDate;
99+}
1010+1111+export function startOfDay(date: Date): Date {
1212+ // Create a new Date object to avoid mutating the original date
1313+ const newDate = new Date(date);
1414+1515+ // Set hours, minutes, seconds, and milliseconds to start of day
1616+ newDate.setHours(0, 0, 0, 0);
1717+1818+ return newDate;
1919+}
2020+2121+export function isSameDay(date1: Date, date2: Date) {
2222+ const newDate1 = new Date(date1);
2323+ const newDate2 = new Date(date2);
2424+2525+ newDate1.setDate(newDate1.getDate());
2626+ newDate1.setHours(0, 0, 0, 0);
2727+2828+ newDate2.setDate(newDate2.getDate());
2929+ newDate2.setHours(0, 0, 0, 0);
3030+3131+ return newDate1.toUTCString() === newDate2.toUTCString();
3232+}