Openstatus www.openstatus.dev

๐ŸŒ More regions (#881)

* ๐Ÿšง wip

* ๐Ÿ”ฅ more regions

* ๐Ÿงช fix

* ๐Ÿงช fix

* ๐Ÿงช fix

* ๐Ÿงช fix

* ๐Ÿ”ฅ fix

* ๐Ÿ“ changelog

* ๐Ÿ–ผ๏ธ update screenshot

* ๐Ÿงน clean

authored by

Thibault Le Ouay and committed by
GitHub
59d180d2 d61bb8f8

+775 -312
+6
apps/checker/README.md
··· 28 28 ```bash 29 29 fly deploy 30 30 ``` 31 + 32 + ## Deploy to all region 33 + 34 + ```bash 35 + fly scale count 35 --region ams,arn,atl,bog,bom,bos,cdg,den,dfw,ewr,eze,fra,gdl,gig,gru,hkg,iad,jnb,lax,lhr,mad,mia,nrt,ord,otp,phx,qro,scl,sjc,sea,sin,syd,waw,yul,yyz 36 + ```
+1 -1
apps/server/src/env.ts
··· 1 1 import { createEnv } from "@t3-oss/env-core"; 2 2 import { z } from "zod"; 3 3 4 - import { flyRegions } from "@openstatus/utils"; 4 + import { flyRegions } from "@openstatus/db/src/schema"; 5 5 6 6 export const env = createEnv({ 7 7 server: {
+6
apps/server/src/v1/monitors/post.ts
··· 70 70 throw new HTTPException(403, { message: "Forbidden" }); 71 71 } 72 72 73 + for (const region of input.regions) { 74 + if (!workspacePlan.limits.regions.includes(region)) { 75 + throw new HTTPException(403, { message: "Upgrade for more region" }); 76 + } 77 + } 78 + 73 79 const { headers, regions, assertions, ...rest } = input; 74 80 75 81 const assert = assertions ? getAssertions(assertions) : [];
+5
apps/server/src/v1/monitors/put.ts
··· 50 50 throw new HTTPException(403, { message: "Forbidden" }); 51 51 } 52 52 53 + for (const region of input.regions) { 54 + if (!workspacePlan.limits.regions.includes(region)) { 55 + throw new HTTPException(403, { message: "Upgrade for more region" }); 56 + } 57 + } 53 58 const _monitor = await db 54 59 .select() 55 60 .from(monitor)
apps/web/public/assets/changelog/more-regions.png

This is a binary file and will not be displayed.

+7 -7
apps/web/src/app/api/checker/cron/_cron.ts
··· 45 45 const parent = client.queuePath( 46 46 env.GCP_PROJECT_ID, 47 47 env.GCP_LOCATION, 48 - periodicity, 48 + periodicity 49 49 ); 50 50 51 51 const timestamp = Date.now(); ··· 54 54 .select({ id: maintenance.id }) 55 55 .from(maintenance) 56 56 .where( 57 - and(lte(maintenance.from, new Date()), gte(maintenance.to, new Date())), 57 + and(lte(maintenance.from, new Date()), gte(maintenance.to, new Date())) 58 58 ) 59 59 .as("currentMaintenance"); 60 60 ··· 63 63 .from(maintenancesToMonitors) 64 64 .innerJoin( 65 65 currentMaintenance, 66 - eq(maintenancesToMonitors.maintenanceId, currentMaintenance.id), 66 + eq(maintenancesToMonitors.maintenanceId, currentMaintenance.id) 67 67 ); 68 68 69 69 const result = await db ··· 73 73 and( 74 74 eq(monitor.periodicity, periodicity), 75 75 eq(monitor.active, true), 76 - notInArray(monitor.id, currentMaintenanceMonitors), 77 - ), 76 + notInArray(monitor.id, currentMaintenanceMonitors) 77 + ) 78 78 ) 79 79 .all(); 80 80 ··· 84 84 const allResult = []; 85 85 86 86 for (const row of monitors) { 87 - const selectedRegions = row.regions.length > 0 ? row.regions : ["auto"]; 87 + const selectedRegions = row.regions.length > 0 ? row.regions : ["ams"]; 88 88 89 89 const result = await db 90 90 .select() ··· 127 127 const failed = allRequests.filter((r) => r.status === "rejected").length; 128 128 129 129 console.log( 130 - `End cron for ${periodicity} with ${allResult.length} jobs with ${success} success and ${failed} failed`, 130 + `End cron for ${periodicity} with ${allResult.length} jobs with ${success} success and ${failed} failed` 131 131 ); 132 132 }; 133 133 // timestamp needs to be in ms
+4 -2
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/data/_components/data-table-wrapper.tsx
··· 19 19 import { LoadingAnimation } from "@/components/loading-animation"; 20 20 import { ResponseDetailTabs } from "@/components/ping-response-analysis/response-detail-tabs"; 21 21 import { api } from "@/trpc/client"; 22 + import type { z } from "zod"; 23 + import type { monitorFlyRegionSchema } from "@openstatus/db/src/schema"; 22 24 23 25 // EXAMPLE: get the type of the response of the endpoint 24 26 // biome-ignore lint/correctness/noUnusedVariables: <explanation> ··· 29 31 monitorId: string; 30 32 url: string; 31 33 latency: number; 32 - region: "ams" | "iad" | "hkg" | "jnb" | "syd" | "gru"; 34 + region: z.infer<typeof monitorFlyRegionSchema>; 33 35 statusCode: number | null; 34 36 timestamp: number; 35 37 workspaceId: string; ··· 80 82 url: row.original.url, 81 83 region: row.original.region, 82 84 cronTimestamp: row.original.cronTimestamp || undefined, 83 - }), 85 + }) 84 86 ); 85 87 86 88 if (!data || data.length === 0) return <p>Something went wrong</p>;
+2 -3
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/details/page.tsx
··· 2 2 import * as z from "zod"; 3 3 4 4 import { Button } from "@openstatus/ui"; 5 - import { flyRegions } from "@openstatus/utils"; 6 5 7 6 import { EmptyState } from "@/components/dashboard/empty-state"; 8 7 import { ResponseDetails } from "@/components/monitor-dashboard/response-details"; 9 8 import { api } from "@/trpc/server"; 10 - 9 + import { monitorFlyRegionSchema } from "@openstatus/db/src/schema"; 11 10 // 12 11 13 12 /** ··· 16 15 const searchParamsSchema = z.object({ 17 16 monitorId: z.string(), 18 17 url: z.string(), 19 - region: z.enum(flyRegions).optional(), 18 + region: monitorFlyRegionSchema.optional(), 20 19 cronTimestamp: z.coerce.number(), 21 20 }); 22 21
+2 -2
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/overview/page.tsx
··· 44 44 value 45 45 ?.trim() 46 46 ?.split(",") 47 - .filter((i) => flyRegions.includes(i as Region)) ?? flyRegions, 47 + .filter((i) => flyRegions.includes(i as Region)) ?? [] 48 48 ), 49 49 }); 50 50 ··· 117 117 period={period} 118 118 quantile={quantile} 119 119 interval={interval} 120 - regions={regions as Region[]} // FIXME: not properly reseted after filtered 120 + regions={regions.length ? (regions as Region[]) : monitor.regions} // FIXME: not properly reseted after filtered 121 121 monitor={monitor} 122 122 isQuantileDisabled={isQuantileDisabled} 123 123 metricsByRegion={metricsByRegion}
+2 -2
apps/web/src/components/data-table/columns.tsx
··· 11 11 TooltipProvider, 12 12 TooltipTrigger, 13 13 } from "@openstatus/ui"; 14 - import { regionsDict } from "@openstatus/utils"; 14 + import { flyRegionsDict } from "@openstatus/utils"; 15 15 16 16 import { DataTableColumnHeader } from "./data-table-column-header"; 17 17 import { DataTableStatusBadge } from "./data-table-status-badge"; ··· 98 98 <div> 99 99 <span className="font-mono">{String(row.getValue("region"))} </span> 100 100 <span className="text-muted-foreground text-xs"> 101 - {regionsDict[row.original.region]?.location} 101 + {flyRegionsDict[row.original.region]?.location} 102 102 </span> 103 103 </div> 104 104 );
+10 -10
apps/web/src/components/forms/monitor-form.tsx
··· 17 17 monitorMethods, 18 18 monitorMethodsSchema, 19 19 monitorPeriodicitySchema, 20 + workspacePlans, 20 21 } from "@openstatus/db/src/schema"; 21 22 import { getLimit } from "@openstatus/plans"; 22 23 import { ··· 103 104 periodicity: defaultValues?.periodicity || "30m", 104 105 active: defaultValues?.active ?? true, 105 106 id: defaultValues?.id || 0, 106 - regions: 107 - defaultValues?.regions || (flyRegions as Writeable<typeof flyRegions>), 107 + regions: defaultValues?.regions || getLimit("free", "regions"), 108 108 headers: defaultValues?.headers?.length 109 109 ? defaultValues?.headers 110 110 : [{ key: "", value: "" }], ··· 441 441 <Select 442 442 onValueChange={(value) => 443 443 field.onChange( 444 - monitorPeriodicitySchema.parse(value), 444 + monitorPeriodicitySchema.parse(value) 445 445 ) 446 446 } 447 447 defaultValue={field.value} ··· 494 494 role="combobox" 495 495 className={cn( 496 496 "h-10 w-full justify-between", 497 - !field.value && "text-muted-foreground", 497 + !field.value && "text-muted-foreground" 498 498 )} 499 499 > 500 500 {renderText()} ··· 526 526 "regions", 527 527 currentRegions.includes(code) 528 528 ? currentRegions.filter( 529 - (r) => r !== code, 529 + (r) => r !== code 530 530 ) 531 - : [...currentRegions, code], 531 + : [...currentRegions, code] 532 532 ); 533 533 }} 534 534 > ··· 537 537 "mr-2 h-4 w-4", 538 538 isSelected 539 539 ? "opacity-100" 540 - : "opacity-0", 540 + : "opacity-0" 541 541 )} 542 542 /> 543 543 {location} 544 544 </CommandItem> 545 545 ); 546 - }, 546 + } 547 547 )} 548 548 </CommandGroup> 549 549 </Command> ··· 650 650 ]) 651 651 : field.onChange( 652 652 field.value?.filter( 653 - (value) => value !== item.id, 654 - ), 653 + (value) => value !== item.id 654 + ) 655 655 ); 656 656 }} 657 657 />
+6 -6
apps/web/src/components/forms/monitor/form.tsx
··· 4 4 import { usePathname, useRouter } from "next/navigation"; 5 5 import * as React from "react"; 6 6 import { useForm } from "react-hook-form"; 7 + import { getLimit } from "@openstatus/plans"; 7 8 8 9 import * as assertions from "@openstatus/assertions"; 9 10 import type { ··· 14 15 Page, 15 16 WorkspacePlan, 16 17 } from "@openstatus/db/src/schema"; 17 - import { flyRegions, insertMonitorSchema } from "@openstatus/db/src/schema"; 18 + import { insertMonitorSchema } from "@openstatus/db/src/schema"; 18 19 import { Badge, Form } from "@openstatus/ui"; 19 20 20 21 import { ··· 72 73 periodicity: defaultValues?.periodicity || "30m", 73 74 active: defaultValues?.active ?? true, 74 75 id: defaultValues?.id || 0, 75 - regions: 76 - defaultValues?.regions || (flyRegions as Writeable<typeof flyRegions>), 76 + regions: defaultValues?.regions || getLimit("free", "regions"), 77 77 headers: defaultValues?.headers?.length 78 78 ? defaultValues?.headers 79 79 : [{ key: "", value: "" }], ··· 138 138 finally: () => { 139 139 setPending(false); 140 140 }, 141 - }, 141 + } 142 142 ); 143 143 }; 144 144 ··· 187 187 JSON.stringify([ 188 188 ...(statusAssertions || []), 189 189 ...(headerAssertions || []), 190 - ]), 190 + ]) 191 191 ); 192 192 193 193 const data = (await res.json()) as RegionChecker; ··· 223 223 if (error instanceof Error && error.name === "AbortError") { 224 224 return { 225 225 error: `Abort error: request takes more then ${formatDuration( 226 - ABORT_TIMEOUT, 226 + ABORT_TIMEOUT 227 227 )}.`, 228 228 }; 229 229 }
+8 -4
apps/web/src/components/forms/monitor/request-test-button.tsx
··· 7 7 InsertMonitor, 8 8 MonitorFlyRegion, 9 9 } from "@openstatus/db/src/schema"; 10 + import { flyRegions } from "@openstatus/db/src/schema"; 10 11 import { 11 12 Button, 12 13 Dialog, ··· 23 24 TooltipProvider, 24 25 TooltipTrigger, 25 26 } from "@openstatus/ui"; 26 - import { flyRegions, flyRegionsDict } from "@openstatus/utils"; 27 + import { flyRegionsDict } from "@openstatus/utils"; 27 28 28 29 import { LoadingAnimation } from "@/components/loading-animation"; 29 30 import { RegionInfo } from "@/components/ping-response-analysis/region-info"; 30 31 import { ResponseDetailTabs } from "@/components/ping-response-analysis/response-detail-tabs"; 31 32 import type { RegionChecker } from "@/components/ping-response-analysis/utils"; 32 33 import { toast, toastAction } from "@/lib/toast"; 34 + import { getLimit } from "@openstatus/plans"; 33 35 34 36 interface Props { 35 37 form: UseFormReturn<InsertMonitor>; 36 38 pingEndpoint( 37 - region?: MonitorFlyRegion, 39 + region?: MonitorFlyRegion 38 40 ): Promise<{ data?: RegionChecker; error?: string }>; 39 41 } 40 42 ··· 75 77 76 78 const { statusAssertions, headerAssertions } = form.getValues(); 77 79 80 + const regions = getLimit("free", "regions"); 81 + 78 82 return ( 79 83 <Dialog open={!!check} onOpenChange={() => setCheck(undefined)}> 80 84 <div className="group flex h-10 items-center rounded-md bg-transparent text-sm ring-offset-background focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2"> ··· 89 93 <SelectValue>{flag}</SelectValue> 90 94 </SelectTrigger> 91 95 <SelectContent> 92 - {flyRegions.map((region) => { 96 + {regions.map((region) => { 93 97 const { flag } = flyRegionsDict[region]; 94 98 return ( 95 99 <SelectItem key={region} value={region}> ··· 138 142 JSON.stringify([ 139 143 ...(statusAssertions || []), 140 144 ...(headerAssertions || []), 141 - ]), 145 + ]) 142 146 )} 143 147 /> 144 148 </div>
+81 -38
apps/web/src/components/forms/monitor/section-scheduling.tsx
··· 3 3 import type { UseFormReturn } from "react-hook-form"; 4 4 5 5 import type { InsertMonitor, WorkspacePlan } from "@openstatus/db/src/schema"; 6 - import { monitorPeriodicitySchema } from "@openstatus/db/src/schema"; 6 + import { 7 + flyRegions, 8 + monitorPeriodicitySchema, 9 + } from "@openstatus/db/src/schema"; 7 10 import { getLimit } from "@openstatus/plans"; 8 11 import { 9 12 FormControl, ··· 18 21 SelectTrigger, 19 22 SelectValue, 20 23 } from "@openstatus/ui"; 21 - import { flyRegions, flyRegionsDict } from "@openstatus/utils"; 24 + import { groupByContinent } from "@openstatus/utils"; 22 25 23 26 import { CheckboxLabel } from "../shared/checkbox-label"; 24 27 import { SectionHeader } from "../shared/section-header"; ··· 40 43 41 44 export function SectionScheduling({ form, plan }: Props) { 42 45 const periodicityLimit = getLimit(plan, "periodicity"); 46 + const regionsLimit = getLimit(plan, "regions"); 47 + console.log(form.getValues()); 43 48 return ( 44 49 <div className="grid w-full gap-4"> 45 50 <SectionHeader ··· 94 99 <div className="mb-4"> 95 100 <FormLabel className="text-base">Regions</FormLabel> 96 101 <FormDescription> 97 - Select the regions you want to monitor your endpoint from. 102 + Select the regions you want to monitor your endpoint from.{" "} 103 + <br /> 104 + {plan === "free" 105 + ? "Only a few regions are available in the free plan. Upgrade to access all regions." 106 + : ""} 98 107 </FormDescription> 99 108 </div> 100 - <div className="grid grid-cols-1 grid-rows-1 gap-4 md:grid-cols-3 sm:grid-cols-2"> 101 - {flyRegions.map((item) => ( 102 - <FormField 103 - key={item} 104 - control={form.control} 105 - name="regions" 106 - render={({ field }) => { 107 - const { flag, location } = flyRegionsDict[item]; 108 - return ( 109 - <FormItem key={item} className="h-full w-full"> 110 - <FormControl className="h-full"> 111 - <CheckboxLabel 112 - id={item} 113 - name="region" 114 - checked={field.value?.includes(item)} 115 - onCheckedChange={(checked) => { 116 - return checked 117 - ? field.onChange([ 118 - ...(field.value ? field.value : []), 119 - item, 120 - ]) 121 - : field.onChange( 122 - field.value?.filter( 123 - (value) => value !== item, 124 - ), 109 + <div> 110 + {Object.entries(groupByContinent) 111 + .sort((a, b) => a[0].localeCompare(b[0])) 112 + .map(([continent, regions]) => { 113 + return { continent, regions }; 114 + }) 115 + .map((current) => { 116 + return ( 117 + <div key={current.continent} className="py-2"> 118 + {current.continent} 119 + 120 + <div className="grid grid-cols-3 grid-rows-1 gap-2 pt-1"> 121 + {current.regions 122 + .sort((a, b) => 123 + a.location.localeCompare(b.location) 124 + ) 125 + .map((item) => { 126 + return ( 127 + <FormField 128 + key={item.code} 129 + control={form.control} 130 + name="regions" 131 + render={({ field }) => { 132 + const { flag, location } = item; 133 + return ( 134 + <FormItem 135 + key={item.code} 136 + className="h-full w-full" 137 + > 138 + <FormControl className="h-full"> 139 + <CheckboxLabel 140 + disabled={ 141 + !regionsLimit.includes(item.code) 142 + } 143 + id={item.code} 144 + name="region" 145 + checked={field.value?.includes( 146 + item.code 147 + )} 148 + onCheckedChange={(checked) => { 149 + console.log(field.value); 150 + return checked 151 + ? field.onChange([ 152 + ...(field.value 153 + ? field.value 154 + : []), 155 + item.code, 156 + ]) 157 + : field.onChange( 158 + field.value?.filter( 159 + (value) => 160 + value !== item.code 161 + ) 162 + ); 163 + }} 164 + > 165 + {location} {flag} 166 + </CheckboxLabel> 167 + </FormControl> 168 + </FormItem> 125 169 ); 126 - }} 127 - > 128 - {location} {flag} 129 - </CheckboxLabel> 130 - </FormControl> 131 - </FormItem> 132 - ); 133 - }} 134 - /> 135 - ))} 170 + }} 171 + /> 172 + ); 173 + })} 174 + </div> 175 + </div> 176 + ); 177 + })} 136 178 </div> 179 + 137 180 <FormMessage /> 138 181 </FormItem> 139 182 );
+4 -1
apps/web/src/components/forms/shared/checkbox-label.tsx
··· 11 11 onCheckedChange(checked: boolean): void; 12 12 className?: string; 13 13 name: string; 14 + disabled?: boolean; 14 15 } 15 16 16 17 export function CheckboxLabel({ ··· 20 21 onCheckedChange, 21 22 className, 22 23 name, 24 + disabled, 23 25 }: Props) { 24 26 return ( 25 27 <div className="relative h-full"> ··· 29 31 className="peer sr-only" 30 32 checked={checked} 31 33 onCheckedChange={onCheckedChange} 34 + disabled={disabled} 32 35 /> 33 36 <Label 34 37 htmlFor={`${name}-${id}`} 35 38 className={cn( 36 39 "flex h-full items-center gap-1 rounded-md border border-border bg-popover p-4 pr-10 [&:has([data-state=checked])]:border-primary peer-data-[state=checked]:border-primary hover:bg-accent hover:text-accent-foreground", 37 - className, 40 + className 38 41 )} 39 42 > 40 43 {children}
+24 -27
apps/web/src/components/monitor-charts/utils.tsx
··· 1 1 import { format } from "date-fns"; 2 2 3 3 import type { Region, ResponseGraph } from "@openstatus/tinybird"; 4 - import { regionsDict } from "@openstatus/utils"; 4 + import { flyRegionsDict } from "@openstatus/utils"; 5 5 6 6 import type { Period, Quantile } from "@/lib/monitor/utils"; 7 7 ··· 14 14 export function groupDataByTimestamp( 15 15 data: ResponseGraph[], 16 16 period: Period, 17 - quantile: Quantile, 17 + quantile: Quantile 18 18 ) { 19 19 let currentTimestamp = 0; 20 20 const regions: Record< 21 21 string, 22 22 { code: string; location: string; flag: string } 23 23 > = {}; 24 - const _data = data.reduce( 25 - (acc, curr) => { 26 - const { timestamp, region } = curr; 27 - const latency = curr[`${quantile}Latency`]; 28 - const { flag, code, location } = regionsDict[region]; 29 - const fullNameRegion = `${code}`; 30 - regions[fullNameRegion] = { flag, code, location }; // to get the region keys 31 - if (timestamp === currentTimestamp) { 32 - // overwrite last object in acc 33 - const last = acc.pop(); 34 - if (last) { 35 - acc.push({ 36 - ...last, 37 - [fullNameRegion]: latency, 38 - }); 39 - } 40 - } else if (timestamp) { 41 - currentTimestamp = timestamp; 42 - // create new object in acc 24 + const _data = data.reduce((acc, curr) => { 25 + const { timestamp, region } = curr; 26 + const latency = curr[`${quantile}Latency`]; 27 + const { flag, code, location } = flyRegionsDict[region]; 28 + const fullNameRegion = `${code}`; 29 + regions[fullNameRegion] = { flag, code, location }; // to get the region keys 30 + if (timestamp === currentTimestamp) { 31 + // overwrite last object in acc 32 + const last = acc.pop(); 33 + if (last) { 43 34 acc.push({ 44 - timestamp: renderTimestamp(timestamp, period), 35 + ...last, 45 36 [fullNameRegion]: latency, 46 37 }); 47 38 } 48 - return acc; 49 - }, 50 - [] as (Partial<Record<Region, string>> & { timestamp: string })[], 51 - ); 39 + } else if (timestamp) { 40 + currentTimestamp = timestamp; 41 + // create new object in acc 42 + acc.push({ 43 + timestamp: renderTimestamp(timestamp, period), 44 + [fullNameRegion]: latency, 45 + }); 46 + } 47 + return acc; 48 + }, [] as (Partial<Record<Region, string>> & { timestamp: string })[]); 52 49 53 50 // regions are sorted by the flag utf-8 code 54 51 return { ··· 74 71 } 75 72 76 73 export function regionFormatter(region: Region) { 77 - const { code, flag } = regionsDict[region]; 74 + const { code, flag } = flyRegionsDict[region]; 78 75 return `${flag} ${code}`; 79 76 }
+11
apps/web/src/content/changelog/more-regions.mdx
··· 1 + --- 2 + title: More regions available ๐ŸŒ 3 + description: You can now monitor your endpoint from 35 regions. 4 + image: /assets/changelog/more-regions.png 5 + publishedAt: 2024-06-19 6 + --- 7 + We have added more regions to OpenStatus. 8 + 9 + You can now monitor your endpoint from 35 regions. 10 + 11 + Let's make synthetic monitoring more global! ๐ŸŒ
+78 -47
packages/api/src/router/monitor.ts
··· 38 38 const monitorLimit = allPlans[opts.ctx.workspace.plan].limits.monitors; 39 39 const periodicityLimit = 40 40 allPlans[opts.ctx.workspace.plan].limits.periodicity; 41 + const regionsLimit = allPlans[opts.ctx.workspace.plan].limits.regions; 41 42 42 43 const monitorNumbers = ( 43 44 await opts.ctx.db.query.monitor.findMany({ 44 45 where: and( 45 46 eq(monitor.workspaceId, opts.ctx.workspace.id), 46 - isNull(monitor.deletedAt), 47 + isNull(monitor.deletedAt) 47 48 ), 48 49 }) 49 50 ).length; ··· 67 68 }); 68 69 } 69 70 71 + if ( 72 + opts.input.regions !== undefined && 73 + opts.input.regions?.length !== 0 74 + ) { 75 + for (const region of opts.input.regions) { 76 + if (!regionsLimit.includes(region)) { 77 + throw new TRPCError({ 78 + code: "FORBIDDEN", 79 + message: "You don't have access to this region.", 80 + }); 81 + } 82 + } 83 + } 84 + 70 85 // FIXME: this is a hotfix 71 86 const { 72 87 regions, ··· 106 121 const allNotifications = await opts.ctx.db.query.notification.findMany({ 107 122 where: and( 108 123 eq(notification.workspaceId, opts.ctx.workspace.id), 109 - inArray(notification.id, notifications), 124 + inArray(notification.id, notifications) 110 125 ), 111 126 }); 112 127 ··· 122 137 const allTags = await opts.ctx.db.query.monitorTag.findMany({ 123 138 where: and( 124 139 eq(monitorTag.workspaceId, opts.ctx.workspace.id), 125 - inArray(monitorTag.id, tags), 140 + inArray(monitorTag.id, tags) 126 141 ), 127 142 }); 128 143 ··· 138 153 const allPages = await opts.ctx.db.query.page.findMany({ 139 154 where: and( 140 155 eq(page.workspaceId, opts.ctx.workspace.id), 141 - inArray(page.id, pages), 156 + inArray(page.id, pages) 142 157 ), 143 158 }); 144 159 ··· 165 180 where: and( 166 181 eq(monitor.id, opts.input.id), 167 182 eq(monitor.workspaceId, opts.ctx.workspace.id), 168 - isNull(monitor.deletedAt), 183 + isNull(monitor.deletedAt) 169 184 ), 170 185 with: { 171 186 monitorTagsToMonitors: { with: { monitorTag: true } }, ··· 190 205 maintenance: _monitor?.maintenancesToMonitors.some( 191 206 (item) => 192 207 item.maintenance.from.getTime() <= Date.now() && 193 - item.maintenance.to.getTime() >= Date.now(), 208 + item.maintenance.to.getTime() >= Date.now() 194 209 ), 195 210 }); 196 211 ··· 212 227 where: and( 213 228 eq(monitor.id, opts.input.id), 214 229 isNull(monitor.deletedAt), 215 - eq(monitor.public, true), 230 + eq(monitor.public, true) 216 231 ), 217 232 }); 218 233 if (!_monitor) return undefined; ··· 224 239 }); 225 240 226 241 const hasPageRelation = _page?.monitorsToPages.find( 227 - ({ monitorId }) => _monitor.id === monitorId, 242 + ({ monitorId }) => _monitor.id === monitorId 228 243 ); 229 244 230 245 if (!hasPageRelation) return undefined; ··· 241 256 const periodicityLimit = 242 257 allPlans[opts.ctx.workspace.plan].limits.periodicity; 243 258 259 + const regionsLimit = allPlans[opts.ctx.workspace.plan].limits.regions; 260 + 244 261 // the user is not allowed to use the cron job 245 262 if ( 246 263 opts.input?.periodicity && ··· 250 267 code: "FORBIDDEN", 251 268 message: "You reached your cron job limits.", 252 269 }); 270 + } 271 + 272 + if ( 273 + opts.input.regions !== undefined && 274 + opts.input.regions?.length !== 0 275 + ) { 276 + for (const region of opts.input.regions) { 277 + if (!regionsLimit.includes(region)) { 278 + throw new TRPCError({ 279 + code: "FORBIDDEN", 280 + message: "You don't have access to this region.", 281 + }); 282 + } 283 + } 253 284 } 254 285 255 286 const { ··· 284 315 and( 285 316 eq(monitor.id, opts.input.id), 286 317 eq(monitor.workspaceId, opts.ctx.workspace.id), 287 - isNull(monitor.deletedAt), 288 - ), 318 + isNull(monitor.deletedAt) 319 + ) 289 320 ) 290 321 .returning() 291 322 .get(); ··· 300 331 (x) => 301 332 !currentMonitorNotifications 302 333 .map(({ notificationId }) => notificationId) 303 - ?.includes(x), 334 + ?.includes(x) 304 335 ); 305 336 306 337 if (addedNotifications.length > 0) { ··· 324 355 eq(notificationsToMonitors.monitorId, currentMonitor.id), 325 356 inArray( 326 357 notificationsToMonitors.notificationId, 327 - removedNotifications, 328 - ), 329 - ), 358 + removedNotifications 359 + ) 360 + ) 330 361 ) 331 362 .run(); 332 363 } ··· 341 372 (x) => 342 373 !currentMonitorTags 343 374 .map(({ monitorTagId }) => monitorTagId) 344 - ?.includes(x), 375 + ?.includes(x) 345 376 ); 346 377 347 378 if (addedTags.length > 0) { ··· 363 394 .where( 364 395 and( 365 396 eq(monitorTagsToMonitors.monitorId, currentMonitor.id), 366 - inArray(monitorTagsToMonitors.monitorTagId, removedTags), 367 - ), 397 + inArray(monitorTagsToMonitors.monitorTagId, removedTags) 398 + ) 368 399 ) 369 400 .run(); 370 401 } ··· 376 407 .all(); 377 408 378 409 const addedPages = pages.filter( 379 - (x) => !currentMonitorPages.map(({ pageId }) => pageId)?.includes(x), 410 + (x) => !currentMonitorPages.map(({ pageId }) => pageId)?.includes(x) 380 411 ); 381 412 382 413 if (addedPages.length > 0) { ··· 398 429 .where( 399 430 and( 400 431 eq(monitorsToPages.monitorId, currentMonitor.id), 401 - inArray(monitorsToPages.pageId, removedPages), 402 - ), 432 + inArray(monitorsToPages.pageId, removedPages) 433 + ) 403 434 ) 404 435 .run(); 405 436 } ··· 410 441 insertMonitorSchema 411 442 .pick({ public: true, active: true }) 412 443 .partial() // batched updates 413 - .extend({ ids: z.number().array() }), // array of monitor ids to update 444 + .extend({ ids: z.number().array() }) // array of monitor ids to update 414 445 ) 415 446 .mutation(async (opts) => { 416 447 const _monitors = await opts.ctx.db ··· 420 451 and( 421 452 inArray(monitor.id, opts.input.ids), 422 453 eq(monitor.workspaceId, opts.ctx.workspace.id), 423 - isNull(monitor.deletedAt), 424 - ), 454 + isNull(monitor.deletedAt) 455 + ) 425 456 ); 426 457 }), 427 458 ··· 431 462 ids: z.number().array(), 432 463 tagId: z.number(), 433 464 action: z.enum(["add", "remove"]), 434 - }), 465 + }) 435 466 ) 436 467 .mutation(async (opts) => { 437 468 const _monitorTag = await opts.ctx.db.query.monitorTag.findFirst({ 438 469 where: and( 439 470 eq(monitorTag.workspaceId, opts.ctx.workspace.id), 440 - eq(monitorTag.id, opts.input.tagId), 471 + eq(monitorTag.id, opts.input.tagId) 441 472 ), 442 473 }); 443 474 444 475 const _monitors = await opts.ctx.db.query.monitor.findMany({ 445 476 where: and( 446 477 eq(monitor.workspaceId, opts.ctx.workspace.id), 447 - inArray(monitor.id, opts.input.ids), 478 + inArray(monitor.id, opts.input.ids) 448 479 ), 449 480 }); 450 481 ··· 462 493 opts.input.ids.map((id) => ({ 463 494 monitorId: id, 464 495 monitorTagId: opts.input.tagId, 465 - })), 496 + })) 466 497 ) 467 498 .onConflictDoNothing() 468 499 .run(); ··· 474 505 .where( 475 506 and( 476 507 inArray(monitorTagsToMonitors.monitorId, opts.input.ids), 477 - eq(monitorTagsToMonitors.monitorTagId, opts.input.tagId), 478 - ), 508 + eq(monitorTagsToMonitors.monitorTagId, opts.input.tagId) 509 + ) 479 510 ) 480 511 .run(); 481 512 } ··· 490 521 .where( 491 522 and( 492 523 eq(monitor.id, opts.input.id), 493 - eq(monitor.workspaceId, opts.ctx.workspace.id), 494 - ), 524 + eq(monitor.workspaceId, opts.ctx.workspace.id) 525 + ) 495 526 ) 496 527 .get(); 497 528 if (!monitorToDelete) return; ··· 530 561 .where( 531 562 and( 532 563 inArray(monitor.id, opts.input.ids), 533 - eq(monitor.workspaceId, opts.ctx.workspace.id), 534 - ), 564 + eq(monitor.workspaceId, opts.ctx.workspace.id) 565 + ) 535 566 ) 536 567 .all(); 537 568 ··· 571 602 const monitors = await opts.ctx.db.query.monitor.findMany({ 572 603 where: and( 573 604 eq(monitor.workspaceId, opts.ctx.workspace.id), 574 - isNull(monitor.deletedAt), 605 + isNull(monitor.deletedAt) 575 606 ), 576 607 with: { 577 608 monitorTagsToMonitors: { with: { monitorTag: true } }, ··· 584 615 monitorTagsToMonitors: z 585 616 .array(z.object({ monitorTag: selectMonitorTagSchema })) 586 617 .default([]), 587 - }), 618 + }) 588 619 ) 589 620 .parse(monitors); 590 621 }), ··· 595 626 const _page = await opts.ctx.db.query.page.findFirst({ 596 627 where: and( 597 628 eq(page.id, opts.input.id), 598 - eq(page.workspaceId, opts.ctx.workspace.id), 629 + eq(page.workspaceId, opts.ctx.workspace.id) 599 630 ), 600 631 }); 601 632 ··· 604 635 const monitors = await opts.ctx.db.query.monitor.findMany({ 605 636 where: and( 606 637 eq(monitor.workspaceId, opts.ctx.workspace.id), 607 - isNull(monitor.deletedAt), 638 + isNull(monitor.deletedAt) 608 639 ), 609 640 with: { 610 641 monitorTagsToMonitors: { with: { monitorTag: true } }, ··· 620 651 monitorTagsToMonitors: z 621 652 .array(z.object({ monitorTag: selectMonitorTagSchema })) 622 653 .default([]), 623 - }), 654 + }) 624 655 ) 625 656 .parse( 626 657 monitors.filter((monitor) => 627 658 monitor.monitorsToPages 628 659 .map(({ pageId }) => pageId) 629 - .includes(_page.id), 630 - ), 660 + .includes(_page.id) 661 + ) 631 662 ); 632 663 }), 633 664 ··· 641 672 and( 642 673 eq(monitor.id, opts.input.id), 643 674 eq(monitor.workspaceId, opts.ctx.workspace.id), 644 - isNull(monitor.deletedAt), 645 - ), 675 + isNull(monitor.deletedAt) 676 + ) 646 677 ) 647 678 .get(); 648 679 ··· 661 692 .where( 662 693 and( 663 694 eq(monitor.id, opts.input.id), 664 - eq(monitor.workspaceId, opts.ctx.workspace.id), 665 - ), 695 + eq(monitor.workspaceId, opts.ctx.workspace.id) 696 + ) 666 697 ) 667 698 .run(); 668 699 }), ··· 690 721 notification, 691 722 and( 692 723 eq(notificationsToMonitors.notificationId, notification.id), 693 - eq(notification.workspaceId, opts.ctx.workspace.id), 694 - ), 724 + eq(notification.workspaceId, opts.ctx.workspace.id) 725 + ) 695 726 ) 696 727 .where(eq(notificationsToMonitors.monitorId, opts.input.id)) 697 728 .all(); ··· 704 735 await opts.ctx.db.query.monitor.findMany({ 705 736 where: and( 706 737 eq(monitor.workspaceId, opts.ctx.workspace.id), 707 - isNull(monitor.deletedAt), 738 + isNull(monitor.deletedAt) 708 739 ), 709 740 }) 710 741 ).length;
+4 -4
packages/db/src/schema/monitor_status/validation.ts
··· 8 8 monitorStatusTable, 9 9 { 10 10 status: monitorStatusSchema.default("active"), 11 - region: monitorRegionSchema.default("auto"), 12 - }, 11 + region: monitorRegionSchema.default("ams"), 12 + } 13 13 ); 14 14 15 15 export const insertMonitorStatusSchema = createInsertSchema( 16 16 monitorStatusTable, 17 17 { 18 18 status: monitorStatusSchema.default("active"), 19 - region: monitorRegionSchema.default("auto"), 20 - }, 19 + region: monitorRegionSchema.default("ams"), 20 + } 21 21 ); 22 22 23 23 // export type InsertMonitorStatus = z.infer<typeof insertMonitorStatusSchema>;
+37 -29
packages/db/src/schema/monitors/constants.ts
··· 1 - /** 2 - * @deprecated 3 - */ 4 - export const vercelRegions = [ 5 - "arn1", 6 - "bom1", 7 - "cdg1", 8 - "cle1", 9 - "cpt1", 10 - "dub1", 11 - "fra1", 12 - "gru1", 13 - "hkg1", 14 - "hnd1", 15 - "iad1", 16 - "icn1", 17 - "kix1", 18 - "lhr1", 19 - "pdx1", 20 - "sfo1", 21 - "sin1", 22 - "syd1", 1 + export const flyRegions = [ 2 + "ams", 3 + "arn", 4 + "atl", 5 + "bog", 6 + "bom", 7 + "bos", 8 + "cdg", 9 + "den", 10 + "dfw", 11 + "ewr", 12 + "eze", 13 + "fra", 14 + "gdl", 15 + "gig", 16 + "gru", 17 + "hkg", 18 + "iad", 19 + "jnb", 20 + "lax", 21 + "lhr", 22 + "mad", 23 + "mia", 24 + "nrt", 25 + "ord", 26 + "otp", 27 + "phx", 28 + "qro", 29 + "scl", 30 + "sjc", 31 + "sea", 32 + "sin", 33 + "syd", 34 + "waw", 35 + "yul", 36 + "yyz", 23 37 ] as const; 24 - 25 - export const flyRegions = ["ams", "iad", "hkg", "jnb", "syd", "gru"] as const; 26 38 27 39 export const monitorPeriodicity = [ 28 40 "30s", ··· 35 47 ] as const; 36 48 export const monitorMethods = ["GET", "POST", "HEAD"] as const; 37 49 export const monitorStatus = ["active", "error"] as const; 38 - export const monitorRegions = [ 39 - ...flyRegions, 40 - ...vercelRegions, 41 - "auto", 42 - ] as const; 50 + export const monitorRegions = [...flyRegions] as const; 43 51 44 52 export const monitorJobTypes = ["website", "cron", "other"] as const;
+1 -1
packages/db/tsconfig.json
··· 18 18 "resolveJsonModule": true, 19 19 "noUncheckedIndexedAccess": true 20 20 }, 21 - "include": ["src", "*.ts", "env.mjs", "**/*.ts"] 21 + "include": ["src", "*.ts", "env.mjs", "**/*.ts", "../utils/try.ts"] 22 22 }
+107
packages/plans/src/config.ts
··· 20 20 monitors: 3, 21 21 periodicity: ["10m", "30m", "1h"], 22 22 "multi-region": true, 23 + "max-regions": 6, 23 24 "data-retention": "14 days", 24 25 "status-pages": 1, 25 26 maintenance: true, ··· 32 33 "notification-channels": 1, 33 34 members: 1, 34 35 "audit-log": false, 36 + regions: ["ams", "gru", "iad", "jnb", "hkg", "syd"], 35 37 }, 36 38 }, 37 39 starter: { ··· 42 44 monitors: 30, 43 45 periodicity: ["1m", "5m", "10m", "30m", "1h"], 44 46 "multi-region": true, 47 + "max-regions": 35, 45 48 "data-retention": "3 months", 46 49 "status-pages": 1, 47 50 maintenance: true, ··· 54 57 "notification-channels": 10, 55 58 members: "Unlimited", 56 59 "audit-log": false, 60 + regions: [ 61 + "ams", 62 + "arn", 63 + "atl", 64 + "bog", 65 + "bom", 66 + "bos", 67 + "cdg", 68 + "den", 69 + "dfw", 70 + "ewr", 71 + "eze", 72 + "fra", 73 + "gdl", 74 + "gig", 75 + "gru", 76 + "hkg", 77 + "iad", 78 + "jnb", 79 + "lax", 80 + "lhr", 81 + "mad", 82 + "mia", 83 + "nrt", 84 + "ord", 85 + "otp", 86 + "phx", 87 + "qro", 88 + "scl", 89 + "syd", 90 + "waw", 91 + "yul", 92 + "yyz", 93 + ], 57 94 }, 58 95 }, 59 96 team: { ··· 64 101 monitors: 100, 65 102 periodicity: ["30s", "1m", "5m", "10m", "30m", "1h"], 66 103 "multi-region": true, 104 + "max-regions": 35, 67 105 "data-retention": "12 months", 68 106 "status-pages": 5, 69 107 maintenance: true, ··· 76 114 "notification-channels": 20, 77 115 members: "Unlimited", 78 116 "audit-log": true, 117 + regions: [ 118 + "ams", 119 + "arn", 120 + "atl", 121 + "bog", 122 + "bom", 123 + "bos", 124 + "cdg", 125 + "den", 126 + "dfw", 127 + "ewr", 128 + "eze", 129 + "fra", 130 + "gdl", 131 + "gig", 132 + "gru", 133 + "hkg", 134 + "iad", 135 + "jnb", 136 + "lax", 137 + "lhr", 138 + "mad", 139 + "mia", 140 + "nrt", 141 + "ord", 142 + "otp", 143 + "phx", 144 + "qro", 145 + "scl", 146 + "syd", 147 + "waw", 148 + "yul", 149 + "yyz", 150 + ], 79 151 }, 80 152 }, 81 153 pro: { ··· 86 158 monitors: 500, 87 159 periodicity: ["30s", "1m", "5m", "10m", "30m", "1h"], 88 160 "multi-region": true, 161 + "max-regions": 35, 89 162 "data-retention": "24 months", 90 163 "status-pages": 20, 91 164 maintenance: true, ··· 98 171 "notification-channels": 50, 99 172 members: "Unlimited", 100 173 "audit-log": true, 174 + regions: [ 175 + "ams", 176 + "arn", 177 + "atl", 178 + "bog", 179 + "bom", 180 + "bos", 181 + "cdg", 182 + "den", 183 + "dfw", 184 + "ewr", 185 + "eze", 186 + "fra", 187 + "gdl", 188 + "gig", 189 + "gru", 190 + "hkg", 191 + "iad", 192 + "jnb", 193 + "lax", 194 + "lhr", 195 + "mad", 196 + "mia", 197 + "nrt", 198 + "ord", 199 + "otp", 200 + "phx", 201 + "qro", 202 + "scl", 203 + "syd", 204 + "waw", 205 + "yul", 206 + "yyz", 207 + ], 101 208 }, 102 209 }, 103 210 };
+1
packages/plans/src/pricing-table.ts
··· 22 22 value: "multi-region", 23 23 label: "Multi-region monitoring", 24 24 }, 25 + { value: "max-regions", label: "Number of Regions" }, 25 26 { value: "data-retention", label: "Data retention" }, 26 27 ], 27 28 },
+6 -1
packages/plans/src/types.ts
··· 1 - import type { MonitorPeriodicity } from "@openstatus/db/src/schema"; 1 + import type { 2 + MonitorFlyRegion, 3 + MonitorPeriodicity, 4 + } from "@openstatus/db/src/schema"; 2 5 3 6 export type Limits = { 4 7 // monitors 5 8 monitors: number; 6 9 periodicity: Partial<MonitorPeriodicity>[]; 7 10 "multi-region": boolean; 11 + "max-regions": number; 8 12 "data-retention": string; 9 13 // status pages 10 14 "status-pages": number; ··· 20 24 // collaboration 21 25 members: "Unlimited" | number; 22 26 "audit-log": boolean; 27 + regions: Partial<MonitorFlyRegion>[]; 23 28 };
+4 -4
packages/tinybird/src/os-client.ts
··· 1 1 import { NoopTinybird, Tinybird } from "@chronark/zod-bird"; 2 2 import { z } from "zod"; 3 3 4 - import { flyRegions } from "@openstatus/utils"; 4 + import { flyRegions } from "../../db/src/schema/monitors/constants"; 5 5 6 6 import type { tbIngestWebVitalsArray } from "./validation"; 7 7 import { ··· 118 118 opts?: { 119 119 cache?: RequestCache | undefined; 120 120 revalidate: number | undefined; 121 - }, // RETHINK: not the best way to handle it 121 + } // RETHINK: not the best way to handle it 122 122 ) => { 123 123 try { 124 124 const res = await this.tb.buildPipe({ ··· 178 178 179 179 endpointStatusPeriod( 180 180 period: "7d" | "45d", 181 - timezone: "UTC" = "UTC", // "EST" | "PST" | "CET" 181 + timezone: "UTC" = "UTC" // "EST" | "PST" | "CET" 182 182 ) { 183 183 const parameters = z.object({ monitorId: z.string() }); 184 184 ··· 187 187 opts?: { 188 188 cache?: RequestCache | undefined; 189 189 revalidate: number | undefined; 190 - }, // RETHINK: not the best way to handle it 190 + } // RETHINK: not the best way to handle it 191 191 ) => { 192 192 try { 193 193 const res = await this.tb.buildPipe({
+6 -6
packages/tinybird/src/validation.ts
··· 1 1 import * as z from "zod"; 2 - 3 - import { flyRegions } from "@openstatus/utils"; 2 + import { monitorFlyRegionSchema } from "../../db/src/schema/monitors/validation"; 3 + import type { flyRegions } from "../../db/src/schema/monitors/constants"; 4 4 5 5 export const tbIngestWebVitals = z.object({ 6 6 dsn: z.string(), ··· 92 92 latency: z.number().int(), // in ms 93 93 cronTimestamp: z.number().int().nullable().default(Date.now()), 94 94 url: z.string().url(), 95 - region: z.enum(flyRegions), 95 + region: monitorFlyRegionSchema, 96 96 message: z.string().nullable().optional(), 97 97 assertions: z.string().nullable().optional(), 98 98 }); ··· 106 106 fromDate: z.number().int().default(0), // always start from a date 107 107 toDate: z.number().int().optional(), 108 108 limit: z.number().int().optional().default(7500), // one day has 2448 pings (17 (regions) * 6 (per hour) * 24) * 3 days for historical data 109 - region: z.enum(flyRegions).optional(), 109 + region: monitorFlyRegionSchema.optional(), 110 110 cronTimestamp: z.number().int().optional(), 111 111 }); 112 112 ··· 174 174 */ 175 175 export const tbBuildResponseGraph = z 176 176 .object({ 177 - region: z.enum(flyRegions), 177 + region: monitorFlyRegionSchema, 178 178 timestamp: z.number().int(), 179 179 }) 180 180 .merge(latencyMetrics); ··· 279 279 */ 280 280 export const tbBuildResponseTimeMetricsByRegion = z 281 281 .object({ 282 - region: z.enum(flyRegions), 282 + region: monitorFlyRegionSchema, 283 283 }) 284 284 .merge(latencyMetrics); 285 285
+348 -117
packages/utils/index.ts
··· 2 2 * AWS data center informations from 18 regions, supported by vercel. 3 3 * https://vercel.com/docs/concepts/edge-network/regions#region-list 4 4 */ 5 - export const vercelRegionsDict = { 6 - /** 7 - * A random location will be chosen 8 - */ 9 - auto: { 10 - code: "auto", 11 - name: "random", 12 - location: "Random", 13 - flag: "๐ŸŒ", 5 + 6 + import type { MonitorFlyRegion } from "@openstatus/db/src/schema"; 7 + 8 + // export const vercelRegionsDict = { 9 + // /** 10 + // * A random location will be chosen 11 + // */ 12 + // auto: { 13 + // code: "auto", 14 + // name: "random", 15 + // location: "Random", 16 + // flag: "๐ŸŒ", 17 + // }, 18 + // arn1: { 19 + // code: "arn1", 20 + // name: "eu-north-1", 21 + // location: "Stockholm, Sweden", 22 + // flag: "๐Ÿ‡ธ๐Ÿ‡ช", 23 + // }, 24 + // bom1: { 25 + // code: "bom1", 26 + // name: "ap-south-1", 27 + // location: "Mumbai, India", 28 + // flag: "๐Ÿ‡ฎ๐Ÿ‡ณ", 29 + // }, 30 + // cdg1: { 31 + // code: "cdg1", 32 + // name: "eu-west-3", 33 + // location: "Paris, France", 34 + // flag: "๐Ÿ‡ซ๐Ÿ‡ท", 35 + // }, 36 + // cle1: { 37 + // code: "cle1", 38 + // name: "us-east-2", 39 + // location: "Cleveland, USA", 40 + // flag: "๐Ÿ‡บ๐Ÿ‡ธ", 41 + // }, 42 + // cpt1: { 43 + // code: "cpt1", 44 + // name: "af-south-1", 45 + // location: "Cape Town, South Africa", 46 + // flag: "๐Ÿ‡ฟ๐Ÿ‡ฆ", 47 + // }, 48 + // dub1: { 49 + // code: "dub1", 50 + // name: "eu-west-1", 51 + // location: "Dublin, Ireland", 52 + // flag: "๐Ÿ‡ฎ๐Ÿ‡ช", 53 + // }, 54 + // fra1: { 55 + // code: "fra1", 56 + // name: "eu-central-1", 57 + // location: "Frankfurt, Germany", 58 + // flag: "๐Ÿ‡ฉ๐Ÿ‡ช", 59 + // }, 60 + // gru1: { 61 + // code: "gru1", 62 + // name: "sa-east-1", 63 + // location: "Sรฃo Paulo, Brazil", 64 + // flag: "๐Ÿ‡ง๐Ÿ‡ท", 65 + // }, 66 + // hkg1: { 67 + // code: "hkg1", 68 + // name: "ap-east-1", 69 + // location: "Hong Kong", 70 + // flag: "๐Ÿ‡ญ๐Ÿ‡ฐ", 71 + // }, 72 + // hnd1: { 73 + // code: "hnd1", 74 + // name: "ap-northeast-1", 75 + // location: "Tokyo, Japan", 76 + // flag: "๐Ÿ‡ฏ๐Ÿ‡ต", 77 + // }, 78 + // iad1: { 79 + // code: "iad1", 80 + // name: "us-east-1", 81 + // location: "Washington, D.C., USA", 82 + // flag: "๐Ÿ‡บ๐Ÿ‡ธ", 83 + // }, 84 + // icn1: { 85 + // code: "icn1", 86 + // name: "ap-northeast-2", 87 + // location: "Seoul, South Korea", 88 + // flag: "๐Ÿ‡ฐ๐Ÿ‡ท", 89 + // }, 90 + // kix1: { 91 + // code: "kix1", 92 + // name: "ap-northeast-3", 93 + // location: "Osaka, Japan", 94 + // flag: "๐Ÿ‡ฏ๐Ÿ‡ต", 95 + // }, 96 + // lhr1: { 97 + // code: "lhr1", 98 + // name: "eu-west-2", 99 + // location: "London, United Kingdom", 100 + // flag: "๐Ÿ‡ฌ๐Ÿ‡ง", 101 + // }, 102 + // pdx1: { 103 + // code: "pdx1", 104 + // name: "us-west-2", 105 + // location: "Portland, USA", 106 + // flag: "๐Ÿ‡บ๐Ÿ‡ธ", 107 + // }, 108 + // sfo1: { 109 + // code: "sfo1", 110 + // name: "us-west-1", 111 + // location: "San Francisco, USA", 112 + // flag: "๐Ÿ‡บ๐Ÿ‡ธ", 113 + // }, 114 + // sin1: { 115 + // code: "sin1", 116 + // name: "ap-southeast-1", 117 + // location: "Singapore", 118 + // flag: "๐Ÿ‡ธ๐Ÿ‡ฌ", 119 + // }, 120 + // syd1: { 121 + // code: "syd1", 122 + // name: "ap-southeast-2", 123 + // location: "Sydney, Australia", 124 + // flag: "๐Ÿ‡ฆ๐Ÿ‡บ", 125 + // }, 126 + // } as const; 127 + 128 + export const flyRegionsDict: Record< 129 + MonitorFlyRegion, 130 + { 131 + code: MonitorFlyRegion; 132 + location: string; 133 + flag: string; 134 + continent: 135 + | "Europe" 136 + | "North America" 137 + | "South America" 138 + | "Asia" 139 + | "Africa" 140 + | "Oceania"; 141 + } 142 + > = { 143 + ams: { 144 + code: "ams", 145 + location: "Amsterdam, Netherlands", 146 + flag: "๐Ÿ‡ณ๐Ÿ‡ฑ", 147 + continent: "Europe", 14 148 }, 15 - arn1: { 16 - code: "arn1", 17 - name: "eu-north-1", 149 + arn: { 150 + code: "arn", 18 151 location: "Stockholm, Sweden", 19 152 flag: "๐Ÿ‡ธ๐Ÿ‡ช", 153 + continent: "Europe", 20 154 }, 21 - bom1: { 22 - code: "bom1", 23 - name: "ap-south-1", 155 + 156 + atl: { 157 + code: "atl", 158 + location: "Atlanta, Georgia, USA", 159 + flag: "๐Ÿ‡บ๐Ÿ‡ธ", 160 + continent: "North America", 161 + }, 162 + bog: { 163 + code: "bog", 164 + location: "Bogotรก, Colombia", 165 + flag: "๐Ÿ‡จ๐Ÿ‡ด", 166 + continent: "South America", 167 + }, 168 + bom: { 169 + code: "bom", 24 170 location: "Mumbai, India", 25 171 flag: "๐Ÿ‡ฎ๐Ÿ‡ณ", 172 + continent: "Asia", 173 + }, 174 + bos: { 175 + code: "bos", 176 + location: "Boston, Massachusetts, USA", 177 + flag: "๐Ÿ‡บ๐Ÿ‡ธ", 178 + continent: "North America", 26 179 }, 27 - cdg1: { 28 - code: "cdg1", 29 - name: "eu-west-3", 180 + cdg: { 181 + code: "cdg", 30 182 location: "Paris, France", 31 183 flag: "๐Ÿ‡ซ๐Ÿ‡ท", 184 + continent: "Europe", 32 185 }, 33 - cle1: { 34 - code: "cle1", 35 - name: "us-east-2", 36 - location: "Cleveland, USA", 186 + den: { 187 + code: "den", 188 + location: "Denver, Colorado, USA", 37 189 flag: "๐Ÿ‡บ๐Ÿ‡ธ", 190 + continent: "North America", 38 191 }, 39 - cpt1: { 40 - code: "cpt1", 41 - name: "af-south-1", 42 - location: "Cape Town, South Africa", 43 - flag: "๐Ÿ‡ฟ๐Ÿ‡ฆ", 192 + dfw: { 193 + code: "dfw", 194 + location: "Dallas, Texas, USA", 195 + flag: "๐Ÿ‡บ๐Ÿ‡ธ", 196 + continent: "North America", 44 197 }, 45 - dub1: { 46 - code: "dub1", 47 - name: "eu-west-1", 48 - location: "Dublin, Ireland", 49 - flag: "๐Ÿ‡ฎ๐Ÿ‡ช", 198 + ewr: { 199 + code: "ewr", 200 + location: "Secaucus, New Jersey, USA", 201 + flag: "๐Ÿ‡บ๐Ÿ‡ธ", 202 + continent: "North America", 203 + }, 204 + eze: { 205 + code: "eze", 206 + location: "Ezeiza, Argentina", 207 + flag: "๐Ÿ‡ฆ๐Ÿ‡ท", 208 + continent: "South America", 50 209 }, 51 - fra1: { 52 - code: "fra1", 53 - name: "eu-central-1", 210 + fra: { 211 + code: "fra", 54 212 location: "Frankfurt, Germany", 55 213 flag: "๐Ÿ‡ฉ๐Ÿ‡ช", 214 + continent: "Europe", 56 215 }, 57 - gru1: { 58 - code: "gru1", 59 - name: "sa-east-1", 60 - location: "Sรฃo Paulo, Brazil", 216 + gdl: { 217 + code: "gdl", 218 + location: "Guadalajara, Mexico", 219 + flag: "๐Ÿ‡ฒ๐Ÿ‡ฝ", 220 + continent: "North America", 221 + }, 222 + gig: { 223 + code: "gig", 224 + location: "Rio de Janeiro, Brazil", 61 225 flag: "๐Ÿ‡ง๐Ÿ‡ท", 226 + continent: "South America", 62 227 }, 63 - hkg1: { 64 - code: "hkg1", 65 - name: "ap-east-1", 66 - location: "Hong Kong", 67 - flag: "๐Ÿ‡ญ๐Ÿ‡ฐ", 228 + gru: { 229 + code: "gru", 230 + location: "Sao Paulo, Brazil", 231 + flag: "๐Ÿ‡ง๐Ÿ‡ท", 232 + continent: "South America", 68 233 }, 69 - hnd1: { 70 - code: "hnd1", 71 - name: "ap-northeast-1", 72 - location: "Tokyo, Japan", 73 - flag: "๐Ÿ‡ฏ๐Ÿ‡ต", 234 + hkg: { 235 + code: "hkg", 236 + location: "Hong Kong, Hong Kong", 237 + flag: "๐Ÿ‡ญ๐Ÿ‡ฐ", 238 + continent: "Asia", 74 239 }, 75 - iad1: { 76 - code: "iad1", 77 - name: "us-east-1", 78 - location: "Washington, D.C., USA", 240 + 241 + iad: { 242 + code: "iad", 243 + location: "Ashburn, Virginia, USA", 79 244 flag: "๐Ÿ‡บ๐Ÿ‡ธ", 245 + continent: "North America", 80 246 }, 81 - icn1: { 82 - code: "icn1", 83 - name: "ap-northeast-2", 84 - location: "Seoul, South Korea", 85 - flag: "๐Ÿ‡ฐ๐Ÿ‡ท", 247 + jnb: { 248 + code: "jnb", 249 + location: "Johannesburg, South Africa", 250 + flag: "๐Ÿ‡ฟ๐Ÿ‡ฆ", 251 + continent: "Africa", 86 252 }, 87 - kix1: { 88 - code: "kix1", 89 - name: "ap-northeast-3", 90 - location: "Osaka, Japan", 91 - flag: "๐Ÿ‡ฏ๐Ÿ‡ต", 253 + lax: { 254 + code: "lax", 255 + location: "Los Angeles, California, USA", 256 + flag: "๐Ÿ‡บ๐Ÿ‡ธ", 257 + continent: "North America", 92 258 }, 93 - lhr1: { 94 - code: "lhr1", 95 - name: "eu-west-2", 259 + lhr: { 260 + code: "lhr", 96 261 location: "London, United Kingdom", 97 262 flag: "๐Ÿ‡ฌ๐Ÿ‡ง", 263 + continent: "Europe", 98 264 }, 99 - pdx1: { 100 - code: "pdx1", 101 - name: "us-west-2", 102 - location: "Portland, USA", 103 - flag: "๐Ÿ‡บ๐Ÿ‡ธ", 265 + mad: { 266 + code: "mad", 267 + location: "Madrid, Spain", 268 + flag: "๐Ÿ‡ช๐Ÿ‡ธ", 269 + continent: "Europe", 104 270 }, 105 - sfo1: { 106 - code: "sfo1", 107 - name: "us-west-1", 108 - location: "San Francisco, USA", 271 + mia: { 272 + code: "mia", 273 + location: "Miami, Florida, USA", 109 274 flag: "๐Ÿ‡บ๐Ÿ‡ธ", 275 + continent: "North America", 110 276 }, 111 - sin1: { 112 - code: "sin1", 113 - name: "ap-southeast-1", 114 - location: "Singapore", 115 - flag: "๐Ÿ‡ธ๐Ÿ‡ฌ", 277 + nrt: { 278 + code: "nrt", 279 + location: "Tokyo, Japan", 280 + flag: "๐Ÿ‡ฏ๐Ÿ‡ต", 281 + continent: "Asia", 116 282 }, 117 - syd1: { 118 - code: "syd1", 119 - name: "ap-southeast-2", 120 - location: "Sydney, Australia", 121 - flag: "๐Ÿ‡ฆ๐Ÿ‡บ", 283 + ord: { 284 + code: "ord", 285 + location: "Chicago, Illinois, USA", 286 + flag: "๐Ÿ‡บ๐Ÿ‡ธ", 287 + continent: "North America", 122 288 }, 123 - } as const; 124 - 125 - export const flyRegionsDict = { 126 - ams: { 127 - code: "ams", 128 - name: "", 129 - location: "Amsterdam, Netherlands", 130 - flag: "๐Ÿ‡ณ๐Ÿ‡ฑ", 289 + otp: { 290 + code: "otp", 291 + location: "Bucharest, Romania", 292 + flag: "๐Ÿ‡ท๐Ÿ‡ด", 293 + continent: "Europe", 131 294 }, 132 - iad: { 133 - code: "iad", 134 - name: "us-east-1", 135 - location: "Ashburn, Virginia, USA", 295 + phx: { 296 + code: "phx", 297 + location: "Phoenix, Arizona, USA", 136 298 flag: "๐Ÿ‡บ๐Ÿ‡ธ", 299 + continent: "North America", 137 300 }, 138 - jnb: { 139 - code: "jnb", 140 - name: "", 141 - location: "Johannesburg, South Africa", 142 - flag: "๐Ÿ‡ฟ๐Ÿ‡ฆ", 301 + qro: { 302 + code: "qro", 303 + location: "Querรฉtaro, Mexico", 304 + flag: "๐Ÿ‡ฒ๐Ÿ‡ฝ", 305 + continent: "North America", 143 306 }, 144 - hkg: { 145 - code: "hkg", 146 - name: "", 147 - location: "Hong Kong, Hong Kong", 148 - flag: "๐Ÿ‡ญ๐Ÿ‡ฐ", 307 + scl: { 308 + code: "scl", 309 + location: "Santiago, Chile", 310 + flag: "๐Ÿ‡จ๐Ÿ‡ฑ", 311 + continent: "South America", 149 312 }, 150 - gru: { 151 - code: "gru", 152 - name: "", 153 - location: "Sao Paulo, Brazil", 154 - flag: "๐Ÿ‡ง๐Ÿ‡ท", 313 + sjc: { 314 + code: "sjc", 315 + location: "San Jose, California, USA", 316 + flag: "๐Ÿ‡บ๐Ÿ‡ธ", 317 + continent: "North America", 318 + }, 319 + sea: { 320 + code: "sea", 321 + location: "Seattle, Washington, USA", 322 + flag: "๐Ÿ‡บ๐Ÿ‡ธ", 323 + continent: "North America", 324 + }, 325 + sin: { 326 + code: "sin", 327 + location: "Singapore, Singapore", 328 + flag: "๐Ÿ‡ธ๐Ÿ‡ฌ", 329 + continent: "Asia", 155 330 }, 156 331 syd: { 157 332 code: "syd", 158 - name: "", 159 333 location: "Sydney, Australia", 160 334 flag: "๐Ÿ‡ฆ๐Ÿ‡บ", 335 + continent: "Oceania", 336 + }, 337 + waw: { 338 + code: "waw", 339 + location: "Warsaw, Poland", 340 + flag: "๐Ÿ‡ต๐Ÿ‡ฑ", 341 + continent: "Europe", 342 + }, 343 + yul: { 344 + code: "yul", 345 + location: "Montreal, Canada", 346 + flag: "๐Ÿ‡จ๐Ÿ‡ฆ", 347 + continent: "North America", 348 + }, 349 + yyz: { 350 + code: "yyz", 351 + location: "Toronto, Canada", 352 + flag: "๐Ÿ‡จ๐Ÿ‡ฆ", 353 + continent: "North America", 161 354 }, 162 355 } as const; 163 356 357 + // const r = t.flatMap((u) => u[1].continent); 358 + 359 + export const groupByContinent = Object.entries(flyRegionsDict).reduce< 360 + Record< 361 + | "Europe" 362 + | "North America" 363 + | "South America" 364 + | "Asia" 365 + | "Africa" 366 + | "Oceania", 367 + { 368 + code: MonitorFlyRegion; 369 + location: string; 370 + flag: string; 371 + continent: 372 + | "Europe" 373 + | "North America" 374 + | "South America" 375 + | "Asia" 376 + | "Africa" 377 + | "Oceania"; 378 + }[] 379 + > 380 + >( 381 + (acc, [_key, value]) => { 382 + Object.assign(acc, { 383 + [value.continent]: [...acc[value.continent], value], 384 + }); 385 + return acc; 386 + }, 387 + { 388 + "North America": [], 389 + Europe: [], 390 + "South America": [], 391 + Oceania: [], 392 + Asia: [], 393 + Africa: [], 394 + } 395 + ); 396 + 164 397 export const vercelRegions = [ 165 398 "arn1", 166 399 "bom1", ··· 182 415 "syd1", 183 416 ] as const; 184 417 185 - export const flyRegions = ["ams", "iad", "hkg", "jnb", "syd", "gru"] as const; 186 - 187 - export const availableRegions = [...vercelRegions, ...flyRegions] as const; 418 + // export const availableRegions = [...vercelRegions, ...flyRegions] as const; 188 419 189 - export const regionsDict = { ...vercelRegionsDict, ...flyRegionsDict } as const; 420 + // export const regionsDict = { ...vercelRegionsDict, ...flyRegionsDict } as const;
+1
packages/utils/package.json
··· 6 6 "scripts": {}, 7 7 "dependencies": {}, 8 8 "devDependencies": { 9 + "@openstatus/db": "workspace:*", 9 10 "@openstatus/tsconfig": "workspace:*", 10 11 "typescript": "5.4.5" 11 12 },
+3
pnpm-lock.yaml
··· 1137 1137 1138 1138 packages/utils: 1139 1139 devDependencies: 1140 + '@openstatus/db': 1141 + specifier: workspace:* 1142 + version: link:../db 1140 1143 '@openstatus/tsconfig': 1141 1144 specifier: workspace:* 1142 1145 version: link:../tsconfig