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 (
-
+
@@ -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) {
- Get Started
+ Get Started
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':