diff --git a/apps/web/package.json b/apps/web/package.json index de3b7b62..882bf95d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -21,6 +21,7 @@ "@openstatus/assertions": "workspace:*", "@openstatus/db": "workspace:*", "@openstatus/emails": "workspace:*", + "@openstatus/header-analysis": "workspace:*", "@openstatus/next-monitoring": "0.0.2", "@openstatus/notification-discord": "workspace:*", "@openstatus/notification-emails": "workspace:*", diff --git a/apps/web/src/app/app/(auth)/layout.tsx b/apps/web/src/app/app/(auth)/login/layout.tsx similarity index 50% rename from apps/web/src/app/app/(auth)/layout.tsx rename to apps/web/src/app/app/(auth)/login/layout.tsx index 21814f3b..40424c0a 100644 --- a/apps/web/src/app/app/(auth)/layout.tsx +++ b/apps/web/src/app/app/(auth)/login/layout.tsx @@ -16,36 +16,38 @@ export default async function AuthLayout({ return (
{children} diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/data/_components/data-table-wrapper.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/data/_components/data-table-wrapper.tsx index bf2e096d..a52c22f0 100644 --- a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/data/_components/data-table-wrapper.tsx +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/data/_components/data-table-wrapper.tsx @@ -101,6 +101,7 @@ function Details({ row }: { row: Row }) { diff --git a/apps/web/src/app/play/checker/[id]/page.tsx b/apps/web/src/app/play/checker/[id]/page.tsx index 5a507a40..f0d1dbc5 100644 --- a/apps/web/src/app/play/checker/[id]/page.tsx +++ b/apps/web/src/app/play/checker/[id]/page.tsx @@ -47,7 +47,7 @@ export default async function CheckPage({ params, searchParams }: Props) { const check = data.checks.find((i) => i.region === selectedRegion) || data.checks?.[0]; - const { region, headers, timing } = check; + const { region, headers, timing, status } = check; return ( <> @@ -77,7 +77,7 @@ export default async function CheckPage({ params, searchParams }: Props) {
- +

@@ -85,7 +85,7 @@ export default async function CheckPage({ params, searchParams }: Props) { 1 day. If you want to persist the data,{" "} login diff --git a/apps/web/src/app/sitemap.ts b/apps/web/src/app/sitemap.ts index f756a380..d82a012f 100644 --- a/apps/web/src/app/sitemap.ts +++ b/apps/web/src/app/sitemap.ts @@ -23,9 +23,7 @@ export default function sitemap(): MetadataRoute.Sitemap { "/about", "/blog", "/changelog", - "/app/sign-in", - "/app/sign-up", - "/monitor/openstatus", + "/app/login", ].map((route) => ({ url: addPathToBaseURL(route), lastModified: new Date(), diff --git a/apps/web/src/app/status-page/[domain]/_components/header.tsx b/apps/web/src/app/status-page/[domain]/_components/header.tsx index e897c745..7d9c048a 100644 --- a/apps/web/src/app/status-page/[domain]/_components/header.tsx +++ b/apps/web/src/app/status-page/[domain]/_components/header.tsx @@ -43,7 +43,7 @@ export function Header({ navigation, plan, page }: Props) { ) : null} - + {navigation.map(({ label, href, disabled, segment }) => { const active = segment === selectedSegment; return ( diff --git a/apps/web/src/components/forms/monitor/request-test-button.tsx b/apps/web/src/components/forms/monitor/request-test-button.tsx index c5d131c7..2ecdf061 100644 --- a/apps/web/src/components/forms/monitor/request-test-button.tsx +++ b/apps/web/src/components/forms/monitor/request-test-button.tsx @@ -133,6 +133,7 @@ export function RequestTestButton({ form, pingEndpoint }: Props) {

diff --git a/apps/web/src/components/marketing/in-between-cta.tsx b/apps/web/src/components/marketing/in-between-cta.tsx index 136f267c..4eb2246f 100644 --- a/apps/web/src/components/marketing/in-between-cta.tsx +++ b/apps/web/src/components/marketing/in-between-cta.tsx @@ -50,7 +50,7 @@ export function BottomCTA() { diff --git a/apps/web/src/components/marketing/pricing/pricing-table.tsx b/apps/web/src/components/marketing/pricing/pricing-table.tsx index 4384030d..a08b2aef 100644 --- a/apps/web/src/components/marketing/pricing/pricing-table.tsx +++ b/apps/web/src/components/marketing/pricing/pricing-table.tsx @@ -80,7 +80,9 @@ export function PricingTable({ if (events?.[key]) { return events[key]?.(); } - return router.push(`/app/sign-up?plan=${key}`); + // FIXME: how to properly handle `?redirectTo` with unknown workspaceSlug + // to redirect user to `/app/[workspaceSlug]/settings/billing?plan=${key}`? + return router.push(`/app/login?plan=${key}`); }} disabled={isCurrentPlan || isLoading} > diff --git a/apps/web/src/components/monitor-dashboard/response-details.tsx b/apps/web/src/components/monitor-dashboard/response-details.tsx index cff4796f..802ad269 100644 --- a/apps/web/src/components/monitor-dashboard/response-details.tsx +++ b/apps/web/src/components/monitor-dashboard/response-details.tsx @@ -23,7 +23,7 @@ export async function ResponseDetails(props: ResponseDetailsParams) { const response = details[0]; - const { timing, headers, message } = response; + const { timing, headers, message, statusCode } = response; const defaultValue = headers ? "headers" : timing ? "timing" : "message"; @@ -50,7 +50,9 @@ export async function ResponseDetails(props: ResponseDetailsParams) { - {headers ? : null} + {headers ? ( + + ) : null} {timing ? : null} diff --git a/apps/web/src/components/ping-response-analysis/response-detail-tabs.tsx b/apps/web/src/components/ping-response-analysis/response-detail-tabs.tsx index 62a75811..1b50890c 100644 --- a/apps/web/src/components/ping-response-analysis/response-detail-tabs.tsx +++ b/apps/web/src/components/ping-response-analysis/response-detail-tabs.tsx @@ -14,11 +14,13 @@ import type { Timing } from "./utils"; export async function ResponseDetailTabs({ timing, headers, + status, message, assertions, }: { timing: Timing | null; headers: Record | null; + status: number | null; message?: string | null; assertions?: Assertion[] | null; }) { @@ -43,9 +45,12 @@ export async function ResponseDetailTabs({ - {headers ? : null} + {headers ? ( + + ) : null} + {/* TODO: show hideInfo={false} when in /play/checker page */} {timing ? : null} diff --git a/apps/web/src/components/ping-response-analysis/response-header-analysis.tsx b/apps/web/src/components/ping-response-analysis/response-header-analysis.tsx new file mode 100644 index 00000000..7c550a6a --- /dev/null +++ b/apps/web/src/components/ping-response-analysis/response-header-analysis.tsx @@ -0,0 +1,240 @@ +import "@openstatus/header-analysis"; + +import React from "react"; +import { Info } from "lucide-react"; + +import { + parseCacheControlHeader, + parseCfCacheStatus, + parseCfRay, + parseFlyRequestId, + parseXVercelCache, + parseXVercelId, +} from "@openstatus/header-analysis"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@openstatus/ui"; + +const allowedHeaders = [ + "Cache-Control", + "Cf-Cache-Status", + "Cf-Ray", + "X-Vercel-Cache", + "X-Vercel-Id", + "Location", + "Fly-Request-Id", +]; + +export function ResponseHeaderAnalysis({ + headerKey, + headers, + status, +}: { + headers: Record; + headerKey: string; + status: number; +}) { + const header = headers[headerKey]; + + if (!header) return null; + if (!allowedHeaders.includes(headerKey)) return null; + + return ( + + + + + + + Header Analysis + + Breaking down the{" "} + "{headerKey}"{" "} + header. + + + {(() => { + switch (headerKey) { + case "Cache-Control": + return ; + case "Cf-Cache-Status": + return ; + case "Cf-Ray": + return ; + case "X-Vercel-Cache": + return ; + case "X-Vercel-Id": + return ; + case "Fly-Request-Id": + return ; + case "Location": + return ; + default: + return null; + } + })()} + + + ); +} + +function CacheControl({ header }: { header: string }) { + const values = parseCacheControlHeader(header); + + return ( +
+ {values.map(({ name, value, description }) => { + return ( + +

+ {name}{" "} + {value !== undefined ? ({value}) : null} +

+

{description}

+
+ ); + })} +
+ ); +} + +function CfCacheStatus({ header }: { header: string }) { + const { value, description } = parseCfCacheStatus(header); + + return ( +
+

+ {value} +

+

{description}

+
+ ); +} + +function XVercelCache({ header }: { header: string }) { + const { value, description } = parseXVercelCache(header); + + return ( +
+

+ {value} +

+

{description}

+
+ ); +} + +export function XVercelId({ header }: { header: string }) { + const value = parseXVercelId(header); + + return ( +
+

+ This header contains a list of Edge regions your request hit, as well as + the region the function was executed in (for both Edge and Serverless): +

+
+ {value.status === "failed" ? ( +

{value.error.message}

+ ) : ( + value.data.map(({ code, location, flag }) => ( + +

+ + {code} + +

+

+ {location} {flag} +

+
+ )) + )} +
+
+ ); +} + +export function CfRay({ header }: { header: string }) { + const value = parseCfRay(header); + + return ( +
+

+ This header is a hashed value that encodes information about the data + center and the visitorโ€™s request. The data center the request hit is: +

+ {value.status === "failed" ? ( +

{value.error.message}

+ ) : ( +
+

+ + {value.data.code} + +

+

+ {value.data.location} {value.data.flag} +

+
+ )} +
+ ); +} + +export function Location({ + header, + status, +}: { + header: string; + status: number; +}) { + return ( +
+

+ This header in HTTP responses is used to redirect the client to a new + URL. It is often seen with status codes like 301 (Moved + Permanently) and 302 (Found), guiding the client's + browser to navigate to a different location. +

+
+

+ {status} +

+

{header}

+
+
+ ); +} + +export function FlyRequestId({ header }: { header: string }) { + const value = parseFlyRequestId(header); + + return ( +
+

+ This header is a hashed value that encodes information about the data + center and the visitorโ€™s request. The data center the request hit is: +

+ {value.status === "failed" ? ( +

{value.error.message}

+ ) : ( +
+

+ + {value.data.code} + +

+

+ {value.data.location} {value.data.flag} +

+
+ )} +
+ ); +} diff --git a/apps/web/src/components/ping-response-analysis/response-header-table.tsx b/apps/web/src/components/ping-response-analysis/response-header-table.tsx index 9bd7f479..819b29cc 100644 --- a/apps/web/src/components/ping-response-analysis/response-header-table.tsx +++ b/apps/web/src/components/ping-response-analysis/response-header-table.tsx @@ -9,11 +9,14 @@ import { } from "@openstatus/ui"; import { CopyToClipboardButton } from "./copy-to-clipboard-button"; +import { ResponseHeaderAnalysis } from "./response-header-analysis"; export function ResponseHeaderTable({ headers, + status, }: { headers: Record; + status: number; }) { return ( @@ -28,8 +31,15 @@ export function ResponseHeaderTable({ {Object.entries(headers).map(([key, value]) => ( -
- {key} +
+
+ {key} + +
Response Timing - Timing - Duration + Timing + Duration {Object.entries(timingPhases).map(([key, value]) => { - const { short, long, description } = - timingDict[key as keyof typeof timingPhases]; + const phase = key as keyof typeof timingPhases; + const { short, long, description } = timingDict[phase]; + const { preWidth, width } = timingPhasesWidth[phase]; + return ( -
+

{short}

{!hideInfo ? ( @@ -63,11 +66,17 @@ export function ResponseTimingTable({ {value}ms - + +
diff --git a/apps/web/src/components/ping-response-analysis/utils.ts b/apps/web/src/components/ping-response-analysis/utils.ts index f65b8f8b..9e49d269 100644 --- a/apps/web/src/components/ping-response-analysis/utils.ts +++ b/apps/web/src/components/ping-response-analysis/utils.ts @@ -10,7 +10,7 @@ export function latencyFormatter(value: number) { } export function timestampFormatter(timestamp: number) { - return new Date(timestamp).toLocaleString(); // TODO: properly format the date + return new Date(timestamp).toUTCString(); // GMT format } export function regionFormatter( @@ -43,6 +43,41 @@ export function getTimingPhases(timing: Timing) { }; } +export function getTimingPhasesWidth(timing: Timing) { + const total = getTotalLatency(timing); + const phases = getTimingPhases(timing); + + const dns = { preWidth: 0, width: (phases.dns / total) * 100 }; + + const connection = { + preWidth: dns.preWidth + dns.width, + width: (phases.connection / total) * 100, + }; + + const tls = { + preWidth: connection.preWidth + connection.width, + width: (phases.tls / total) * 100, + }; + + const ttfb = { + preWidth: tls.preWidth + tls.width, + width: (phases.ttfb / total) * 100, + }; + + const transfer = { + preWidth: ttfb.preWidth + ttfb.width, + width: (phases.transfer / total) * 100, + }; + + return { + dns, + connection, + tls, + ttfb, + transfer, + }; +} + export const timingSchema = z.object({ dnsStart: z.number(), dnsDone: z.number(), diff --git a/packages/header-analysis/package.json b/packages/header-analysis/package.json new file mode 100644 index 00000000..b787e52c --- /dev/null +++ b/packages/header-analysis/package.json @@ -0,0 +1,15 @@ +{ + "name": "@openstatus/header-analysis", + "version": "1.0.0", + "description": "", + "main": "src/index.ts", + "scripts": {}, + "dependencies": {}, + "devDependencies": { + "@openstatus/tsconfig": "workspace:*", + "typescript": "5.4.5" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/packages/header-analysis/src/index.ts b/packages/header-analysis/src/index.ts new file mode 100644 index 00000000..90d59056 --- /dev/null +++ b/packages/header-analysis/src/index.ts @@ -0,0 +1,6 @@ +export * from "./parser/cache-control"; +export * from "./parser/cf-cache-status"; +export * from "./parser/cf-ray"; +export * from "./parser/fly-request-id"; +export * from "./parser/x-vercel-cache"; +export * from "./parser/x-vercel-id"; \ No newline at end of file diff --git a/packages/header-analysis/src/parser/cache-control.ts b/packages/header-analysis/src/parser/cache-control.ts new file mode 100644 index 00000000..1de4bcc3 --- /dev/null +++ b/packages/header-analysis/src/parser/cache-control.ts @@ -0,0 +1,63 @@ +interface CacheControlInfo { + directive: string; // unparsed directive + description: string; + name: string; + value?: number; +} + +export function parseCacheControlHeader(header: string): CacheControlInfo[] { + const cacheControlDirectives = header + .split(",") + .map((directive) => directive.trim()); + + const cacheControlInfo: CacheControlInfo[] = []; + + cacheControlDirectives.forEach((directive) => { + const parts = directive.split("="); + + const name = parts[0].trim(); + const value = !isNaN(Number(parts[1])) ? Number(parts[1]) : undefined; + const description = getDirectiveDescription(name); + + cacheControlInfo.push({ description, name, value, directive }); + }); + + return cacheControlInfo; +} + +function getDirectiveDescription(key: string): string { + switch (key.toLowerCase()) { + case "max-age": + return "Specifies the maximum amount of time in seconds that a resource is considered fresh in a cache. After this time, the cache is required to revalidate the resource with the origin server."; + case "max-stale": + return "Indicates that a cache can serve a stale response even after its freshness lifetime has expired. Optionally, a value can be specified to indicate the maximum staleness allowed."; + case "min-fresh": + return "Specifies the minimum amount of time in seconds that a resource must be considered fresh. Caches should not serve a response that is older than this value without revalidating with the origin server."; + case "s-maxage": + return "Similar to max-age, but applies specifically to shared caches, such as proxy servers. Overrides max-age when present in shared caches."; + case "no-cache": + return "Forces caches to submit the request to the origin server for validation before releasing a cached copy. The response must be validated with the origin server before it can be used."; + case "no-store": + return "Instructs caches not to store any part of either the request or response. The content must be obtained from the origin server for each request."; + case "no-transform": + return "Specifies that intermediaries should not modify the content of the resource, such as by transcoding or compressing it."; + case "only-if-cached": + return "Indicates that a cache should respond only if the resource is available in the cache. It should not contact the origin server to retrieve the resource."; + case "must-revalidate": + return "Specifies that caches must revalidate stale responses with the origin server before using them."; + case "proxy-revalidate": + return "Similar to must-revalidate, but applies specifically to shared caches. It indicates that shared caches must revalidate stale responses with the origin server before using them."; + case "private": + return "Indicates that the response is intended for a single user and should not be cached by shared caches."; + case "public": + return "Specifies that the response may be cached by any cache, including both private and shared caches."; + case "immutable": + return "Indicates that the response body will not change over time. Caches can store immutable responses indefinitely."; + case "stale-while-revalidate": + return "Allows a cache to serve stale responses while asynchronously revalidating them with the origin server in the background."; + case "stale-if-error": + return "Allows a cache to serve stale responses if the origin server is unavailable or returns an error."; + default: + return ""; + } +} diff --git a/packages/header-analysis/src/parser/cf-cache-status.ts b/packages/header-analysis/src/parser/cf-cache-status.ts new file mode 100644 index 00000000..08bfa5e5 --- /dev/null +++ b/packages/header-analysis/src/parser/cf-cache-status.ts @@ -0,0 +1,26 @@ +interface CfCacheStatusInfo { + description: string; + value: string; +} + +export function parseCfCacheStatus(header: string): CfCacheStatusInfo { + const description = getCacheDescription(header); + return { description, value: header }; +} + +function getCacheDescription(key: string): string { + switch (key.toUpperCase()) { + case "HIT": + return "Your resource was found in Cloudflareโ€™s cache. This means that it has been previously accessed from your original server and loaded into Cache. It has not expired."; + case "MISS": + return "Cloudflare looked for your resource in cache but did not find it. Cloudflare went back to your origin server to retrieve the resource. The next time this resource is accessed its status should be HIT."; + case "BYPASS": + return "Cloudflare has been instructed to not cache this asset. It has been served directly from the origin. This is usually because something like an existing NO-CACHE header is being respected."; + case "EXPIRED": + return "Cloudflare has previously retrieved this resource, but its cache has expired. Cloudflare will go back to the origin to retrieve this resource again. The next time this resource is accessed its status should be HIT."; + case "DYNAMIC": + return "This resource is not cached by default and there are no explicit settings configured to cache it. You will see this frequently when Cloudflare is handling a POST request. This request will always go to the origin."; + default: + return ""; + } +} diff --git a/packages/header-analysis/src/parser/cf-ray.ts b/packages/header-analysis/src/parser/cf-ray.ts new file mode 100644 index 00000000..beebf454 --- /dev/null +++ b/packages/header-analysis/src/parser/cf-ray.ts @@ -0,0 +1,22 @@ +// List of all Cloudflare data center taken by https://www.feitsui.com/en/article/26 +import { regions } from "../regions/cloudflare"; +import type { ParserReturn, Region } from "../types"; + +export function parseCfRay(header: string): ParserReturn { + const regex = /\b([A-Z]{3})\b/g; + const arr = header.match(regex); + + if (!arr || !Array.isArray(arr) || arr.length === 0) { + return { status: "failed", error: new Error("Couldn't parse the header.") }; + } + + const region = regions[arr[0]]; + if (region) return { status: "success", data: region }; + + return { + status: "failed", + error: new Error( + `It seems like the data center '${arr[0]}' (iata) is not listed.`, + ), + }; +} diff --git a/packages/header-analysis/src/parser/fly-request-id.ts b/packages/header-analysis/src/parser/fly-request-id.ts new file mode 100644 index 00000000..26589835 --- /dev/null +++ b/packages/header-analysis/src/parser/fly-request-id.ts @@ -0,0 +1,21 @@ +import { regions } from "../regions/fly"; +import type { ParserReturn, Region } from "../types"; + +export function parseFlyRequestId(header: string): ParserReturn { + const regex = /\b([a-z]{3})\b/g; + const arr = header.match(regex); + + if (!arr || arr.length === 0) { + return { status: "failed", error: new Error("Couldn't parse the header.") }; + } + + const region = regions[arr[0]]; + if (region) return { status: "success", data: region }; + + return { + status: "failed", + error: new Error( + `It seems like the data center '${arr[0]}' (iata) is not listed.`, + ), + }; +} diff --git a/packages/header-analysis/src/parser/x-vercel-cache.ts b/packages/header-analysis/src/parser/x-vercel-cache.ts new file mode 100644 index 00000000..70e3ee6b --- /dev/null +++ b/packages/header-analysis/src/parser/x-vercel-cache.ts @@ -0,0 +1,26 @@ +interface VercelCacheInfo { + description: string; + value: string; +} + +export function parseXVercelCache(header: string): VercelCacheInfo { + const description = getCacheDescription(header); + return { description, value: header }; +} + +function getCacheDescription(key: string): string { + switch (key.toUpperCase()) { + case "MISS": + return "The response was not found in the edge cache and was fetched from the origin server."; + case "HIT": + return "The response was served from the edge cache."; + case "STALE": + return "The response was served from the edge cache. A background request to the origin server was made to update the content."; + case "PRERENDER": + return "The response was served from static storage."; + case "REVLIDATED": + return "The response was served from the origin server and the cache was refreshed due to an authorization from the user in the incoming request."; + default: + return ""; + } +} diff --git a/packages/header-analysis/src/parser/x-vercel-id.ts b/packages/header-analysis/src/parser/x-vercel-id.ts new file mode 100644 index 00000000..b282ad41 --- /dev/null +++ b/packages/header-analysis/src/parser/x-vercel-id.ts @@ -0,0 +1,18 @@ +import { regions } from "../regions/vercel"; +import type { ParserReturn, Region } from "../types"; + +export function parseXVercelId(header: string): ParserReturn { + const regex = /([a-z]{3}[0-9])+:+/g; + + const arr = header.match(regex); + if (!arr || !arr.length) { + return { status: "failed", error: new Error("Couldn't parse the header.") }; + } + + const data = arr.map((r) => { + const regionId = r.replace(/:+/, ""); + return regions[regionId]; + }); + + return { status: "success", data }; +} diff --git a/packages/header-analysis/src/regions/cloudflare.ts b/packages/header-analysis/src/regions/cloudflare.ts new file mode 100644 index 00000000..5bf5ed7c --- /dev/null +++ b/packages/header-analysis/src/regions/cloudflare.ts @@ -0,0 +1,289 @@ +import type { Region } from "../types"; + +// REMINDER: nono-official data center list +// https://www.feitsui.com/en/article/26 +export const regions: Record = { + CGB: { code: "CGB", location: "Cuiabรก, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + COR: { code: "COR", location: "Cรณrdoba, Argentina", flag: "๐Ÿ‡ฆ๐Ÿ‡ท" }, + BTS: { code: "BTS", location: "Bratislava, Slovakia", flag: "๐Ÿ‡ธ๐Ÿ‡ฐ" }, + KHH: { code: "KHH", location: "Kaohsiung, Taiwan", flag: "๐Ÿ‡น๐Ÿ‡ผ" }, + GND: { code: "GND", location: "Grenada", flag: "๐Ÿ‡ฌ๐Ÿ‡ฉ" }, + NVT: { code: "NVT", location: "Navegantes, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + ISU: { code: "ISU", location: "Sulaimaniyah, Iraq", flag: "๐Ÿ‡ฎ๐Ÿ‡ถ" }, + PAP: { code: "PAP", location: "Port-au-Prince, Haiti", flag: "๐Ÿ‡ญ๐Ÿ‡น" }, + KLD: { code: "KLD", location: "Tver, Russia", flag: "๐Ÿ‡ท๐Ÿ‡บ" }, + REC: { code: "REC", location: "Recife, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + FSD: { code: "FSD", location: "Sioux Falls, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + ADB: { code: "ADB", location: "Izmir, Turkey", flag: "๐Ÿ‡น๐Ÿ‡ท" }, + CHC: { code: "CHC", location: "Christchurch, New Zealand", flag: "๐Ÿ‡ณ๐Ÿ‡ฟ" }, + UDI: { code: "UDI", location: "Uberlรขndia, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + PPT: { code: "PPT", location: "Papeete, French Polynesia", flag: "๐Ÿ‡ต๐Ÿ‡ซ" }, + KIN: { code: "KIN", location: "Kingston, Jamaica", flag: "๐Ÿ‡ฏ๐Ÿ‡ฒ" }, + STR: { code: "STR", location: "Stuttgart, Germany", flag: "๐Ÿ‡ฉ๐Ÿ‡ช" }, + COK: { code: "COK", location: "Kochi, India", flag: "๐Ÿ‡ฎ๐Ÿ‡ณ" }, + KJA: { code: "KJA", location: "Krasnoyarsk, Russia", flag: "๐Ÿ‡ท๐Ÿ‡บ" }, + AUS: { code: "AUS", location: "Austin, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + ORN: { code: "ORN", location: "Oran, Algeria", flag: "๐Ÿ‡ฉ๐Ÿ‡ฟ" }, + ACC: { code: "ACC", location: "Accra, Ghana", flag: "๐Ÿ‡ฌ๐Ÿ‡ญ" }, + GDL: { code: "GDL", location: "Guadalajara, Mexico", flag: "๐Ÿ‡ฒ๐Ÿ‡ฝ" }, + SJK: { code: "SJK", location: "Sรฃo Josรฉ dos Campos, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + AAE: { code: "AAE", location: "Annaba, Algeria", flag: "๐Ÿ‡ฉ๐Ÿ‡ฟ" }, + MDL: { code: "MDL", location: "Mandalay, Myanmar", flag: "๐Ÿ‡ฒ๐Ÿ‡ฒ" }, + DPS: { code: "DPS", location: "Denpasar, Indonesia", flag: "๐Ÿ‡ฎ๐Ÿ‡ฉ" }, + VIX: { code: "VIX", location: "Vitรณria, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + FUK: { code: "FUK", location: "Fukuoka, Japan", flag: "๐Ÿ‡ฏ๐Ÿ‡ต" }, + CNN: { code: "CNN", location: "Chengdu, China", flag: "๐Ÿ‡จ๐Ÿ‡ณ" }, + LYS: { code: "LYS", location: "Lyon, France", flag: "๐Ÿ‡ซ๐Ÿ‡ท" }, + XAP: { code: "XAP", location: "Chapecรณ, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + CAI: { code: "CAI", location: "Cairo, Egypt", flag: "๐Ÿ‡ช๐Ÿ‡ฌ" }, + RDU: { code: "RDU", location: "Raleigh-Durham, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + CAW: { code: "CAW", location: "Campos dos Goytacazes, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + OKC: { code: "OKC", location: "Oklahoma City, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + SDQ: { + code: "SDQ", + location: "Santo Domingo, Dominican Republic", + flag: "๐Ÿ‡ฉ๐Ÿ‡ด", + }, + OUA: { code: "OUA", location: "Ouagadougou, Burkina Faso", flag: "๐Ÿ‡ง๐Ÿ‡ซ" }, + YHZ: { code: "YHZ", location: "Halifax, Canada", flag: "๐Ÿ‡จ๐Ÿ‡ฆ" }, + ANC: { code: "ANC", location: "Anchorage, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + LPB: { code: "LPB", location: "La Paz, Bolivia", flag: "๐Ÿ‡ง๐Ÿ‡ด" }, + SUV: { code: "SUV", location: "Suva, Fiji", flag: "๐Ÿ‡ซ๐Ÿ‡ฏ" }, + BGR: { code: "BGR", location: "Bangor, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + SJU: { code: "SJU", location: "San Juan, Puerto Rico", flag: "๐Ÿ‡ต๐Ÿ‡ท" }, + BBI: { code: "BBI", location: "Bhubaneswar, India", flag: "๐Ÿ‡ฎ๐Ÿ‡ณ" }, + ALG: { code: "ALG", location: "Algiers, Algeria", flag: "๐Ÿ‡ฉ๐Ÿ‡ฟ" }, + LAD: { code: "LAD", location: "Luanda, Angola", flag: "๐Ÿ‡ฆ๐Ÿ‡ด" }, + EZE: { code: "EZE", location: "Buenos Aires, Argentina", flag: "๐Ÿ‡ฆ๐Ÿ‡ท" }, + NQN: { code: "NQN", location: "Neuquรฉn, Argentina", flag: "๐Ÿ‡ฆ๐Ÿ‡ท" }, + EVN: { code: "EVN", location: "Yerevan, Armenia", flag: "๐Ÿ‡ฆ๐Ÿ‡ฒ" }, + ADL: { code: "ADL", location: "Adelaide, Australia", flag: "๐Ÿ‡ฆ๐Ÿ‡บ" }, + BNE: { code: "BNE", location: "Brisbane, Australia", flag: "๐Ÿ‡ฆ๐Ÿ‡บ" }, + CBR: { code: "CBR", location: "Canberra, Australia", flag: "๐Ÿ‡ฆ๐Ÿ‡บ" }, + HBA: { code: "HBA", location: "Hobart, Australia", flag: "๐Ÿ‡ฆ๐Ÿ‡บ" }, + MEL: { code: "MEL", location: "Melbourne, Australia", flag: "๐Ÿ‡ฆ๐Ÿ‡บ" }, + PER: { code: "PER", location: "Perth, Australia", flag: "๐Ÿ‡ฆ๐Ÿ‡บ" }, + SYD: { code: "SYD", location: "Sydney, Australia", flag: "๐Ÿ‡ฆ๐Ÿ‡บ" }, + VIE: { code: "VIE", location: "Vienna, Austria", flag: "๐Ÿ‡ฆ๐Ÿ‡น" }, + LLK: { code: "LLK", location: "Luleรฅ, Sweden", flag: "๐Ÿ‡ธ๐Ÿ‡ช" }, + GYD: { code: "GYD", location: "Baku, Azerbaijan", flag: "๐Ÿ‡ฆ๐Ÿ‡ฟ" }, + BAH: { code: "BAH", location: "Manama, Bahrain", flag: "๐Ÿ‡ง๐Ÿ‡ญ" }, + CGP: { code: "CGP", location: "Chittagong, Bangladesh", flag: "๐Ÿ‡ง๐Ÿ‡ฉ" }, + DAC: { code: "DAC", location: "Dhaka, Bangladesh", flag: "๐Ÿ‡ง๐Ÿ‡ฉ" }, + JSR: { code: "JSR", location: "Jessore, Bangladesh", flag: "๐Ÿ‡ง๐Ÿ‡ฉ" }, + MSQ: { code: "MSQ", location: "Minsk, Belarus", flag: "๐Ÿ‡ง๐Ÿ‡พ" }, + BRU: { code: "BRU", location: "Brussels, Belgium", flag: "๐Ÿ‡ง๐Ÿ‡ช" }, + PBH: { code: "PBH", location: "Paro, Bhutan", flag: "๐Ÿ‡ง๐Ÿ‡น" }, + GBE: { code: "GBE", location: "Gaborone, Botswana", flag: "๐Ÿ‡ง๐Ÿ‡ผ" }, + QWJ: { code: "QWJ", location: "Bouake, Ivory Coast", flag: "๐Ÿ‡จ๐Ÿ‡ฎ" }, + CNF: { code: "CNF", location: "Belo Horizonte, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + BEL: { code: "BEL", location: "Belรฉm, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + BNU: { code: "BNU", location: "Blumenau, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + BSB: { code: "BSB", location: "Brasรญlia, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + VCP: { code: "VCP", location: "Campinas, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + CFC: { code: "CFC", location: "Cafelรขndia, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + CWB: { code: "CWB", location: "Curitiba, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + FLN: { code: "FLN", location: "Florianรณpolis, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + FOR: { code: "FOR", location: "Fortaleza, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + GYN: { code: "GYN", location: "Goiรขnia, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + ITJ: { code: "ITJ", location: "Itajaรญ, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + JOI: { code: "JOI", location: "Joinville, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + JDO: { code: "JDO", location: "Juazeiro do Norte, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + MAO: { code: "MAO", location: "Manaus, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + POA: { code: "POA", location: "Porto Alegre, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + RAO: { code: "RAO", location: "Ribeirรฃo Preto, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + GIG: { code: "GIG", location: "Rio de Janeiro, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + SSA: { code: "SSA", location: "Salvador, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + SOD: { code: "SOD", location: "Sorocaba, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + SJP: { code: "SJP", location: "Sรฃo Josรฉ do Rio Preto, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + GRU: { code: "GRU", location: "Sรฃo Paulo, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + BWN: { code: "BWN", location: "Bandar Seri Begawan, Brunei", flag: "๐Ÿ‡ง๐Ÿ‡ณ" }, + SOF: { code: "SOF", location: "Sofia, Bulgaria", flag: "๐Ÿ‡ง๐Ÿ‡ฌ" }, + PNH: { code: "PNH", location: "Phnom Penh, Cambodia", flag: "๐Ÿ‡ฐ๐Ÿ‡ญ" }, + YYC: { code: "YYC", location: "Calgary, Canada", flag: "๐Ÿ‡จ๐Ÿ‡ฆ" }, + YUL: { code: "YUL", location: "Montreal, Canada", flag: "๐Ÿ‡จ๐Ÿ‡ฆ" }, + YOW: { code: "YOW", location: "Ottawa, Canada", flag: "๐Ÿ‡จ๐Ÿ‡ฆ" }, + YXE: { code: "YXE", location: "Saskatoon, Canada", flag: "๐Ÿ‡จ๐Ÿ‡ฆ" }, + YYZ: { code: "YYZ", location: "Toronto, Canada", flag: "๐Ÿ‡จ๐Ÿ‡ฆ" }, + YVR: { code: "YVR", location: "Vancouver, Canada", flag: "๐Ÿ‡จ๐Ÿ‡ฆ" }, + YWG: { code: "YWG", location: "Winnipeg, Canada", flag: "๐Ÿ‡จ๐Ÿ‡ฆ" }, + ARI: { code: "ARI", location: "Arica, Chile", flag: "๐Ÿ‡จ๐Ÿ‡ฑ" }, + SCL: { code: "SCL", location: "Santiago, Chile", flag: "๐Ÿ‡จ๐Ÿ‡ฑ" }, + HKG: { code: "HKG", location: "Hong Kong, Hong Kong", flag: "๐Ÿ‡ญ๐Ÿ‡ฐ" }, + MFM: { code: "MFM", location: "Macau, Macau", flag: "๐Ÿ‡ฒ๐Ÿ‡ด" }, + TPE: { code: "TPE", location: "Taipei, Taiwan", flag: "๐Ÿ‡น๐Ÿ‡ผ" }, + BOG: { code: "BOG", location: "Bogotรก, Colombia", flag: "๐Ÿ‡จ๐Ÿ‡ด" }, + MDE: { code: "MDE", location: "Medellรญn, Colombia", flag: "๐Ÿ‡จ๐Ÿ‡ด" }, + SJO: { code: "SJO", location: "San Josรฉ, Costa Rica", flag: "๐Ÿ‡จ๐Ÿ‡ท" }, + ZAG: { code: "ZAG", location: "Zagreb, Croatia", flag: "๐Ÿ‡ญ๐Ÿ‡ท" }, + LCA: { code: "LCA", location: "Larnaca, Cyprus", flag: "๐Ÿ‡จ๐Ÿ‡พ" }, + PRG: { code: "PRG", location: "Prague, Czech Republic", flag: "๐Ÿ‡จ๐Ÿ‡ฟ" }, + CPH: { code: "CPH", location: "Copenhagen, Denmark", flag: "๐Ÿ‡ฉ๐Ÿ‡ฐ" }, + JIB: { code: "JIB", location: "Djibouti", flag: "๐Ÿ‡ฉ๐Ÿ‡ฏ" }, + GYE: { code: "GYE", location: "Guayaquil, Ecuador", flag: "๐Ÿ‡ช๐Ÿ‡จ" }, + UIO: { code: "UIO", location: "Quito, Ecuador", flag: "๐Ÿ‡ช๐Ÿ‡จ" }, + TLL: { code: "TLL", location: "Tallinn, Estonia", flag: "๐Ÿ‡ช๐Ÿ‡ช" }, + HEL: { code: "HEL", location: "Helsinki, Finland", flag: "๐Ÿ‡ซ๐Ÿ‡ฎ" }, + MRS: { code: "MRS", location: "Marseille, France", flag: "๐Ÿ‡ซ๐Ÿ‡ท" }, + CDG: { code: "CDG", location: "Paris, France", flag: "๐Ÿ‡ซ๐Ÿ‡ท" }, + RUN: { code: "RUN", location: "Saint-Denis, Rรฉunion", flag: "๐Ÿ‡ท๐Ÿ‡ช" }, + TBS: { code: "TBS", location: "Tbilisi, Georgia", flag: "๐Ÿ‡ฌ๐Ÿ‡ช" }, + TXL: { code: "TXL", location: "Berlin, Germany", flag: "๐Ÿ‡ฉ๐Ÿ‡ช" }, + DUS: { code: "DUS", location: "Dรผsseldorf, Germany", flag: "๐Ÿ‡ฉ๐Ÿ‡ช" }, + FRA: { code: "FRA", location: "Frankfurt, Germany", flag: "๐Ÿ‡ฉ๐Ÿ‡ช" }, + HAM: { code: "HAM", location: "Hamburg, Germany", flag: "๐Ÿ‡ฉ๐Ÿ‡ช" }, + MUC: { code: "MUC", location: "Munich, Germany", flag: "๐Ÿ‡ฉ๐Ÿ‡ช" }, + ATH: { code: "ATH", location: "Athens, Greece", flag: "๐Ÿ‡ฌ๐Ÿ‡ท" }, + SKG: { code: "SKG", location: "Thessaloniki, Greece", flag: "๐Ÿ‡ฌ๐Ÿ‡ท" }, + GUM: { code: "GUM", location: "Hagรฅtรฑa, Guam", flag: "๐Ÿ‡ฌ๐Ÿ‡บ" }, + GUA: { code: "GUA", location: "Guatemala City, Guatemala", flag: "๐Ÿ‡ฌ๐Ÿ‡น" }, + GEO: { code: "GEO", location: "Georgetown, Guyana", flag: "๐Ÿ‡ฌ๐Ÿ‡พ" }, + TGU: { code: "TGU", location: "Tegucigalpa, Honduras", flag: "๐Ÿ‡ญ๐Ÿ‡ณ" }, + BUD: { code: "BUD", location: "Budapest, Hungary", flag: "๐Ÿ‡ญ๐Ÿ‡บ" }, + KEF: { code: "KEF", location: "Reykjavรญk, Iceland", flag: "๐Ÿ‡ฎ๐Ÿ‡ธ" }, + AMD: { code: "AMD", location: "Ahmedabad, India", flag: "๐Ÿ‡ฎ๐Ÿ‡ณ" }, + BLR: { code: "BLR", location: "Bengaluru, India", flag: "๐Ÿ‡ฎ๐Ÿ‡ณ" }, + IXC: { code: "IXC", location: "Chandigarh, India", flag: "๐Ÿ‡ฎ๐Ÿ‡ณ" }, + MAA: { code: "MAA", location: "Chennai, India", flag: "๐Ÿ‡ฎ๐Ÿ‡ณ" }, + HYD: { code: "HYD", location: "Hyderabad, India", flag: "๐Ÿ‡ฎ๐Ÿ‡ณ" }, + KNU: { code: "KNU", location: "Kanpur, India", flag: "๐Ÿ‡ฎ๐Ÿ‡ณ" }, + CCU: { code: "CCU", location: "Kolkata, India", flag: "๐Ÿ‡ฎ๐Ÿ‡ณ" }, + BOM: { code: "BOM", location: "Mumbai, India", flag: "๐Ÿ‡ฎ๐Ÿ‡ณ" }, + NAG: { code: "NAG", location: "Nagpur, India", flag: "๐Ÿ‡ฎ๐Ÿ‡ณ" }, + DEL: { code: "DEL", location: "New Delhi, India", flag: "๐Ÿ‡ฎ๐Ÿ‡ณ" }, + PAT: { code: "PAT", location: "Patna, India", flag: "๐Ÿ‡ฎ๐Ÿ‡ณ" }, + CGK: { code: "CGK", location: "Jakarta, Indonesia", flag: "๐Ÿ‡ฎ๐Ÿ‡ฉ" }, + JOG: { code: "JOG", location: "Yogyakarta, Indonesia", flag: "๐Ÿ‡ฎ๐Ÿ‡ฉ" }, + BGW: { code: "BGW", location: "Baghdad, Iraq", flag: "๐Ÿ‡ฎ๐Ÿ‡ถ" }, + BSR: { code: "BSR", location: "Basra, Iraq", flag: "๐Ÿ‡ฎ๐Ÿ‡ถ" }, + EBL: { code: "EBL", location: "Erbil, Iraq", flag: "๐Ÿ‡ฎ๐Ÿ‡ถ" }, + NJF: { code: "NJF", location: "Najaf, Iraq", flag: "๐Ÿ‡ฎ๐Ÿ‡ถ" }, + XNH: { code: "XNH", location: "Nouadhibou, Mauritania", flag: "๐Ÿ‡ฒ๐Ÿ‡ท" }, + ORK: { code: "ORK", location: "Cork, Ireland", flag: "๐Ÿ‡ฎ๐Ÿ‡ช" }, + DUB: { code: "DUB", location: "Dublin, Ireland", flag: "๐Ÿ‡ฎ๐Ÿ‡ช" }, + HFA: { code: "HFA", location: "Haifa, Israel", flag: "๐Ÿ‡ฎ๐Ÿ‡ฑ" }, + TLV: { code: "TLV", location: "Tel-Aviv, Israel", flag: "๐Ÿ‡ฎ๐Ÿ‡ฑ" }, + MXP: { code: "MXP", location: "Milan, Italy", flag: "๐Ÿ‡ฎ๐Ÿ‡น" }, + PMO: { code: "PMO", location: "Palermo, Italy", flag: "๐Ÿ‡ฎ๐Ÿ‡น" }, + FCO: { code: "FCO", location: "Rome, Italy", flag: "๐Ÿ‡ฎ๐Ÿ‡น" }, + OKA: { code: "OKA", location: "Okinawa, Japan", flag: "๐Ÿ‡ฏ๐Ÿ‡ต" }, + KIX: { code: "KIX", location: "Osaka, Japan", flag: "๐Ÿ‡ฏ๐Ÿ‡ต" }, + NRT: { code: "NRT", location: "Tokyo, Japan", flag: "๐Ÿ‡ฏ๐Ÿ‡ต" }, + AMM: { code: "AMM", location: "Amman, Jordan", flag: "๐Ÿ‡ฏ๐Ÿ‡ด" }, + ALA: { code: "ALA", location: "Almaty, Kazakhstan", flag: "๐Ÿ‡ฐ๐Ÿ‡ฟ" }, + MBA: { code: "MBA", location: "Mombasa, Kenya", flag: "๐Ÿ‡ฐ๐Ÿ‡ช" }, + NBO: { code: "NBO", location: "Nairobi, Kenya", flag: "๐Ÿ‡ฐ๐Ÿ‡ช" }, + KWI: { code: "KWI", location: "Kuwait City, Kuwait", flag: "๐Ÿ‡ฐ๐Ÿ‡ผ" }, + VTE: { code: "VTE", location: "Vientiane, Laos", flag: "๐Ÿ‡ฑ๐Ÿ‡ฆ" }, + RIX: { code: "RIX", location: "Riga, Latvia", flag: "๐Ÿ‡ฑ๐Ÿ‡ป" }, + VNO: { code: "VNO", location: "Vilnius, Lithuania", flag: "๐Ÿ‡ฑ๐Ÿ‡น" }, + LUX: { code: "LUX", location: "Luxembourg", flag: "๐Ÿ‡ฑ๐Ÿ‡บ" }, + TNR: { code: "TNR", location: "Antananarivo, Madagascar", flag: "๐Ÿ‡ฒ๐Ÿ‡ฌ" }, + JHB: { code: "JHB", location: "Johor Bahru, Malaysia", flag: "๐Ÿ‡ฒ๐Ÿ‡พ" }, + KUL: { code: "KUL", location: "Kuala Lumpur, Malaysia", flag: "๐Ÿ‡ฒ๐Ÿ‡พ" }, + MLE: { code: "MLE", location: "Malรฉ, Maldives", flag: "๐Ÿ‡ฒ๐Ÿ‡ป" }, + MRU: { code: "MRU", location: "Port Louis, Mauritius", flag: "๐Ÿ‡ฒ๐Ÿ‡บ" }, + MEX: { code: "MEX", location: "Mexico City, Mexico", flag: "๐Ÿ‡ฒ๐Ÿ‡ฝ" }, + QRO: { code: "QRO", location: "Querรฉtaro, Mexico", flag: "๐Ÿ‡ฒ๐Ÿ‡ฝ" }, + KIV: { code: "KIV", location: "Chiศ™inฤƒu, Moldova", flag: "๐Ÿ‡ฒ๐Ÿ‡ฉ" }, + ULN: { code: "ULN", location: "Ulaanbaatar, Mongolia", flag: "๐Ÿ‡ฒ๐Ÿ‡ณ" }, + CMN: { code: "CMN", location: "Casablanca, Morocco", flag: "๐Ÿ‡ฒ๐Ÿ‡ฆ" }, + MPM: { code: "MPM", location: "Maputo, Mozambique", flag: "๐Ÿ‡ฒ๐Ÿ‡ฟ" }, + RGN: { code: "RGN", location: "Yangon, Myanmar", flag: "๐Ÿ‡ฒ๐Ÿ‡ฒ" }, + KTM: { code: "KTM", location: "Kathmandu, Nepal", flag: "๐Ÿ‡ณ๐Ÿ‡ต" }, + AMS: { code: "AMS", location: "Amsterdam, Netherlands", flag: "๐Ÿ‡ณ๐Ÿ‡ฑ" }, + NOU: { code: "NOU", location: "Noumรฉa, New Caledonia", flag: "๐Ÿ‡ณ๐Ÿ‡จ" }, + AKL: { code: "AKL", location: "Auckland, New Zealand", flag: "๐Ÿ‡ณ๐Ÿ‡ฟ" }, + LOS: { code: "LOS", location: "Lagos, Nigeria", flag: "๐Ÿ‡ณ๐Ÿ‡ฌ" }, + OSL: { code: "OSL", location: "Oslo, Norway", flag: "๐Ÿ‡ณ๐Ÿ‡ด" }, + MCT: { code: "MCT", location: "Muscat, Oman", flag: "๐Ÿ‡ด๐Ÿ‡ฒ" }, + ISB: { code: "ISB", location: "Islamabad, Pakistan", flag: "๐Ÿ‡ต๐Ÿ‡ฐ" }, + KHI: { code: "KHI", location: "Karachi, Pakistan", flag: "๐Ÿ‡ต๐Ÿ‡ฐ" }, + LHE: { code: "LHE", location: "Lahore, Pakistan", flag: "๐Ÿ‡ต๐Ÿ‡ฐ" }, + ZDM: { code: "ZDM", location: "Zahedan, Iran", flag: "๐Ÿ‡ฎ๐Ÿ‡ท" }, + PTY: { code: "PTY", location: "Panama City, Panama", flag: "๐Ÿ‡ต๐Ÿ‡ฆ" }, + ASU: { code: "ASU", location: "Asunciรณn, Paraguay", flag: "๐Ÿ‡ต๐Ÿ‡พ" }, + LIM: { code: "LIM", location: "Lima, Peru", flag: "๐Ÿ‡ต๐Ÿ‡ช" }, + CGY: { code: "CGY", location: "Cagayan de Oro, Philippines", flag: "๐Ÿ‡ต๐Ÿ‡ญ" }, + CEB: { code: "CEB", location: "Cebu, Philippines", flag: "๐Ÿ‡ต๐Ÿ‡ญ" }, + MNL: { code: "MNL", location: "Manila, Philippines", flag: "๐Ÿ‡ต๐Ÿ‡ญ" }, + WAW: { code: "WAW", location: "Warsaw, Poland", flag: "๐Ÿ‡ต๐Ÿ‡ฑ" }, + LIS: { code: "LIS", location: "Lisbon, Portugal", flag: "๐Ÿ‡ต๐Ÿ‡น" }, + DOH: { code: "DOH", location: "Doha, Qatar", flag: "๐Ÿ‡ถ๐Ÿ‡ฆ" }, + OTP: { code: "OTP", location: "Bucharest, Romania", flag: "๐Ÿ‡ท๐Ÿ‡ด" }, + LED: { code: "LED", location: "Saint Petersburg, Russia", flag: "๐Ÿ‡ท๐Ÿ‡บ" }, + DME: { code: "DME", location: "Moscow, Russia", flag: "๐Ÿ‡ท๐Ÿ‡บ" }, + KZN: { code: "KZN", location: "Kazan, Russia", flag: "๐Ÿ‡ท๐Ÿ‡บ" }, + KRR: { code: "KRR", location: "Krasnodar, Russia", flag: "๐Ÿ‡ท๐Ÿ‡บ" }, + SVX: { code: "SVX", location: "Yekaterinburg, Russia", flag: "๐Ÿ‡ท๐Ÿ‡บ" }, + KGL: { code: "KGL", location: "Kigali, Rwanda", flag: "๐Ÿ‡ท๐Ÿ‡ผ" }, + JED: { code: "JED", location: "Jeddah, Saudi Arabia", flag: "๐Ÿ‡ธ๐Ÿ‡ฆ" }, + RUH: { code: "RUH", location: "Riyadh, Saudi Arabia", flag: "๐Ÿ‡ธ๐Ÿ‡ฆ" }, + DKR: { code: "DKR", location: "Dakar, Senegal", flag: "๐Ÿ‡ธ๐Ÿ‡ณ" }, + BEG: { code: "BEG", location: "Belgrade, Serbia", flag: "๐Ÿ‡ท๐Ÿ‡ธ" }, + SGP: { code: "SGP", location: "Singapore", flag: "๐Ÿ‡ธ๐Ÿ‡ฌ" }, + LJU: { code: "LJU", location: "Ljubljana, Slovenia", flag: "๐Ÿ‡ธ๐Ÿ‡ฎ" }, + CPT: { code: "CPT", location: "Cape Town, South Africa", flag: "๐Ÿ‡ฟ๐Ÿ‡ฆ" }, + DUR: { code: "DUR", location: "Durban, South Africa", flag: "๐Ÿ‡ฟ๐Ÿ‡ฆ" }, + JNB: { code: "JNB", location: "Johannesburg, South Africa", flag: "๐Ÿ‡ฟ๐Ÿ‡ฆ" }, + ICN: { code: "ICN", location: "Seoul, South Korea", flag: "๐Ÿ‡ฐ๐Ÿ‡ท" }, + MAD: { code: "MAD", location: "Madrid, Spain", flag: "๐Ÿ‡ช๐Ÿ‡ธ" }, + BCN: { code: "BCN", location: "Barcelona, Spain", flag: "๐Ÿ‡ช๐Ÿ‡ธ" }, + GOT: { code: "GOT", location: "Gothenburg, Sweden", flag: "๐Ÿ‡ธ๐Ÿ‡ช" }, + ARN: { code: "ARN", location: "Stockholm, Sweden", flag: "๐Ÿ‡ธ๐Ÿ‡ช" }, + BSL: { code: "BSL", location: "Basel, Switzerland", flag: "๐Ÿ‡จ๐Ÿ‡ญ" }, + GVA: { code: "GVA", location: "Geneva, Switzerland", flag: "๐Ÿ‡จ๐Ÿ‡ญ" }, + ZRH: { code: "ZRH", location: "Zรผrich, Switzerland", flag: "๐Ÿ‡จ๐Ÿ‡ญ" }, + DAR: { code: "DAR", location: "Dar es Salaam, Tanzania", flag: "๐Ÿ‡น๐Ÿ‡ฟ" }, + BKK: { code: "BKK", location: "Bangkok, Thailand", flag: "๐Ÿ‡น๐Ÿ‡ญ" }, + IST: { code: "IST", location: "Istanbul, Turkey", flag: "๐Ÿ‡น๐Ÿ‡ท" }, + TUN: { code: "TUN", location: "Tunis, Tunisia", flag: "๐Ÿ‡น๐Ÿ‡ณ" }, + KBP: { code: "KBP", location: "Kyiv, Ukraine", flag: "๐Ÿ‡บ๐Ÿ‡ฆ" }, + DXB: { code: "DXB", location: "Dubai, United Arab Emirates", flag: "๐Ÿ‡ฆ๐Ÿ‡ช" }, + EDI: { code: "EDI", location: "Edinburgh, United Kingdom", flag: "๐Ÿ‡ฌ๐Ÿ‡ง" }, + LHR: { code: "LHR", location: "London, United Kingdom", flag: "๐Ÿ‡ฌ๐Ÿ‡ง" }, + MAN: { code: "MAN", location: "Manchester, United Kingdom", flag: "๐Ÿ‡ฌ๐Ÿ‡ง" }, + IAD: { code: "IAD", location: "Washington, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + ATL: { code: "ATL", location: "Atlanta, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + BOS: { code: "BOS", location: "Boston, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + BUF: { code: "BUF", location: "Buffalo, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + CLT: { code: "CLT", location: "Charlotte, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + ORD: { code: "ORD", location: "Chicago, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + CMH: { code: "CMH", location: "Columbus, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + DFW: { code: "DFW", location: "Dallas, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + DEN: { code: "DEN", location: "Denver, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + DTW: { code: "DTW", location: "Detroit, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + HNL: { code: "HNL", location: "Honolulu, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + IAH: { code: "IAH", location: "Houston, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + JAX: { code: "JAX", location: "Jacksonville, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + MCI: { code: "MCI", location: "Kansas City, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + LAS: { code: "LAS", location: "Las Vegas, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + LAX: { code: "LAX", location: "Los Angeles, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + MFE: { code: "MFE", location: "McAllen, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + MEM: { code: "MEM", location: "Memphis, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + MIA: { code: "MIA", location: "Miami, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + MSP: { code: "MSP", location: "Minneapolis, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + MGM: { code: "MGM", location: "Montgomery, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + BNA: { code: "BNA", location: "Nashville, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + EWR: { code: "EWR", location: "Newark, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + OMA: { code: "OMA", location: "Omaha, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + PHL: { code: "PHL", location: "Philadelphia, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + PHX: { code: "PHX", location: "Phoenix, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + PIT: { code: "PIT", location: "Pittsburgh, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + PDX: { code: "PDX", location: "Portland, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + RIC: { code: "RIC", location: "Richmond, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + SMF: { code: "SMF", location: "Sacramento, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + SLC: { code: "SLC", location: "Salt Lake City, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + SAN: { code: "SAN", location: "San Diego, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + SFO: { code: "SFO", location: "San Francisco, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + SJC: { code: "SJC", location: "San Jose, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + SEA: { code: "SEA", location: "Seattle, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + IND: { code: "IND", location: "Indianapolis, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + STL: { code: "STL", location: "St. Louis, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + TLH: { code: "TLH", location: "Tallahassee, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + TPA: { code: "TPA", location: "Tampa, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + TAS: { code: "TAS", location: "Tashkent, Uzbekistan", flag: "๐Ÿ‡บ๐Ÿ‡ฟ" }, + HAN: { code: "HAN", location: "Hanoi, Vietnam", flag: "๐Ÿ‡ป๐Ÿ‡ณ" }, + SGN: { code: "SGN", location: "Ho Chi Minh City, Vietnam", flag: "๐Ÿ‡ป๐Ÿ‡ณ" }, + HRE: { code: "HRE", location: "Harare, Zimbabwe", flag: "๐Ÿ‡ฟ๐Ÿ‡ผ" }, +}; diff --git a/packages/header-analysis/src/regions/fly.ts b/packages/header-analysis/src/regions/fly.ts new file mode 100644 index 00000000..5adc4fc6 --- /dev/null +++ b/packages/header-analysis/src/regions/fly.ts @@ -0,0 +1,40 @@ +import type { Region } from "../types"; + +// https://fly.io/docs/reference/regions/ +export const regions: Record = { + ams: { code: "ams", location: "Amsterdam, Netherlands", flag: "๐Ÿ‡ณ๐Ÿ‡ฑ" }, + arn: { code: "arn", location: "Stockholm, Sweden", flag: "๐Ÿ‡ธ๐Ÿ‡ช" }, + atl: { code: "atl", location: "Atlanta, Georgia (US)", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + bog: { code: "bog", location: "Bogotรก, Colombia", flag: "๐Ÿ‡จ๐Ÿ‡ด" }, + bom: { code: "bom", location: "Mumbai, India", flag: "๐Ÿ‡ฎ๐Ÿ‡ณ" }, + bos: { code: "bos", location: "Boston, Massachusetts (US)", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + cdg: { code: "cdg", location: "Paris, France", flag: "๐Ÿ‡ซ๐Ÿ‡ท" }, + den: { code: "den", location: "Denver, Colorado (US)", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + dfw: { code: "dfw", location: "Dallas, Texas (US)", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + ewr: { code: "ewr", location: "Secaucus, NJ (US)", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + eze: { code: "eze", location: "Ezeiza, Argentina", flag: "๐Ÿ‡ฆ๐Ÿ‡ท" }, + fra: { code: "fra", location: "Frankfurt, Germany", flag: "๐Ÿ‡ฉ๐Ÿ‡ช" }, + gdl: { code: "gdl", location: "Guadalajara, Mexico", flag: "๐Ÿ‡ฒ๐Ÿ‡ฝ" }, + gig: { code: "gig", location: "Rio de Janeiro, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + gru: { code: "gru", location: "Sao Paulo, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + hkg: { code: "hkg", location: "Hong Kong, Hong Kong", flag: "๐Ÿ‡ญ๐Ÿ‡ฐ" }, + iad: { code: "iad", location: "Ashburn, Virginia (US)", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + jnb: { code: "jnb", location: "Johannesburg, South Africa", flag: "๐Ÿ‡ฟ๐Ÿ‡ฆ" }, + lax: { code: "lax", location: "Los Angeles, California (US)", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + lhr: { code: "lhr", location: "London, United Kingdom", flag: "๐Ÿ‡ฌ๐Ÿ‡ง" }, + mad: { code: "mad", location: "Madrid, Spain", flag: "๐Ÿ‡ช๐Ÿ‡ธ" }, + mia: { code: "mia", location: "Miami, Florida (US)", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + nrt: { code: "nrt", location: "Tokyo, Japan", flag: "๐Ÿ‡ฏ๐Ÿ‡ต" }, + ord: { code: "ord", location: "Chicago, Illinois (US)", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + otp: { code: "otp", location: "Bucharest, Romania", flag: "๐Ÿ‡ท๐Ÿ‡ด" }, + phx: { code: "phx", location: "Phoenix, Arizona (US)", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + qro: { code: "qro", location: "Querรฉtaro, Mexico", flag: "๐Ÿ‡ฒ๐Ÿ‡ฝ" }, + scl: { code: "scl", location: "Santiago, Chile", flag: "๐Ÿ‡จ๐Ÿ‡ฑ" }, + sea: { code: "sea", location: "Seattle, Washington (US)", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + sin: { code: "sin", location: "Singapore, Singapore", flag: "๐Ÿ‡ธ๐Ÿ‡ฌ" }, + sjc: { code: "sjc", location: "San Jose, California (US)", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + syd: { code: "syd", location: "Sydney, Australia", flag: "๐Ÿ‡ฆ๐Ÿ‡บ" }, + waw: { code: "waw", location: "Warsaw, Poland", flag: "๐Ÿ‡ต๐Ÿ‡ฑ" }, + yul: { code: "yul", location: "Montreal, Canada", flag: "๐Ÿ‡จ๐Ÿ‡ฆ" }, + yyz: { code: "yyz", location: "Toronto, Canada", flag: "๐Ÿ‡จ๐Ÿ‡ฆ" }, +}; diff --git a/packages/header-analysis/src/regions/vercel.ts b/packages/header-analysis/src/regions/vercel.ts new file mode 100644 index 00000000..fd4fe859 --- /dev/null +++ b/packages/header-analysis/src/regions/vercel.ts @@ -0,0 +1,23 @@ +import type { Region } from "../types"; + +// https://vercel.com/docs/edge-network/regions +export const regions: Record = { + arn1: { code: "arn1", location: "Stockholm, Sweden", flag: "๐Ÿ‡ธ๐Ÿ‡ช" }, + bom1: { code: "bom1", location: "Mumbai, India", flag: "๐Ÿ‡ฎ๐Ÿ‡ณ" }, + cdg1: { code: "cdg1", location: "Paris, France", flag: "๐Ÿ‡ซ๐Ÿ‡ท" }, + cle1: { code: "cle1", location: "Cleveland, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + cpt1: { code: "cpt1", location: "Cape Town, South Africa", flag: "๐Ÿ‡ฟ๐Ÿ‡ฆ" }, + dub1: { code: "dub1", location: "Dublin, Ireland", flag: "๐Ÿ‡ฎ๐Ÿ‡ช" }, + fra1: { code: "fra1", location: "Frankfurt, Germany", flag: "๐Ÿ‡ฉ๐Ÿ‡ช" }, + gru1: { code: "gru1", location: "Sรฃo Paulo, Brazil", flag: "๐Ÿ‡ง๐Ÿ‡ท" }, + hkg1: { code: "hkg1", location: "Hong Kong", flag: "๐Ÿ‡ญ๐Ÿ‡ฐ" }, + hnd1: { code: "hnd1", location: "Tokyo, Japan", flag: "๐Ÿ‡ฏ๐Ÿ‡ต" }, + iad1: { code: "iad1", location: "Washington, D.C., USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + icn1: { code: "icn1", location: "Seoul, South Korea", flag: "๐Ÿ‡ฐ๐Ÿ‡ท" }, + kix1: { code: "kix1", location: "Osaka, Japan", flag: "๐Ÿ‡ฏ๐Ÿ‡ต" }, + lhr1: { code: "lhr1", location: "London, United Kingdom", flag: "๐Ÿ‡ฌ๐Ÿ‡ง" }, + pdx1: { code: "pdx1", location: "Portland, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + sfo1: { code: "sfo1", location: "San Francisco, USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ" }, + sin1: { code: "sin1", location: "Singapore", flag: "๐Ÿ‡ธ๐Ÿ‡ฌ" }, + syd1: { code: "syd1", location: "Sydney, Australia", flag: "๐Ÿ‡ฆ๐Ÿ‡บ" }, +}; diff --git a/packages/header-analysis/src/types/index.ts b/packages/header-analysis/src/types/index.ts new file mode 100644 index 00000000..e1e37976 --- /dev/null +++ b/packages/header-analysis/src/types/index.ts @@ -0,0 +1,9 @@ +export type Region = { + code: string; + location: string; + flag: string; +}; + +export type ParserReturn = + | { status: "success"; data: T } + | { status: "failed"; error: Error }; diff --git a/packages/header-analysis/tsconfig.json b/packages/header-analysis/tsconfig.json new file mode 100644 index 00000000..48baa5cf --- /dev/null +++ b/packages/header-analysis/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@openstatus/tsconfig/base.json", + "include": ["src", "*.ts"], + "compilerOptions": { + "resolveJsonModule": true + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3680dc3c..9d07abeb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -260,6 +260,9 @@ importers: '@openstatus/emails': specifier: workspace:* version: link:../../packages/emails + '@openstatus/header-analysis': + specifier: workspace:* + version: link:../../packages/header-analysis '@openstatus/next-monitoring': specifier: 0.0.2 version: 0.0.2(next@14.2.3(@babel/core@7.24.4)(@opentelemetry/api@1.4.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0) @@ -735,6 +738,15 @@ importers: specifier: 5.4.5 version: 5.4.5 + packages/header-analysis: + devDependencies: + '@openstatus/tsconfig': + specifier: workspace:* + version: link:../tsconfig + typescript: + specifier: 5.4.5 + version: 5.4.5 + packages/notifications/discord: dependencies: '@openstatus/db':