Openstatus www.openstatus.dev

fix: timezones (#469)

* chore: timezone playground

* fix: truncate

* fix: missing timezone props

* wip: small adjustements

* fix: og image monitor id

* chore: activate cache

* chore: update tinybird schema and docs

authored by

Maximilian Kaske and committed by
GitHub
fa4c7d63 e39faa91

+356 -89
+48 -18
apps/docs/developer-guide/setup-env.mdx
··· 164 164 165 165 ### Datasource 166 166 167 - We ingest the monitor response into the `ping_response__v3` table. If you want 167 + We ingest the monitor response into the `ping_response__v4` table. If you want 168 168 to have some dummy data to start with, check out the 169 169 [csv](/dummy/ping_response__v3.csv) file. 170 170 ··· 181 181 182 182 We currently have three pipes: 183 183 184 - 1. `monitor_list__v0`: returns aggregated data from the `ping_response` table. 184 + 1. `status_timezone__v0`: returns aggregated data from the `ping_response` 185 + table. 185 186 2. `response_list__v0`: returns the list of responses from the `ping_response` 186 187 table. 187 188 3. `home_stats__v0`: returns the total amount of pings during a timeframe from ··· 293 294 > Please make sure while importing you edit the table header fields in 294 295 > camelcase . 295 296 296 - 6. Create pipe from the datasource name as a `response_list__v0` and 297 - `monitor_list__v0` 297 + 6. Create pipe from the datasource name as a `response_list__v1` and 298 + `status_timezone__v0` 298 299 299 300 7. Inside `response_list__v0` pipe, create a endpoint by running a following 300 301 query : ··· 302 303 ``` 303 304 % 304 305 SELECT id, latency, monitorId, pageId, region, statusCode, timestamp, url, workspaceId, cronTimestamp 305 - FROM ping_response__v3 306 + FROM ping_response__v4 306 307 WHERE monitorId = {{ String(monitorId, 'openstatusPing') }} 307 308 {% if defined(region) %} 308 309 AND region = {{ String(region) }} ··· 320 321 LIMIT {{Int32(limit, 1000)}} 321 322 ``` 322 323 323 - 8. Inside `monitor_list__v0` pipe , create a endpoint by running a following 324 + 8. Inside `status_timezone__v0` pipe , create a endpoint by running a following 324 325 query : 325 326 326 327 ``` 327 - % 328 + VERSION 0 329 + 330 + NODE group_by_cronTimestamp 331 + SQL > 332 + 333 + % 334 + SELECT 335 + toDateTime(cronTimestamp / 1000, 'UTC') AS day, 336 + -- only for debugging purposes 337 + toTimezone(day, {{ String(timezone, 'Europe/Berlin') }}) as with_timezone, 338 + toStartOfDay(with_timezone) as start_of_day, 339 + avg(latency) AS avgLatency, 340 + count() AS count, 341 + count(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok 342 + FROM ping_response__v4 343 + WHERE 344 + (day IS NOT NULL) 345 + AND (day != 0) 346 + AND monitorId = {{ String(monitorId, '1') }} 347 + -- By default, we only only query the last 45 days 348 + AND cronTimestamp >= toUnixTimestamp64Milli( 349 + toDateTime64(toStartOfDay(date_sub(DAY, 45, now())), 3) 350 + ) 351 + GROUP BY cronTimestamp, monitorId 352 + ORDER BY day DESC 353 + 354 + 355 + 356 + NODE group_by_day 357 + SQL > 358 + 359 + % 328 360 SELECT 329 - {% if defined(groupBy) and groupBy == "day" %} 330 - toUnixTimestamp64Milli(toDateTime64(DATE(cronTimestamp / 1000), 3)) 331 - {% else %} 332 - cronTimestamp 333 - {% end %} 334 - AS cronTimestamp, ROUND(AVG(latency)) as avgLatency, COUNT(*) as count, COUNT(CASE WHEN statusCode BETWEEN 200 AND 299 THEN 1 ELSE NULL END) as ok 335 - FROM ping_response__v3 336 - WHERE monitorId = {{ String(monitorId, 'openstatusPing') }} AND cronTimestamp IS NOT NULL AND cronTimestamp != 0 337 - GROUP BY cronTimestamp 338 - ORDER BY cronTimestamp DESC 339 - LIMIT {{Int32(limit, 1000)}} 361 + start_of_day as day, 362 + sum(count) as count, 363 + sum(ok) as ok, 364 + round(avg(avgLatency)) as avgLatency 365 + FROM group_by_cronTimestamp 366 + GROUP BY start_of_day 367 + ORDER BY start_of_day DESC 368 + LIMIT {{ Int32(limit, 100) }} 369 + 340 370 341 371 ``` 342 372
+7 -1
apps/web/src/app/api/og/route.tsx
··· 1 + import { headers } from "next/headers"; 1 2 import { ImageResponse } from "next/server"; 2 3 3 4 import { DESCRIPTION, TITLE } from "@/app/shared-metadata"; ··· 13 14 }; 14 15 15 16 const LIMIT = 40; 17 + const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 16 18 17 19 const interRegular = fetch( 18 20 new URL("../../../public/fonts/Inter-Regular.ttf", import.meta.url), ··· 41 43 ? searchParams.get("monitorId") 42 44 : undefined; 43 45 46 + const headersList = headers(); 47 + const timezone = headersList.get("x-vercel-ip-timezone") || currentTimezone; 48 + 44 49 // currently, we only show the tracker for a single(!) monitor 45 50 const data = 46 51 (monitorId && 47 52 (await getMonitorListData({ 48 53 monitorId, 49 54 limit: LIMIT, 55 + timezone, 50 56 }))) || 51 57 []; 52 58 53 - const { bars, uptime } = cleanData({ data, last: LIMIT }); 59 + const { bars, uptime } = cleanData(data, LIMIT); 54 60 55 61 return new ImageResponse( 56 62 (
+101
apps/web/src/app/play/_components/timezone-combobox.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import { usePathname, useRouter } from "next/navigation"; 5 + import { Check, ChevronsUpDown } from "lucide-react"; 6 + 7 + import { 8 + Button, 9 + Command, 10 + CommandEmpty, 11 + CommandGroup, 12 + CommandInput, 13 + CommandItem, 14 + Popover, 15 + PopoverContent, 16 + PopoverTrigger, 17 + } from "@openstatus/ui"; 18 + 19 + import useUpdateSearchParams from "@/hooks/use-update-search-params"; 20 + import { cn } from "@/lib/utils"; 21 + 22 + const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 23 + const supportedTimezones = Intl.supportedValuesOf("timeZone"); 24 + 25 + export function TimezoneCombobox({ defaultValue }: { defaultValue?: string }) { 26 + const [open, setOpen] = React.useState(false); 27 + const pathname = usePathname(); 28 + const router = useRouter(); 29 + const updateSearchParams = useUpdateSearchParams(); 30 + 31 + const value = defaultValue?.toLowerCase(); 32 + 33 + const timezones = supportedTimezones.map((timezone) => ({ 34 + value: timezone.toLowerCase(), 35 + label: timezone, 36 + })); 37 + 38 + return ( 39 + <Popover open={open} onOpenChange={setOpen}> 40 + <PopoverTrigger asChild> 41 + <Button 42 + variant="outline" 43 + role="combobox" 44 + aria-expanded={open} 45 + className="w-[220px] justify-between" 46 + > 47 + <span className="truncate"> 48 + {value 49 + ? timezones.find((timezone) => timezone.value === value)?.label 50 + : "Select timezone..."} 51 + {defaultValue?.toLowerCase() === currentTimezone?.toLowerCase() ? ( 52 + <span className="text-muted-foreground ml-1 text-xs font-light"> 53 + (default) 54 + </span> 55 + ) : null} 56 + </span> 57 + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> 58 + </Button> 59 + </PopoverTrigger> 60 + <PopoverContent className="w-[300px] p-0"> 61 + <Command> 62 + <CommandInput placeholder="Search timezone..." /> 63 + <CommandEmpty>No timezone found.</CommandEmpty> 64 + <CommandGroup className="max-h-[300px] overflow-y-auto"> 65 + {timezones.map((timezone) => ( 66 + <CommandItem 67 + key={timezone.value} 68 + value={timezone.value} 69 + onSelect={(currentValue) => { 70 + setOpen(false); 71 + 72 + // update search params 73 + const searchParams = updateSearchParams({ 74 + timezone: 75 + currentValue === value 76 + ? null // remove search param and use default timezone 77 + : timezones.find( 78 + (timezone) => timezone.value === currentValue, 79 + )?.label || null, 80 + }); 81 + 82 + // refresh page with new search params 83 + router.replace(`${pathname}?${searchParams}`); 84 + router.refresh(); 85 + }} 86 + > 87 + <Check 88 + className={cn( 89 + "mr-2 h-4 w-4", 90 + value === timezone.value ? "opacity-100" : "opacity-0", 91 + )} 92 + /> 93 + {timezone.label} 94 + </CommandItem> 95 + ))} 96 + </CommandGroup> 97 + </Command> 98 + </PopoverContent> 99 + </Popover> 100 + ); 101 + }
+2 -2
apps/web/src/app/play/layout.tsx
··· 14 14 ...defaultMetadata, 15 15 twitter: { 16 16 ...twitterMetadata, 17 - images: [`/api/og?monitorId=openstatus`], 17 + images: [`/api/og?monitorId=1`], 18 18 }, 19 19 openGraph: { 20 20 ...ogMetadata, 21 - images: [`/api/og?monitorId=openstatus`], 21 + images: [`/api/og?monitorId=1`], 22 22 }, 23 23 }; 24 24
+55 -7
apps/web/src/app/play/page.tsx
··· 1 + import { headers } from "next/headers"; 2 + import * as z from "zod"; 3 + 4 + import { Label } from "@openstatus/ui"; 5 + 1 6 import { Tracker } from "@/components/tracker"; 2 7 import { getHomeMonitorListData } from "@/lib/tb"; 8 + import { TimezoneCombobox } from "./_components/timezone-combobox"; 9 + 10 + const supportedTimezones = Intl.supportedValuesOf("timeZone"); 11 + const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 12 + 13 + /** 14 + * allowed URL search params 15 + */ 16 + const searchParamsSchema = z.object({ 17 + timezone: z.string().optional(), 18 + }); 3 19 4 - export default async function PlayPage() { 5 - const data = await getHomeMonitorListData(); 20 + export default async function PlayPage({ 21 + searchParams, 22 + }: { 23 + searchParams: { [key: string]: string | string[] | undefined }; 24 + }) { 25 + const headersList = headers(); 26 + const timezone = headersList.get("x-vercel-ip-timezone") || currentTimezone; 27 + 28 + const search = searchParamsSchema.safeParse(searchParams); 29 + 30 + function getDefaultValue() { 31 + if ( 32 + search.success && 33 + search.data.timezone && 34 + supportedTimezones.includes(search.data.timezone) 35 + ) { 36 + return search.data.timezone; 37 + } 38 + return timezone; 39 + } 40 + 41 + const defaultValue = getDefaultValue(); 42 + 43 + const data = await getHomeMonitorListData({ timezone: defaultValue }); 44 + 6 45 return ( 7 - <div className="relative flex flex-col items-center justify-center gap-4"> 8 - <p className="font-cal mb-1 text-3xl">Status</p> 9 - <p className="text-muted-foreground text-lg font-light"> 10 - Build your own within seconds. 11 - </p> 46 + <div className="relative grid gap-4"> 47 + <div className="mx-auto grid gap-4 text-center"> 48 + <p className="font-cal mb-1 text-3xl">Status</p> 49 + <p className="text-muted-foreground text-lg font-light"> 50 + Build your own within seconds. 51 + </p> 52 + </div> 12 53 <div className="mx-auto w-full max-w-md"> 13 54 {data && ( 14 55 <Tracker ··· 17 58 name="Ping" 18 59 url="https://www.openstatus.dev/api/ping" 19 60 context="play" 61 + timezone={defaultValue} 20 62 /> 21 63 )} 64 + </div> 65 + <div className="mt-6 flex justify-start"> 66 + <div className="grid items-center gap-1"> 67 + <Label className="text-muted-foreground text-xs">Timezone</Label> 68 + <TimezoneCombobox defaultValue={defaultValue} /> 69 + </div> 22 70 </div> 23 71 </div> 24 72 );
+7 -1
apps/web/src/components/marketing/example.tsx
··· 1 1 import { Suspense } from "react"; 2 + import { headers } from "next/headers"; 2 3 import Link from "next/link"; 3 4 4 5 import { Button } from "@openstatus/ui"; ··· 6 7 import { Shell } from "@/components/dashboard/shell"; 7 8 import { Tracker } from "@/components/tracker"; 8 9 import { getHomeMonitorListData } from "@/lib/tb"; 10 + 11 + const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 9 12 10 13 export async function Example() { 11 14 return ( ··· 35 38 } 36 39 37 40 async function ExampleTracker() { 38 - const data = await getHomeMonitorListData(); 41 + const headersList = headers(); 42 + const timezone = headersList.get("x-vercel-ip-timezone") || currentTimezone; 43 + const data = await getHomeMonitorListData({ timezone }); 39 44 if (!data) return null; 40 45 return ( 41 46 <Tracker ··· 44 49 name="Ping" 45 50 context="play" 46 51 url="https://www.openstatus.dev/api/ping" 52 + timezone={timezone} 47 53 /> 48 54 ); 49 55 }
+11 -1
apps/web/src/components/status-page/monitor.tsx
··· 1 + import { headers } from "next/headers"; 1 2 import type { z } from "zod"; 2 3 3 4 import type { selectPublicMonitorSchema } from "@openstatus/db/src/schema"; 4 5 5 6 import { getMonitorListData } from "@/lib/tb"; 6 7 import { Tracker } from "../tracker"; 8 + 9 + const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 7 10 8 11 export const Monitor = async ({ 9 12 monitor, 10 13 }: { 11 14 monitor: z.infer<typeof selectPublicMonitorSchema>; 12 15 }) => { 13 - const data = await getMonitorListData({ monitorId: String(monitor.id) }); 16 + const headersList = headers(); 17 + const timezone = headersList.get("x-vercel-ip-timezone") || currentTimezone; 18 + 19 + const data = await getMonitorListData({ 20 + monitorId: String(monitor.id), 21 + timezone, 22 + }); 14 23 if (!data) return <div>Something went wrong</div>; 24 + 15 25 return ( 16 26 <Tracker 17 27 data={data}
+10 -4
apps/web/src/components/tracker.tsx
··· 45 45 name: string; 46 46 description?: string; 47 47 context?: "play" | "status-page"; // TODO: we might need to extract those two different use cases - for now it's ok I'd say. 48 + timezone?: string; 48 49 } 49 50 50 51 export function Tracker({ ··· 54 55 name, 55 56 context = "status-page", 56 57 description, 58 + timezone, 57 59 }: TrackerProps) { 58 60 const { isMobile } = useWindowSize(); 59 61 // TODO: it is better than how it was currently, but creates a small content shift on first render 60 62 const maxSize = React.useMemo(() => (isMobile ? 35 : 45), [isMobile]); 61 - const { bars, uptime } = cleanData({ data, last: maxSize }); 63 + const { bars, uptime } = cleanData(data, maxSize, timezone); 62 64 63 65 return ( 64 66 <div className="flex flex-col"> ··· 134 136 const toDate = date.setDate(date.getDate() + 1); 135 137 const dateFormat = "dd/MM/yy"; 136 138 139 + const className = tracker({ 140 + variant: blacklist ? "blacklist" : getStatus(ratio).variant, 141 + }); 142 + // console.log({ className, blacklist }); 143 + 137 144 return ( 138 145 <HoverCard 139 146 openDelay={100} ··· 143 150 > 144 151 <HoverCardTrigger onClick={() => setOpen(true)} asChild> 145 152 <div 146 - className={tracker({ 147 - variant: blacklist ? "blacklist" : getStatus(ratio).variant, 148 - })} 153 + // suppressHydrationWarning 154 + className={className} 149 155 /> 150 156 </HoverCardTrigger> 151 157 <HoverCardContent side="top" className="w-64">
+4 -2
apps/web/src/lib/tb.ts
··· 37 37 } 38 38 39 39 // Includes caching of data for 10 minutes 40 - export async function getHomeMonitorListData() { 40 + export async function getHomeMonitorListData( 41 + props: Pick<MonitorListParams, "timezone">, 42 + ) { 41 43 try { 42 - const res = await getHomeMonitorList(tb)({ monitorId: "1" }); 44 + const res = await getHomeMonitorList(tb)({ monitorId: "1", ...props }); 43 45 return res.data; 44 46 } catch (e) { 45 47 console.error(e);
+45 -27
apps/web/src/lib/tracker.ts
··· 36 36 37 37 // TODO: move into Class component sharing the same `data` 38 38 39 - export type CleanMonitor = Monitor & { 39 + // FIXME: name TrackerMonitor 40 + 41 + export type CleanMonitor = { 42 + count: number; 43 + ok: number; 44 + avgLatency: number; 45 + cronTimestamp: number; 40 46 blacklist?: string; 41 47 }; 42 48 43 - export function cleanData({ data, last }: { data: Monitor[]; last: number }) { 44 - const today = new Date(); 49 + /** 50 + * Clean the data to show only the last X days 51 + * @param data array of monitors 52 + * @param last number of days to show 53 + * @param timeZone timezone of the monitor 54 + * @returns 55 + */ 56 + export function cleanData(data: Monitor[], last: number, timeZone?: string) { 57 + const today = new Date(new Date().toLocaleString("en-US", { timeZone })); 45 58 46 59 const currentDay = new Date(today); 47 - currentDay.setUTCDate(today.getDate()); 48 - currentDay.setUTCHours(0, 0, 0, 0); 60 + currentDay.setDate(today.getDate()); 61 + currentDay.setHours(0, 0, 0, 0); 49 62 50 63 const lastDay = new Date(today); 51 - lastDay.setUTCDate(today.getDate() - last); 52 - lastDay.setUTCHours(0, 0, 0, 0); 64 + lastDay.setDate(today.getDate() - last); 65 + lastDay.setHours(0, 0, 0, 0); 53 66 54 67 const dateSequence = generateDateSequence(lastDay, currentDay); 55 68 ··· 67 80 for (const date of dateSequence) { 68 81 const timestamp = date.getTime(); 69 82 const cronTimestamp = 70 - dataIndex < data.length ? data[dataIndex].cronTimestamp : undefined; 83 + dataIndex < data.length ? data[dataIndex].day.getTime() : undefined; 84 + 71 85 if ( 72 86 cronTimestamp && 73 - areDatesEqualByDayMonthYear(new Date(cronTimestamp), date) 87 + areDatesEqualByDayMonthYear(date, new Date(cronTimestamp)) 74 88 ) { 75 - const isBlacklisted = isInBlacklist(cronTimestamp); 89 + const blacklist = isInBlacklist(cronTimestamp); 76 90 77 91 /** 78 92 * automatically remove the data from the array to avoid wrong uptime 79 93 * that provides time to remove cursed logs from tinybird via mv migration 80 94 */ 81 - if (isBlacklisted) { 95 + if (blacklist) { 82 96 filledData.push({ 83 - ...emptyData(timestamp), 84 - blacklist: blacklistDates[cronTimestamp], 97 + ...emptyData(cronTimestamp), 98 + blacklist, 85 99 }); 86 100 } else { 87 - filledData.push(data[dataIndex]); 101 + const { day, ...props } = data[dataIndex]; 102 + filledData.push({ ...props, cronTimestamp }); 88 103 } 89 104 dataIndex++; 90 105 } else { ··· 105 120 } 106 121 107 122 /** 108 - * equal UTC days - fixes issue with daylight saving 123 + * equal days - fixes issue with daylight saving 109 124 * @param date1 110 125 * @param date2 111 126 * @returns 112 127 */ 113 128 function areDatesEqualByDayMonthYear(date1: Date, date2: Date) { 114 - date1.setUTCDate(date1.getDate()); 115 - date1.setUTCHours(0, 0, 0, 0); 129 + date1.setDate(date1.getDate()); 130 + date1.setHours(0, 0, 0, 0); 116 131 117 - date2.setUTCDate(date2.getDate()); 118 - date2.setUTCHours(0, 0, 0, 0); 132 + date2.setDate(date2.getDate()); 133 + date2.setHours(0, 0, 0, 0); 119 134 120 135 return date1.toUTCString() === date2.toUTCString(); 121 136 } ··· 132 147 133 148 while (currentDate <= endDate) { 134 149 dateSequence.push(new Date(currentDate)); 135 - currentDate.setUTCDate(currentDate.getDate() + 1); 150 + currentDate.setDate(currentDate.getDate() + 1); 136 151 } 137 152 138 153 return dateSequence.reverse(); 139 154 } 140 155 141 - export function getTotalUptime(data: Monitor[]) { 156 + export function getTotalUptime(data: { ok: number; count: number }[]) { 142 157 const reducedData = data.reduce( 143 158 (prev, curr) => { 144 159 prev.ok += curr.ok; ··· 153 168 return reducedData; 154 169 } 155 170 156 - export function getTotalUptimeString(data: Monitor[]) { 171 + export function getTotalUptimeString(data: { ok: number; count: number }[]) { 157 172 const reducedData = getTotalUptime(data); 158 173 const uptime = (reducedData.ok / reducedData.count) * 100; 159 174 ··· 163 178 } 164 179 165 180 export function isInBlacklist(timestamp: number) { 166 - return Object.keys(blacklistDates).includes(timestamp.toString()); 181 + const el = Object.keys(blacklistDates).find((date) => 182 + areDatesEqualByDayMonthYear(new Date(date), new Date(timestamp)), 183 + ); 184 + return el ? blacklistDates[el] : undefined; 167 185 } 168 186 169 187 /** 170 188 * Blacklist dates where we had issues with data collection 171 189 */ 172 - export const blacklistDates: Record<number, string> = { 173 - 1692921600000: 190 + export const blacklistDates: Record<string, string> = { 191 + "Fri Aug 25 2023": 174 192 "OpenStatus faced issues between 24.08. and 27.08., preventing data collection.", 175 - 1693008000000: 193 + "Sat Aug 26 2023": 176 194 "OpenStatus faced issues between 24.08. and 27.08., preventing data collection.", 177 - 1697587200000: 195 + "Wed Oct 18 2023": 178 196 "OpenStatus migrated from Vercel to Fly to improve the performance of the checker.", 179 197 };
+2
packages/tinybird/datasources/aggregated_monitor_per_day_mv.datasource packages/tinybird/_legacy/aggregated_monitor_per_day_mv.datasource
··· 1 + # REMINDER: legacy 2 + 1 3 # Data Source created from Pipe 'aggregated_monitor_day_mv' 2 4 VERSION 0 3 5
+3 -1
packages/tinybird/pipes/aggregated_monitor_day_mv.pipe packages/tinybird/_legacy/aggregated_monitor_day_mv.pipe
··· 1 + # REMINDER: legacy 2 + 1 3 VERSION 0 2 4 3 5 NODE aggregated_monitor_day_0 ··· 9 11 avgState(latency) AS avgLatency, 10 12 countState() AS count, 11 13 countState(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok 12 - FROM ping_response__v3 14 + FROM ping_response__v4 13 15 WHERE (day IS NOT NULL) AND (day != 0) 14 16 GROUP BY 15 17 day,
+1 -1
packages/tinybird/pipes/home_stats.pipe
··· 5 5 6 6 % 7 7 SELECT COUNT(*) as count 8 - FROM ping_response__v3 8 + FROM ping_response__v4 9 9 {% if defined(period) %} 10 10 {% if String(period) == "1h" %} 11 11 WHERE cronTimestamp > toUnixTimestamp(now() - INTERVAL 1 HOUR) * 1000
-14
packages/tinybird/pipes/monitor_list.pipe
··· 1 - VERSION 1 2 - 3 - NODE monitor_list_0 4 - SQL > 5 - 6 - % 7 - SELECT day as cronTimestamp, round(avgMerge(avgLatency)) avgLatency, countMerge(ok) ok, countMerge(count) count 8 - FROM aggregated_monitor_per_day_mv 9 - WHERE monitorId = {{ String(monitorId, 'openstatusPing') }} 10 - GROUP BY day, monitorId 11 - ORDER BY cronTimestamp DESC 12 - LIMIT {{Int32(limit, 100)}} 13 - 14 -
+47
packages/tinybird/pipes/status_timezone.pipe
··· 1 + VERSION 0 2 + 3 + DESCRIPTION > 4 + TODO: descripe what it is for! 5 + 6 + 7 + NODE group_by_cronTimestamp 8 + SQL > 9 + 10 + % 11 + SELECT 12 + toDateTime(cronTimestamp / 1000, 'UTC') AS day, 13 + -- only for debugging purposes 14 + toTimezone(day, {{ String(timezone, 'Europe/Berlin') }}) as with_timezone, 15 + toStartOfDay(with_timezone) as start_of_day, 16 + avg(latency) AS avgLatency, 17 + count() AS count, 18 + count(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok 19 + FROM ping_response__v4 20 + WHERE 21 + (day IS NOT NULL) 22 + AND (day != 0) 23 + AND monitorId = {{ String(monitorId, '1') }} 24 + -- By default, we only only query the last 45 days 25 + AND cronTimestamp >= toUnixTimestamp64Milli( 26 + toDateTime64(toStartOfDay(date_sub(DAY, 45, now())), 3) 27 + ) 28 + GROUP BY cronTimestamp, monitorId 29 + ORDER BY day DESC 30 + 31 + 32 + 33 + NODE group_by_day 34 + SQL > 35 + 36 + % 37 + SELECT 38 + start_of_day as day, 39 + sum(count) as count, 40 + sum(ok) as ok, 41 + round(avg(avgLatency)) as avgLatency 42 + FROM group_by_cronTimestamp 43 + GROUP BY start_of_day 44 + ORDER BY start_of_day DESC 45 + LIMIT {{ Int32(limit, 100) }} 46 + 47 +
+7 -5
packages/tinybird/src/client.ts
··· 24 24 parameters: tbParameterResponseList, 25 25 data: tbBuildResponseList, 26 26 opts: { 27 - cache: "no-store", 27 + // cache: "no-store", 28 + revalidate: 30, // 30 seconds cache 28 29 }, 29 30 }); 30 31 } 31 32 32 33 export function getMonitorList(tb: Tinybird) { 33 34 return tb.buildPipe({ 34 - pipe: "monitor_list__v1", 35 + pipe: "status_timezone__v0", 35 36 parameters: tbParameterMonitorList, 36 37 data: tbBuildMonitorList, 37 38 opts: { 38 - cache: "no-store", 39 + // cache: "no-store", 40 + revalidate: 30, // 30 seconds cache 39 41 }, 40 42 }); 41 43 } ··· 47 49 */ 48 50 export function getHomeMonitorList(tb: Tinybird) { 49 51 return tb.buildPipe({ 50 - pipe: "monitor_list__v1", 52 + pipe: "status_timezone__v0", 51 53 parameters: tbParameterMonitorList, 52 54 data: tbBuildMonitorList, 53 55 opts: { ··· 65 67 parameters: tbParameterHomeStats, 66 68 data: tbBuildHomeStats, 67 69 opts: { 68 - revalidate: 86400, // 60 * 60 * 24 = 86400s - 1d 70 + revalidate: 86400, // 60 * 60 * 24 = 86400s = 1d 69 71 }, 70 72 }); 71 73 }
+6 -5
packages/tinybird/src/validation.ts
··· 68 68 }); 69 69 70 70 /** 71 - * Params for pipe monitor_list__v1 71 + * Params for pipe status_timezone__v0 72 72 */ 73 73 export const tbParameterMonitorList = z.object({ 74 74 monitorId: z.string(), 75 - limit: z.number().int().default(60).optional(), // 40 days 75 + timezone: z.string().optional(), 76 + limit: z.number().int().default(46).optional(), // 46 days 76 77 }); 77 78 78 79 /** 79 - * Values from the pipe monitor_list__v1 80 + * Values from the pipe status_timezone__v0 80 81 */ 81 82 export const tbBuildMonitorList = z.object({ 82 83 count: z.number().int(), 83 84 ok: z.number().int(), 84 - avgLatency: z.number().int(), // in ms 85 - cronTimestamp: z.number().int(), 85 + avgLatency: z.number().int(), 86 + day: z.coerce.date(), 86 87 }); 87 88 88 89 /**