Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: improve tracker date handling (#445)

* chore: improve tracker date handling

* fix: play and include blacklist to clean data

authored by

Maximilian Kaske and committed by
GitHub
52791d2a 4a76fec5

+158 -145
+7 -11
apps/web/src/app/api/og/route.tsx
··· 2 2 3 3 import { DESCRIPTION, TITLE } from "@/app/shared-metadata"; 4 4 import { getMonitorListData } from "@/lib/tb"; 5 - import { blacklistDates, getMonitorList, getStatus } from "@/lib/tracker"; 5 + import { cleanData, getStatus, isInBlacklist } from "@/lib/tracker"; 6 6 import { cn, formatDate } from "@/lib/utils"; 7 7 8 8 export const runtime = "edge"; ··· 50 50 }))) || 51 51 []; 52 52 53 - const { monitors, uptime } = getMonitorList(data, { 54 - maxSize: 40, 55 - }); 53 + const { bars, uptime } = cleanData({ data, last: LIMIT }); 56 54 57 55 return new ImageResponse( 58 56 ( ··· 78 76 {title} 79 77 </h1> 80 78 <p tw="text-slate-600 text-3xl">{description}</p> 81 - {monitors && monitors.length > 0 ? ( 79 + {bars && bars.length > 0 ? ( 82 80 <div tw="flex flex-col w-full mt-6"> 83 81 <div tw="flex flex-row items-center justify-between -mb-1 text-black font-light"> 84 82 <p tw="">{formatDate(new Date())}</p> 85 - <p tw="mr-1">{uptime}% uptime</p> 83 + <p tw="mr-1">{uptime}</p> 86 84 </div> 87 85 <div tw="flex flex-row relative"> 88 86 {/* Empty State */} ··· 94 92 ></div> 95 93 ); 96 94 })} 97 - <div tw="flex flex-row absolute right-0"> 98 - {monitors.map((item, i) => { 95 + <div tw="flex flex-row-reverse absolute right-0"> 96 + {bars.map((item, i) => { 99 97 const { variant } = getStatus(item.ok / item.count); 100 - const isBlackListed = Object.keys(blacklistDates).includes( 101 - String(item.cronTimestamp), 102 - ); 98 + const isBlackListed = Boolean(item.blacklist); 103 99 if (isBlackListed) { 104 100 return ( 105 101 <div
+3 -3
apps/web/src/app/play/page.tsx
··· 1 1 import { Tracker } from "@/components/tracker"; 2 - import { getMonitorListData } from "@/lib/tb"; 2 + import { getHomeMonitorListData } from "@/lib/tb"; 3 3 4 4 export default async function PlayPage() { 5 - const data = await getMonitorListData({ monitorId: "openstatusPing" }); 5 + const data = await getHomeMonitorListData(); 6 6 return ( 7 7 <div className="relative flex flex-col items-center justify-center gap-4"> 8 8 <p className="font-cal mb-1 text-3xl">Status</p> ··· 13 13 {data && ( 14 14 <Tracker 15 15 data={data} 16 - id="openstatusPing" 16 + id="1" 17 17 name="Ping" 18 18 url="https://www.openstatus.dev/api/ping" 19 19 />
+10 -1
apps/web/src/app/status-page/[domain]/loading.tsx
··· 1 + import { Skeleton } from "@openstatus/ui"; 2 + 1 3 import { Container } from "@/components/dashboard/container"; 2 4 import { Header } from "@/components/dashboard/header"; 3 5 4 - export default function StatusPageLoading() { 6 + export default function Loading() { 5 7 return ( 6 8 <div className="grid gap-6"> 7 9 <Header.Skeleton /> 10 + <div className="flex flex-col items-center gap-2"> 11 + <div className="flex items-center gap-3"> 12 + <Skeleton className="h-7 w-24" /> 13 + <Skeleton className="h-10 w-10 rounded-full" /> 14 + </div> 15 + <Skeleton className="h-4 w-28" /> 16 + </div> 8 17 <div className="grid gap-4"> 9 18 <Container.Skeleton /> 10 19 <Container.Skeleton />
-2
apps/web/src/app/status-page/[domain]/page.tsx
··· 14 14 import { IncidentList } from "@/components/status-page/incident-list"; 15 15 import { MonitorList } from "@/components/status-page/monitor-list"; 16 16 import { StatusCheck } from "@/components/status-page/status-check"; 17 - import { getMonitorListData } from "@/lib/tb"; 18 - import { notEmpty } from "@/lib/utils"; 19 17 import { api } from "@/trpc/server"; 20 18 21 19 const url =
-1
apps/web/src/components/status-page/monitor.tsx
··· 19 19 name={monitor.name} 20 20 url={monitor.url} 21 21 description={monitor.description} 22 - context="status-page" 23 22 /> 24 23 ); 25 24 };
+18 -6
apps/web/src/components/status-page/status-check.tsx
··· 1 + import { cva } from "class-variance-authority"; 1 2 import type { z } from "zod"; 2 3 3 4 import type { ··· 10 11 import { getStatus } from "@/lib/tracker"; 11 12 import { cn, notEmpty } from "@/lib/utils"; 12 13 import { Icons } from "../icons"; 14 + 15 + const check = cva("border-border rounded-full border p-2", { 16 + variants: { 17 + variant: { 18 + up: "text-green-500 bg-green-500", 19 + down: "text-red-500 bg-red-500", 20 + degraded: "text-yellow-500 bg-yellow-500", 21 + empty: "text-gray-500 bg-gray-500", 22 + incident: "text-yellow-500 bg-yellow-500", 23 + }, 24 + }, 25 + defaultVariants: { 26 + variant: "up", 27 + }, 28 + }); 13 29 14 30 export async function StatusCheck({ 15 31 incidents, ··· 49 65 const incident = { 50 66 label: "Incident", 51 67 variant: "incident", 52 - twColor: "text-yellow-500", 53 - twBgColor: "bg-yellow-500", 54 68 } as const; 55 69 56 - const { label, variant, twBgColor } = isIncident ? incident : status; 70 + const { label, variant } = isIncident ? incident : status; 57 71 58 72 return ( 59 73 <div className="flex flex-col items-center gap-2"> 60 74 <div className="flex items-center gap-3"> 61 75 <p className="text-lg font-semibold">{label}</p> 62 - <span 63 - className={cn("border-border rounded-full border p-2", twBgColor)} 64 - > 76 + <span className={check({ variant })}> 65 77 <StatusIcon variant={variant} /> 66 78 </span> 67 79 </div>
+17 -33
apps/web/src/components/tracker.tsx
··· 19 19 } from "@openstatus/ui"; 20 20 21 21 import useWindowSize from "@/hooks/use-window-size"; 22 - import { blacklistDates, getMonitorList, getStatus } from "@/lib/tracker"; 22 + import type { CleanMonitor } from "@/lib/tracker"; 23 + import { blacklistDates, cleanData, getStatus } from "@/lib/tracker"; 23 24 24 25 // What would be cool is tracker that turn from green to red depending on the number of errors 25 26 const tracker = cva("h-10 rounded-full flex-1", { ··· 28 29 up: "bg-green-500 data-[state=open]:bg-green-600", 29 30 down: "bg-red-500 data-[state=open]:bg-red-600", 30 31 degraded: "bg-yellow-500 data-[state=open]:bg-yellow-600", 31 - empty: "bg-muted-foreground/20", 32 - blacklist: "bg-green-400", 32 + empty: "bg-muted-foreground/20 data-[state=open]:bg-muted-foreground/30", 33 + blacklist: "bg-green-400 data-[state=open]:bg-green-600", 33 34 }, 34 35 }, 35 36 defaultVariants: { ··· 55 56 description, 56 57 }: TrackerProps) { 57 58 const { isMobile } = useWindowSize(); 58 - const maxSize = React.useMemo(() => (isMobile ? 35 : 45), [isMobile]); // TODO: it is better than how it is currently, but creates a small content shift on first render 59 - const { monitors, placeholder, uptime } = getMonitorList(data, { 60 - maxSize, 61 - context, 62 - }); 59 + // TODO: it is better than how it was currently, but creates a small content shift on first render 60 + const maxSize = React.useMemo(() => (isMobile ? 35 : 45), [isMobile]); 61 + const { bars, uptime } = cleanData({ data, last: maxSize }); 63 62 64 63 return ( 65 64 <div className="flex flex-col"> ··· 70 69 <MoreInfo {...{ url, id, context, description }} /> 71 70 ) : null} 72 71 </div> 73 - <p className="text-muted-foreground shrink-0 font-light"> 74 - {uptime}% uptime 75 - </p> 72 + <p className="text-muted-foreground shrink-0 font-light">{uptime}</p> 76 73 </div> 77 74 <div className="relative h-full w-full"> 78 - <div className="flex gap-0.5"> 79 - {Array(Math.abs(placeholder.length - monitors.length)) 80 - .fill(null) 81 - .map((_, i) => { 82 - // TODO: use `Bar` component and `HoverCard` with empty state 83 - return <div key={i} className={tracker({ variant: "empty" })} />; 84 - })} 85 - {monitors.map((props) => { 75 + <div className="flex flex-row-reverse gap-0.5"> 76 + {bars.map((props) => { 86 77 return ( 87 78 <Bar key={props.cronTimestamp} context={context} {...props} /> 88 79 ); ··· 134 125 ok, 135 126 avgLatency, 136 127 cronTimestamp, 128 + blacklist, 137 129 context, 138 - }: Monitor & Pick<TrackerProps, "context">) => { 130 + }: CleanMonitor & Pick<TrackerProps, "context">) => { 139 131 const [open, setOpen] = React.useState(false); 140 132 const ratio = ok / count; 141 - // FIX: this is an easy way to detect if cronTimestamps have been aggregated 142 - const isMidnight = String(cronTimestamp).endsWith("00000"); 143 133 const date = new Date(cronTimestamp); 144 - const toDate = isMidnight ? date.setDate(date.getDate() + 1) : cronTimestamp; 145 - const dateFormat = isMidnight ? "dd/MM/yy" : "dd/MM/yy HH:mm"; 146 - 147 - const isBlackListed = Object.keys(blacklistDates).includes( 148 - String(cronTimestamp), 149 - ); 134 + const toDate = date.setDate(date.getDate() + 1); 135 + const dateFormat = "dd/MM/yy"; 150 136 151 137 return ( 152 138 <HoverCard ··· 158 144 <HoverCardTrigger onClick={() => setOpen(true)} asChild> 159 145 <div 160 146 className={tracker({ 161 - variant: isBlackListed ? "blacklist" : getStatus(ratio).variant, 147 + variant: blacklist ? "blacklist" : getStatus(ratio).variant, 162 148 })} 163 149 /> 164 150 </HoverCardTrigger> 165 151 <HoverCardContent side="top" className="w-64"> 166 - {isBlackListed ? ( 167 - <p className="text-muted-foreground text-xs"> 168 - {blacklistDates[cronTimestamp]} 169 - </p> 152 + {blacklist ? ( 153 + <p className="text-muted-foreground text-xs">{blacklist}</p> 170 154 ) : ( 171 155 <> 172 156 <div className="flex justify-between">
+103 -88
apps/web/src/lib/tracker.ts
··· 5 5 type GetStatusReturnType = { 6 6 label: string; 7 7 variant: StatusVariant; 8 - twColor: string; 9 - twBgColor: string; 10 8 }; 11 9 12 10 /** ··· 19 17 return { 20 18 label: "Missing", 21 19 variant: "empty", 22 - twColor: "text-gray-500", 23 - twBgColor: "bg-gray-500", 24 20 }; 25 21 if (ratio >= 0.98) 26 22 return { 27 23 label: "Operational", 28 24 variant: "up", 29 - twColor: "text-green-500", 30 - twBgColor: "bg-green-500", 31 25 }; 32 26 if (ratio >= 0.5) 33 27 return { 34 28 label: "Degraded", 35 29 variant: "degraded", 36 - twColor: "text-yellow-500", 37 - twBgColor: "bg-yellow-500", 38 30 }; 39 31 return { 40 32 label: "Downtime", 41 33 variant: "down", 42 - twColor: "text-red-500", 43 - twBgColor: "bg-red-500", 44 34 }; 45 35 }; 46 36 37 + // TODO: move into Class component sharing the same `data` 38 + 39 + export type CleanMonitor = Monitor & { 40 + blacklist?: string; 41 + }; 42 + 43 + export function cleanData({ data, last }: { data: Monitor[]; last: number }) { 44 + const today = new Date(); 45 + 46 + const currentDay = new Date(today); 47 + currentDay.setUTCDate(today.getDate()); 48 + currentDay.setUTCHours(0, 0, 0, 0); 49 + 50 + const lastDay = new Date(today); 51 + lastDay.setUTCDate(today.getDate() - last); 52 + lastDay.setUTCHours(0, 0, 0, 0); 53 + 54 + const dateSequence = generateDateSequence(lastDay, currentDay); 55 + 56 + const filledData = fillEmptyData(data, dateSequence); 57 + 58 + const uptime = getTotalUptimeString(filledData); 59 + 60 + return { bars: filledData, uptime }; // possibly only return filledData? 61 + } 62 + 63 + function fillEmptyData(data: Monitor[], dateSequence: Date[]) { 64 + const filledData: CleanMonitor[] = []; 65 + let dataIndex = 0; 66 + 67 + for (const date of dateSequence) { 68 + const timestamp = date.getTime(); 69 + const cronTimestamp = 70 + dataIndex < data.length ? data[dataIndex].cronTimestamp : undefined; 71 + if ( 72 + cronTimestamp && 73 + areDatesEqualByDayMonthYear(new Date(cronTimestamp), date) 74 + ) { 75 + const isBlacklisted = isInBlacklist(cronTimestamp); 76 + 77 + /** 78 + * automatically remove the data from the array to avoid wrong uptime 79 + * that provides time to remove cursed logs from tinybird via mv migration 80 + */ 81 + if (isBlacklisted) { 82 + filledData.push({ 83 + ...emptyData(timestamp), 84 + blacklist: blacklistDates[cronTimestamp], 85 + }); 86 + } else { 87 + filledData.push(data[dataIndex]); 88 + } 89 + dataIndex++; 90 + } else { 91 + filledData.push(emptyData(timestamp)); 92 + } 93 + } 94 + 95 + return filledData; 96 + } 97 + 98 + function emptyData(cronTimestamp: number) { 99 + return { 100 + count: 0, 101 + ok: 0, 102 + avgLatency: 0, 103 + cronTimestamp, 104 + }; 105 + } 106 + 47 107 /** 48 - * 49 - * @param data Array of monitors from tinybird 108 + * equal UTC days - fixes issue with daylight saving 109 + * @param date1 110 + * @param date2 50 111 * @returns 51 112 */ 52 - export function getMonitorList( 53 - data: Monitor[], 54 - { maxSize, context }: { maxSize: number; context?: string }, 55 - ) { 56 - const slicedData = data.slice(0, maxSize).reverse(); 113 + function areDatesEqualByDayMonthYear(date1: Date, date2: Date) { 114 + date1.setUTCDate(date1.getDate()); 115 + date1.setUTCHours(0, 0, 0, 0); 116 + 117 + date2.setUTCDate(date2.getDate()); 118 + date2.setUTCHours(0, 0, 0, 0); 57 119 58 - const filledData: Monitor[] = 59 - context === "play" ? slicedData : fillMissingDates(slicedData); 120 + return date1.toUTCString() === date2.toUTCString(); 121 + } 60 122 61 - const placeholderData: null[] = Array( 62 - Math.max(maxSize, filledData.length), // we might have more data than maxSize as we are adding empty dates in between 63 - ).fill(null); 123 + /** 124 + * 125 + * @param startDate 126 + * @param endDate 127 + * @returns 128 + */ 129 + export function generateDateSequence(startDate: Date, endDate: Date): Date[] { 130 + const dateSequence: Date[] = []; 131 + const currentDate = new Date(startDate); 64 132 65 - const totalUptime = getTotalUptime(filledData); 133 + while (currentDate <= endDate) { 134 + dateSequence.push(new Date(currentDate)); 135 + currentDate.setUTCDate(currentDate.getDate() + 1); 136 + } 66 137 67 - return { 68 - monitors: filledData, 69 - placeholder: placeholderData, 70 - uptime: totalUptime, 71 - }; 138 + return dateSequence.reverse(); 72 139 } 73 140 74 141 export function getTotalUptime(data: Monitor[]) { ··· 83 150 ok: 0, 84 151 }, 85 152 ); 86 - 87 - const uptime = 88 - reducedData.count !== 0 89 - ? ((reducedData.ok / reducedData.count) * 100).toFixed(2) 90 - : ""; 91 - 92 - return uptime; 153 + return reducedData; 93 154 } 94 155 95 - /** 96 - * It happens that some monitors don't have data for a specific date 97 - * This function fills the missing dates in between 98 - * @param data Array of monitors from tinybird 99 - * @returns 100 - */ 101 - export function fillMissingDates(data: Monitor[]) { 102 - if (data.length === 0) { 103 - return []; 104 - } 156 + export function getTotalUptimeString(data: Monitor[]) { 157 + const reducedData = getTotalUptime(data); 158 + const uptime = (reducedData.ok / reducedData.count) * 100; 105 159 106 - const startDate = new Date(data[0].cronTimestamp); 107 - const endDate = new Date( 108 - new Date(data[data.length - 1].cronTimestamp).setHours(23, 59, 59, 999), 109 - ); 110 - 111 - // The reason why we cannot use `date-fns` is because it isn't supported on the edge 112 - // const dateSequence = eachDayOfInterval({start:startDate, end:endDate}) 113 - const dateSequence = generateDateSequence(startDate, endDate); 114 - 115 - const filledData: Monitor[] = []; 116 - let dataIndex = 0; 160 + if (isNaN(uptime)) return ""; 117 161 118 - for (const currentDate of dateSequence) { 119 - const currentTimestamp = currentDate.getTime(); 120 - if ( 121 - dataIndex < data.length && 122 - (data[dataIndex].cronTimestamp === currentTimestamp || 123 - data[dataIndex].cronTimestamp + 60 * 60 * 1000 === currentTimestamp) 124 - ) { 125 - filledData.push(data[dataIndex]); 126 - dataIndex++; 127 - } else { 128 - filledData.push({ 129 - count: 0, 130 - ok: 0, 131 - avgLatency: 0, 132 - cronTimestamp: currentTimestamp, 133 - }); 134 - } 135 - } 136 - 137 - return filledData; 162 + return `${uptime.toFixed(2)}% uptime`; 138 163 } 139 164 140 - // Function to generate a sequence of dates between two dates 141 - export function generateDateSequence(startDate: Date, endDate: Date) { 142 - const dateSequence = []; 143 - const currentDate = new Date(startDate); 144 - 145 - while (currentDate <= endDate) { 146 - dateSequence.push(new Date(currentDate)); 147 - currentDate.setDate(currentDate.getDate() + 1); 148 - } 149 - 150 - return dateSequence; 165 + export function isInBlacklist(timestamp: number) { 166 + return Object.keys(blacklistDates).includes(timestamp.toString()); 151 167 } 152 168 153 169 /** ··· 158 174 "OpenStatus faced issues between 24.08. and 27.08., preventing data collection.", 159 175 1693008000000: 160 176 "OpenStatus faced issues between 24.08. and 27.08., preventing data collection.", 161 - // Downtime on 18. Oct. 2023 between 20h40 - 23h55 - TOD0: remove error logs 162 177 1697587200000: 163 178 "OpenStatus migrated from Vercel to Fly to improve the performance of the checker.", 164 179 };