Openstatus www.openstatus.dev

feat: response header analysis (#809)

* feat: response header analysis

* chore: improve structure and add fly request id

* chore: remove deps

* fix: vercel return type

authored by

Maximilian Kaske and committed by
GitHub
2d9420c5 e81698d2

+963 -80
+1
apps/web/package.json
··· 21 21 "@openstatus/assertions": "workspace:*", 22 22 "@openstatus/db": "workspace:*", 23 23 "@openstatus/emails": "workspace:*", 24 + "@openstatus/header-analysis": "workspace:*", 24 25 "@openstatus/next-monitoring": "0.0.2", 25 26 "@openstatus/notification-discord": "workspace:*", 26 27 "@openstatus/notification-emails": "workspace:*",
-55
apps/web/src/app/app/(auth)/layout.tsx
··· 1 - import * as React from "react"; 2 - import Image from "next/image"; 3 - import Link from "next/link"; 4 - import { redirect } from "next/navigation"; 5 - 6 - import { auth } from "@/lib/auth"; 7 - 8 - export default async function AuthLayout({ 9 - children, 10 - }: { 11 - children: React.ReactNode; 12 - }) { 13 - const session = await auth(); 14 - if (session) redirect("/app"); 15 - 16 - return ( 17 - <div className="grid min-h-screen grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-5"> 18 - <aside className="border-border col-span-1 flex w-full flex-col gap-4 border p-4 backdrop-blur-[2px] md:p-8 xl:col-span-2"> 19 - <Link href="/" className="relative"> 20 - <Image 21 - src="/icon.png" 22 - alt="OpenStatus" 23 - height={30} 24 - width={30} 25 - className="border-border rounded-full border" 26 - /> 27 - </Link> 28 - <div className="flex w-full max-w-lg flex-1 flex-col justify-center text-left"> 29 - <h1 className="font-cal text-foreground mb-3 text-2xl"> 30 - Open Source Monitoring Service 31 - </h1> 32 - <p className="text-muted-foreground"> 33 - Monitor your website or API and create your own status page within a 34 - couple of minutes. Want to know how it works? <br /> 35 - <br /> 36 - Check out{" "} 37 - <a 38 - href="https://github.com/openstatushq/openstatus" 39 - target="_blank" 40 - rel="noreferrer" 41 - className="text-foreground underline underline-offset-4 hover:no-underline" 42 - > 43 - GitHub 44 - </a>{" "} 45 - and let us know your use case! 46 - </p> 47 - </div> 48 - <div className="md:h-[30px]" /> 49 - </aside> 50 - <main className="container col-span-1 mx-auto flex items-center justify-center md:col-span-1 xl:col-span-3"> 51 - {children} 52 - </main> 53 - </div> 54 - ); 55 - }
+57
apps/web/src/app/app/(auth)/login/layout.tsx
··· 1 + import * as React from "react"; 2 + import Image from "next/image"; 3 + import Link from "next/link"; 4 + import { redirect } from "next/navigation"; 5 + 6 + import { auth } from "@/lib/auth"; 7 + 8 + export default async function AuthLayout({ 9 + children, 10 + }: { 11 + children: React.ReactNode; 12 + }) { 13 + const session = await auth(); 14 + if (session) redirect("/app"); 15 + 16 + return ( 17 + <div className="grid min-h-screen grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-5"> 18 + <aside className="border-border col-span-1 flex w-full flex-col gap-4 border p-4 backdrop-blur-[2px] md:p-8 xl:col-span-2"> 19 + <Link href="/" className="relative h-8 w-8"> 20 + <Image 21 + src="/icon.png" 22 + alt="OpenStatus" 23 + height={32} 24 + width={32} 25 + className="border-border rounded-full border" 26 + /> 27 + </Link> 28 + <div className="flex w-full max-w-lg flex-1 flex-col justify-center gap-8 text-left"> 29 + <div className="mx-auto grid gap-3"> 30 + <h1 className="font-cal text-foreground text-2xl"> 31 + Open Source Monitoring Service 32 + </h1> 33 + <p className="text-muted-foreground"> 34 + Monitor your website or API and create your own status page within 35 + a couple of minutes. Want to know how it works? <br /> 36 + <br /> 37 + Check out{" "} 38 + <a 39 + href="https://github.com/openstatushq/openstatus" 40 + target="_blank" 41 + rel="noreferrer" 42 + className="text-foreground underline underline-offset-4 hover:no-underline" 43 + > 44 + GitHub 45 + </a>{" "} 46 + and let us know your use case! 47 + </p> 48 + </div> 49 + </div> 50 + <div className="md:h-8" /> 51 + </aside> 52 + <main className="container col-span-1 mx-auto flex items-center justify-center md:col-span-1 xl:col-span-3"> 53 + {children} 54 + </main> 55 + </div> 56 + ); 57 + }
+1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/data/_components/data-table-wrapper.tsx
··· 101 101 <ResponseDetailTabs 102 102 timing={first.timing} 103 103 headers={first.headers} 104 + status={first.statusCode} 104 105 message={first.message} 105 106 assertions={assertions.deserialize(first.assertions || "[]")} 106 107 />
+3 -3
apps/web/src/app/play/checker/[id]/page.tsx
··· 47 47 const check = 48 48 data.checks.find((i) => i.region === selectedRegion) || data.checks?.[0]; 49 49 50 - const { region, headers, timing } = check; 50 + const { region, headers, timing, status } = check; 51 51 52 52 return ( 53 53 <> ··· 77 77 <RegionInfo check={check} /> 78 78 </div> 79 79 </div> 80 - <ResponseDetailTabs timing={timing} headers={headers} /> 80 + <ResponseDetailTabs {...{ timing, headers, status }} /> 81 81 </div> 82 82 <Separator /> 83 83 <p className="text-muted-foreground text-sm"> ··· 85 85 <span className="text-foreground">1 day</span>. If you want to persist 86 86 the data,{" "} 87 87 <Link 88 - href="/app/sign-in" 88 + href="/app/login" 89 89 className="text-foreground underline underline-offset-4 hover:no-underline" 90 90 > 91 91 login
+1 -3
apps/web/src/app/sitemap.ts
··· 23 23 "/about", 24 24 "/blog", 25 25 "/changelog", 26 - "/app/sign-in", 27 - "/app/sign-up", 28 - "/monitor/openstatus", 26 + "/app/login", 29 27 ].map((route) => ({ 30 28 url: addPathToBaseURL(route), 31 29 lastModified: new Date(),
+1 -1
apps/web/src/app/status-page/[domain]/_components/header.tsx
··· 43 43 </div> 44 44 ) : null} 45 45 </div> 46 - <TabsContainer className="-mb-[12px] hidden sm:block"> 46 + <TabsContainer className="-mb-[14px] hidden sm:block"> 47 47 {navigation.map(({ label, href, disabled, segment }) => { 48 48 const active = segment === selectedSegment; 49 49 return (
+1
apps/web/src/components/forms/monitor/request-test-button.tsx
··· 133 133 <ResponseDetailTabs 134 134 timing={check.data.timing} 135 135 headers={check.data.headers} 136 + status={check.data.status} 136 137 assertions={deserialize( 137 138 JSON.stringify([ 138 139 ...(statusAssertions || []),
+1 -1
apps/web/src/components/marketing/hero.tsx
··· 41 41 <div className="my-4 grid gap-2 sm:grid-cols-2"> 42 42 <div className="text-center sm:block sm:text-right"> 43 43 <Button className="w-48 rounded-full sm:w-auto" asChild> 44 - <Link href="/app/sign-up">Get Started</Link> 44 + <Link href="/app/login">Get Started</Link> 45 45 </Button> 46 46 </div> 47 47 <div className="text-center sm:block sm:text-left">
+1 -1
apps/web/src/components/marketing/in-between-cta.tsx
··· 50 50 <InBetweenCTA 51 51 description="Learn over time how your services are performing, and inform your users when there are issues." 52 52 actions={{ 53 - primary: { label: "Start for Free", href: "/app/sign-up" }, 53 + primary: { label: "Start for Free", href: "/app/login" }, 54 54 secondary: { label: "Schedule a Demo", href: "/cal", target: "_blank" }, 55 55 }} 56 56 />
+3 -1
apps/web/src/components/marketing/pricing/pricing-table.tsx
··· 80 80 if (events?.[key]) { 81 81 return events[key]?.(); 82 82 } 83 - return router.push(`/app/sign-up?plan=${key}`); 83 + // FIXME: how to properly handle `?redirectTo` with unknown workspaceSlug 84 + // to redirect user to `/app/[workspaceSlug]/settings/billing?plan=${key}`? 85 + return router.push(`/app/login?plan=${key}`); 84 86 }} 85 87 disabled={isCurrentPlan || isLoading} 86 88 >
+4 -2
apps/web/src/components/monitor-dashboard/response-details.tsx
··· 23 23 24 24 const response = details[0]; 25 25 26 - const { timing, headers, message } = response; 26 + const { timing, headers, message, statusCode } = response; 27 27 28 28 const defaultValue = headers ? "headers" : timing ? "timing" : "message"; 29 29 ··· 50 50 </TabsTrigger> 51 51 </TabsList> 52 52 <TabsContent value="headers"> 53 - {headers ? <ResponseHeaderTable headers={headers} /> : null} 53 + {headers ? ( 54 + <ResponseHeaderTable headers={headers} status={statusCode || 0} /> 55 + ) : null} 54 56 </TabsContent> 55 57 <TabsContent value="timing"> 56 58 {timing ? <ResponseTimingTable timing={timing} hideInfo /> : null}
+6 -1
apps/web/src/components/ping-response-analysis/response-detail-tabs.tsx
··· 14 14 export async function ResponseDetailTabs({ 15 15 timing, 16 16 headers, 17 + status, 17 18 message, 18 19 assertions, 19 20 }: { 20 21 timing: Timing | null; 21 22 headers: Record<string, string> | null; 23 + status: number | null; 22 24 message?: string | null; 23 25 assertions?: Assertion[] | null; 24 26 }) { ··· 43 45 </TabsTrigger> 44 46 </TabsList> 45 47 <TabsContent value="headers"> 46 - {headers ? <ResponseHeaderTable headers={headers} /> : null} 48 + {headers ? ( 49 + <ResponseHeaderTable headers={headers} status={status || 0} /> 50 + ) : null} 47 51 </TabsContent> 48 52 <TabsContent value="timing"> 53 + {/* TODO: show hideInfo={false} when in /play/checker page */} 49 54 {timing ? <ResponseTimingTable timing={timing} hideInfo /> : null} 50 55 </TabsContent> 51 56 <TabsContent value="message">
+240
apps/web/src/components/ping-response-analysis/response-header-analysis.tsx
··· 1 + import "@openstatus/header-analysis"; 2 + 3 + import React from "react"; 4 + import { Info } from "lucide-react"; 5 + 6 + import { 7 + parseCacheControlHeader, 8 + parseCfCacheStatus, 9 + parseCfRay, 10 + parseFlyRequestId, 11 + parseXVercelCache, 12 + parseXVercelId, 13 + } from "@openstatus/header-analysis"; 14 + import { 15 + Dialog, 16 + DialogContent, 17 + DialogDescription, 18 + DialogHeader, 19 + DialogTitle, 20 + DialogTrigger, 21 + } from "@openstatus/ui"; 22 + 23 + const allowedHeaders = [ 24 + "Cache-Control", 25 + "Cf-Cache-Status", 26 + "Cf-Ray", 27 + "X-Vercel-Cache", 28 + "X-Vercel-Id", 29 + "Location", 30 + "Fly-Request-Id", 31 + ]; 32 + 33 + export function ResponseHeaderAnalysis({ 34 + headerKey, 35 + headers, 36 + status, 37 + }: { 38 + headers: Record<string, string | undefined>; 39 + headerKey: string; 40 + status: number; 41 + }) { 42 + const header = headers[headerKey]; 43 + 44 + if (!header) return null; 45 + if (!allowedHeaders.includes(headerKey)) return null; 46 + 47 + return ( 48 + <Dialog> 49 + <DialogTrigger className="text-muted-foreground hover:text-foreground data-[state=open]:text-foreground"> 50 + <Info className="h-4 w-4" /> 51 + </DialogTrigger> 52 + <DialogContent className="sm:max-w-2xl"> 53 + <DialogHeader> 54 + <DialogTitle>Header Analysis</DialogTitle> 55 + <DialogDescription> 56 + Breaking down the{" "} 57 + <code className="font-semibold">&quot;{headerKey}&quot;</code>{" "} 58 + header. 59 + </DialogDescription> 60 + </DialogHeader> 61 + {(() => { 62 + switch (headerKey) { 63 + case "Cache-Control": 64 + return <CacheControl header={header} />; 65 + case "Cf-Cache-Status": 66 + return <CfCacheStatus header={header} />; 67 + case "Cf-Ray": 68 + return <CfRay header={header} />; 69 + case "X-Vercel-Cache": 70 + return <XVercelCache header={header} />; 71 + case "X-Vercel-Id": 72 + return <XVercelId header={header} />; 73 + case "Fly-Request-Id": 74 + return <FlyRequestId header={header} />; 75 + case "Location": 76 + return <Location header={header} status={status} />; 77 + default: 78 + return null; 79 + } 80 + })()} 81 + </DialogContent> 82 + </Dialog> 83 + ); 84 + } 85 + 86 + function CacheControl({ header }: { header: string }) { 87 + const values = parseCacheControlHeader(header); 88 + 89 + return ( 90 + <div className="grid gap-4 sm:grid-cols-4"> 91 + {values.map(({ name, value, description }) => { 92 + return ( 93 + <React.Fragment key={name}> 94 + <p className="sm:col-span-1"> 95 + <code className="bg-muted rounded p-1 font-semibold">{name}</code>{" "} 96 + {value !== undefined ? <code>({value})</code> : null} 97 + </p> 98 + <p className="sm:col-span-3">{description}</p> 99 + </React.Fragment> 100 + ); 101 + })} 102 + </div> 103 + ); 104 + } 105 + 106 + function CfCacheStatus({ header }: { header: string }) { 107 + const { value, description } = parseCfCacheStatus(header); 108 + 109 + return ( 110 + <div className="grid gap-4 sm:grid-cols-4"> 111 + <p className="sm:col-span-1"> 112 + <code className="bg-muted rounded p-1 font-semibold">{value}</code> 113 + </p> 114 + <p className="sm:col-span-3">{description}</p> 115 + </div> 116 + ); 117 + } 118 + 119 + function XVercelCache({ header }: { header: string }) { 120 + const { value, description } = parseXVercelCache(header); 121 + 122 + return ( 123 + <div className="grid gap-4 sm:grid-cols-4"> 124 + <p className="sm:col-span-1"> 125 + <code className="bg-muted rounded p-1 font-semibold">{value}</code> 126 + </p> 127 + <p className="sm:col-span-3">{description}</p> 128 + </div> 129 + ); 130 + } 131 + 132 + export function XVercelId({ header }: { header: string }) { 133 + const value = parseXVercelId(header); 134 + 135 + return ( 136 + <div className="grid gap-4"> 137 + <p className="text-muted-foreground col-span-full text-sm"> 138 + This header contains a list of Edge regions your request hit, as well as 139 + the region the function was executed in (for both Edge and Serverless): 140 + </p> 141 + <div className="grid grid-cols-4 gap-3"> 142 + {value.status === "failed" ? ( 143 + <p className="text-destructive">{value.error.message}</p> 144 + ) : ( 145 + value.data.map(({ code, location, flag }) => ( 146 + <React.Fragment key={code}> 147 + <p className="sm:col-span-1"> 148 + <code className="bg-muted rounded p-1 font-semibold"> 149 + {code} 150 + </code> 151 + </p> 152 + <p className="sm:col-span-3"> 153 + {location} {flag} 154 + </p> 155 + </React.Fragment> 156 + )) 157 + )} 158 + </div> 159 + </div> 160 + ); 161 + } 162 + 163 + export function CfRay({ header }: { header: string }) { 164 + const value = parseCfRay(header); 165 + 166 + return ( 167 + <div className="grid gap-4"> 168 + <p className="text-muted-foreground col-span-full text-sm"> 169 + This header is a hashed value that encodes information about the data 170 + center and the visitor’s request. The data center the request hit is: 171 + </p> 172 + {value.status === "failed" ? ( 173 + <p className="text-destructive">{value.error.message}</p> 174 + ) : ( 175 + <div className="grid grid-cols-4 gap-3"> 176 + <p className="sm:col-span-1"> 177 + <code className="bg-muted rounded p-1 font-semibold"> 178 + {value.data.code} 179 + </code> 180 + </p> 181 + <p className="sm:col-span-3"> 182 + {value.data.location} {value.data.flag} 183 + </p> 184 + </div> 185 + )} 186 + </div> 187 + ); 188 + } 189 + 190 + export function Location({ 191 + header, 192 + status, 193 + }: { 194 + header: string; 195 + status: number; 196 + }) { 197 + return ( 198 + <div className="grid gap-4"> 199 + <p className="text-muted-foreground col-span-full text-sm"> 200 + This header in HTTP responses is used to redirect the client to a new 201 + URL. It is often seen with status codes like <code>301</code> (Moved 202 + Permanently) and <code>302</code> (Found), guiding the client&apos;s 203 + browser to navigate to a different location. 204 + </p> 205 + <div className="grid gap-4 sm:grid-cols-4"> 206 + <p className="sm:col-span-1"> 207 + <code className="bg-muted rounded p-1 font-semibold">{status}</code> 208 + </p> 209 + <p className="sm:col-span-3">{header}</p> 210 + </div> 211 + </div> 212 + ); 213 + } 214 + 215 + export function FlyRequestId({ header }: { header: string }) { 216 + const value = parseFlyRequestId(header); 217 + 218 + return ( 219 + <div className="grid gap-4"> 220 + <p className="text-muted-foreground col-span-full text-sm"> 221 + This header is a hashed value that encodes information about the data 222 + center and the visitor’s request. The data center the request hit is: 223 + </p> 224 + {value.status === "failed" ? ( 225 + <p className="text-destructive">{value.error.message}</p> 226 + ) : ( 227 + <div className="grid grid-cols-4 gap-3"> 228 + <p className="sm:col-span-1"> 229 + <code className="bg-muted rounded p-1 font-semibold"> 230 + {value.data.code} 231 + </code> 232 + </p> 233 + <p className="sm:col-span-3"> 234 + {value.data.location} {value.data.flag} 235 + </p> 236 + </div> 237 + )} 238 + </div> 239 + ); 240 + }
+12 -2
apps/web/src/components/ping-response-analysis/response-header-table.tsx
··· 9 9 } from "@openstatus/ui"; 10 10 11 11 import { CopyToClipboardButton } from "./copy-to-clipboard-button"; 12 + import { ResponseHeaderAnalysis } from "./response-header-analysis"; 12 13 13 14 export function ResponseHeaderTable({ 14 15 headers, 16 + status, 15 17 }: { 16 18 headers: Record<string, string>; 19 + status: number; 17 20 }) { 18 21 return ( 19 22 <Table> ··· 28 31 {Object.entries(headers).map(([key, value]) => ( 29 32 <TableRow key={key}> 30 33 <TableCell className="group"> 31 - <div className="min-[130px] flex items-center justify-between gap-1"> 32 - <code className="break-all font-medium">{key}</code> 34 + <div className="flex min-w-[130px] items-center justify-between gap-2"> 35 + <div className="flex items-center justify-between gap-2"> 36 + <code className="break-all font-medium">{key}</code> 37 + <ResponseHeaderAnalysis 38 + headerKey={key} 39 + headers={headers} 40 + status={status} 41 + /> 42 + </div> 33 43 <CopyToClipboardButton 34 44 copyValue={key} 35 45 className="invisible group-hover:visible"
+18 -9
apps/web/src/components/ping-response-analysis/response-timing-table.tsx
··· 15 15 16 16 import { timingDict } from "./config"; 17 17 import type { Timing } from "./utils"; 18 - import { getTimingPhases, getTotalLatency } from "./utils"; 18 + import { getTimingPhases, getTimingPhasesWidth } from "./utils"; 19 19 20 20 export function ResponseTimingTable({ 21 21 timing, ··· 24 24 timing: Timing; 25 25 hideInfo?: boolean; 26 26 }) { 27 - const total = getTotalLatency(timing); 28 27 const timingPhases = getTimingPhases(timing); 28 + const timingPhasesWidth = getTimingPhasesWidth(timing); 29 + 29 30 return ( 30 31 <Table> 31 32 <TableCaption className="mt-2">Response Timing</TableCaption> 32 33 <TableHeader> 33 34 <TableRow> 34 - <TableHead className="w-[120px] md:w-[150px]">Timing</TableHead> 35 - <TableHead className="w-[120px] md:w-[150px]">Duration</TableHead> 35 + <TableHead className="w-[72px] md:w-[150px]">Timing</TableHead> 36 + <TableHead className="w-[100px] md:w-[150px]">Duration</TableHead> 36 37 <TableHead /> 37 38 </TableRow> 38 39 </TableHeader> 39 40 <TableBody> 40 41 {Object.entries(timingPhases).map(([key, value]) => { 41 - const { short, long, description } = 42 - timingDict[key as keyof typeof timingPhases]; 42 + const phase = key as keyof typeof timingPhases; 43 + const { short, long, description } = timingDict[phase]; 44 + const { preWidth, width } = timingPhasesWidth[phase]; 45 + 43 46 return ( 44 47 <TableRow key={key}> 45 48 <TableCell> 46 - <div className="flex w-[80px] items-center justify-between gap-2"> 49 + <div className="flex w-[72px] items-center justify-between gap-2"> 47 50 <p className="text-muted-foreground">{short}</p> 48 51 {!hideInfo ? ( 49 52 <Popover> ··· 63 66 <TableCell> 64 67 <code>{value}ms</code> 65 68 </TableCell> 66 - <TableCell> 69 + <TableCell className="flex w-full"> 70 + <div 71 + style={{ 72 + width: `${preWidth}%`, 73 + minWidth: "1px", 74 + }} 75 + /> 67 76 <div 68 77 className="bg-foreground h-3 rounded-md" 69 78 style={{ 70 - width: `${(value / total) * 100}%`, 79 + width: `${width}%`, 71 80 minWidth: "1px", 72 81 }} 73 82 />
+36 -1
apps/web/src/components/ping-response-analysis/utils.ts
··· 10 10 } 11 11 12 12 export function timestampFormatter(timestamp: number) { 13 - return new Date(timestamp).toLocaleString(); // TODO: properly format the date 13 + return new Date(timestamp).toUTCString(); // GMT format 14 14 } 15 15 16 16 export function regionFormatter( ··· 33 33 const tls = timing.tlsHandshakeDone - timing.tlsHandshakeStart; 34 34 const ttfb = timing.firstByteDone - timing.firstByteStart; 35 35 const transfer = timing.transferDone - timing.transferStart; 36 + 37 + return { 38 + dns, 39 + connection, 40 + tls, 41 + ttfb, 42 + transfer, 43 + }; 44 + } 45 + 46 + export function getTimingPhasesWidth(timing: Timing) { 47 + const total = getTotalLatency(timing); 48 + const phases = getTimingPhases(timing); 49 + 50 + const dns = { preWidth: 0, width: (phases.dns / total) * 100 }; 51 + 52 + const connection = { 53 + preWidth: dns.preWidth + dns.width, 54 + width: (phases.connection / total) * 100, 55 + }; 56 + 57 + const tls = { 58 + preWidth: connection.preWidth + connection.width, 59 + width: (phases.tls / total) * 100, 60 + }; 61 + 62 + const ttfb = { 63 + preWidth: tls.preWidth + tls.width, 64 + width: (phases.ttfb / total) * 100, 65 + }; 66 + 67 + const transfer = { 68 + preWidth: ttfb.preWidth + ttfb.width, 69 + width: (phases.transfer / total) * 100, 70 + }; 36 71 37 72 return { 38 73 dns,
+15
packages/header-analysis/package.json
··· 1 + { 2 + "name": "@openstatus/header-analysis", 3 + "version": "1.0.0", 4 + "description": "", 5 + "main": "src/index.ts", 6 + "scripts": {}, 7 + "dependencies": {}, 8 + "devDependencies": { 9 + "@openstatus/tsconfig": "workspace:*", 10 + "typescript": "5.4.5" 11 + }, 12 + "keywords": [], 13 + "author": "", 14 + "license": "ISC" 15 + }
+6
packages/header-analysis/src/index.ts
··· 1 + export * from "./parser/cache-control"; 2 + export * from "./parser/cf-cache-status"; 3 + export * from "./parser/cf-ray"; 4 + export * from "./parser/fly-request-id"; 5 + export * from "./parser/x-vercel-cache"; 6 + export * from "./parser/x-vercel-id";
+63
packages/header-analysis/src/parser/cache-control.ts
··· 1 + interface CacheControlInfo { 2 + directive: string; // unparsed directive 3 + description: string; 4 + name: string; 5 + value?: number; 6 + } 7 + 8 + export function parseCacheControlHeader(header: string): CacheControlInfo[] { 9 + const cacheControlDirectives = header 10 + .split(",") 11 + .map((directive) => directive.trim()); 12 + 13 + const cacheControlInfo: CacheControlInfo[] = []; 14 + 15 + cacheControlDirectives.forEach((directive) => { 16 + const parts = directive.split("="); 17 + 18 + const name = parts[0].trim(); 19 + const value = !isNaN(Number(parts[1])) ? Number(parts[1]) : undefined; 20 + const description = getDirectiveDescription(name); 21 + 22 + cacheControlInfo.push({ description, name, value, directive }); 23 + }); 24 + 25 + return cacheControlInfo; 26 + } 27 + 28 + function getDirectiveDescription(key: string): string { 29 + switch (key.toLowerCase()) { 30 + case "max-age": 31 + 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."; 32 + case "max-stale": 33 + 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."; 34 + case "min-fresh": 35 + 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."; 36 + case "s-maxage": 37 + return "Similar to max-age, but applies specifically to shared caches, such as proxy servers. Overrides max-age when present in shared caches."; 38 + case "no-cache": 39 + 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."; 40 + case "no-store": 41 + 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."; 42 + case "no-transform": 43 + return "Specifies that intermediaries should not modify the content of the resource, such as by transcoding or compressing it."; 44 + case "only-if-cached": 45 + 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."; 46 + case "must-revalidate": 47 + return "Specifies that caches must revalidate stale responses with the origin server before using them."; 48 + case "proxy-revalidate": 49 + 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."; 50 + case "private": 51 + return "Indicates that the response is intended for a single user and should not be cached by shared caches."; 52 + case "public": 53 + return "Specifies that the response may be cached by any cache, including both private and shared caches."; 54 + case "immutable": 55 + return "Indicates that the response body will not change over time. Caches can store immutable responses indefinitely."; 56 + case "stale-while-revalidate": 57 + return "Allows a cache to serve stale responses while asynchronously revalidating them with the origin server in the background."; 58 + case "stale-if-error": 59 + return "Allows a cache to serve stale responses if the origin server is unavailable or returns an error."; 60 + default: 61 + return ""; 62 + } 63 + }
+26
packages/header-analysis/src/parser/cf-cache-status.ts
··· 1 + interface CfCacheStatusInfo { 2 + description: string; 3 + value: string; 4 + } 5 + 6 + export function parseCfCacheStatus(header: string): CfCacheStatusInfo { 7 + const description = getCacheDescription(header); 8 + return { description, value: header }; 9 + } 10 + 11 + function getCacheDescription(key: string): string { 12 + switch (key.toUpperCase()) { 13 + case "HIT": 14 + 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."; 15 + case "MISS": 16 + 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."; 17 + case "BYPASS": 18 + 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."; 19 + case "EXPIRED": 20 + 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."; 21 + case "DYNAMIC": 22 + 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."; 23 + default: 24 + return ""; 25 + } 26 + }
+22
packages/header-analysis/src/parser/cf-ray.ts
··· 1 + // List of all Cloudflare data center taken by https://www.feitsui.com/en/article/26 2 + import { regions } from "../regions/cloudflare"; 3 + import type { ParserReturn, Region } from "../types"; 4 + 5 + export function parseCfRay(header: string): ParserReturn<Region> { 6 + const regex = /\b([A-Z]{3})\b/g; 7 + const arr = header.match(regex); 8 + 9 + if (!arr || !Array.isArray(arr) || arr.length === 0) { 10 + return { status: "failed", error: new Error("Couldn't parse the header.") }; 11 + } 12 + 13 + const region = regions[arr[0]]; 14 + if (region) return { status: "success", data: region }; 15 + 16 + return { 17 + status: "failed", 18 + error: new Error( 19 + `It seems like the data center '${arr[0]}' (iata) is not listed.`, 20 + ), 21 + }; 22 + }
+21
packages/header-analysis/src/parser/fly-request-id.ts
··· 1 + import { regions } from "../regions/fly"; 2 + import type { ParserReturn, Region } from "../types"; 3 + 4 + export function parseFlyRequestId(header: string): ParserReturn<Region> { 5 + const regex = /\b([a-z]{3})\b/g; 6 + const arr = header.match(regex); 7 + 8 + if (!arr || arr.length === 0) { 9 + return { status: "failed", error: new Error("Couldn't parse the header.") }; 10 + } 11 + 12 + const region = regions[arr[0]]; 13 + if (region) return { status: "success", data: region }; 14 + 15 + return { 16 + status: "failed", 17 + error: new Error( 18 + `It seems like the data center '${arr[0]}' (iata) is not listed.`, 19 + ), 20 + }; 21 + }
+26
packages/header-analysis/src/parser/x-vercel-cache.ts
··· 1 + interface VercelCacheInfo { 2 + description: string; 3 + value: string; 4 + } 5 + 6 + export function parseXVercelCache(header: string): VercelCacheInfo { 7 + const description = getCacheDescription(header); 8 + return { description, value: header }; 9 + } 10 + 11 + function getCacheDescription(key: string): string { 12 + switch (key.toUpperCase()) { 13 + case "MISS": 14 + return "The response was not found in the edge cache and was fetched from the origin server."; 15 + case "HIT": 16 + return "The response was served from the edge cache."; 17 + case "STALE": 18 + return "The response was served from the edge cache. A background request to the origin server was made to update the content."; 19 + case "PRERENDER": 20 + return "The response was served from static storage."; 21 + case "REVLIDATED": 22 + 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."; 23 + default: 24 + return ""; 25 + } 26 + }
+18
packages/header-analysis/src/parser/x-vercel-id.ts
··· 1 + import { regions } from "../regions/vercel"; 2 + import type { ParserReturn, Region } from "../types"; 3 + 4 + export function parseXVercelId(header: string): ParserReturn<Region[]> { 5 + const regex = /([a-z]{3}[0-9])+:+/g; 6 + 7 + const arr = header.match(regex); 8 + if (!arr || !arr.length) { 9 + return { status: "failed", error: new Error("Couldn't parse the header.") }; 10 + } 11 + 12 + const data = arr.map((r) => { 13 + const regionId = r.replace(/:+/, ""); 14 + return regions[regionId]; 15 + }); 16 + 17 + return { status: "success", data }; 18 + }
+289
packages/header-analysis/src/regions/cloudflare.ts
··· 1 + import type { Region } from "../types"; 2 + 3 + // REMINDER: nono-official data center list 4 + // https://www.feitsui.com/en/article/26 5 + export const regions: Record<string, Region> = { 6 + CGB: { code: "CGB", location: "Cuiabá, Brazil", flag: "🇧🇷" }, 7 + COR: { code: "COR", location: "Córdoba, Argentina", flag: "🇦🇷" }, 8 + BTS: { code: "BTS", location: "Bratislava, Slovakia", flag: "🇸🇰" }, 9 + KHH: { code: "KHH", location: "Kaohsiung, Taiwan", flag: "🇹🇼" }, 10 + GND: { code: "GND", location: "Grenada", flag: "🇬🇩" }, 11 + NVT: { code: "NVT", location: "Navegantes, Brazil", flag: "🇧🇷" }, 12 + ISU: { code: "ISU", location: "Sulaimaniyah, Iraq", flag: "🇮🇶" }, 13 + PAP: { code: "PAP", location: "Port-au-Prince, Haiti", flag: "🇭🇹" }, 14 + KLD: { code: "KLD", location: "Tver, Russia", flag: "🇷🇺" }, 15 + REC: { code: "REC", location: "Recife, Brazil", flag: "🇧🇷" }, 16 + FSD: { code: "FSD", location: "Sioux Falls, USA", flag: "🇺🇸" }, 17 + ADB: { code: "ADB", location: "Izmir, Turkey", flag: "🇹🇷" }, 18 + CHC: { code: "CHC", location: "Christchurch, New Zealand", flag: "🇳🇿" }, 19 + UDI: { code: "UDI", location: "Uberlândia, Brazil", flag: "🇧🇷" }, 20 + PPT: { code: "PPT", location: "Papeete, French Polynesia", flag: "🇵🇫" }, 21 + KIN: { code: "KIN", location: "Kingston, Jamaica", flag: "🇯🇲" }, 22 + STR: { code: "STR", location: "Stuttgart, Germany", flag: "🇩🇪" }, 23 + COK: { code: "COK", location: "Kochi, India", flag: "🇮🇳" }, 24 + KJA: { code: "KJA", location: "Krasnoyarsk, Russia", flag: "🇷🇺" }, 25 + AUS: { code: "AUS", location: "Austin, USA", flag: "🇺🇸" }, 26 + ORN: { code: "ORN", location: "Oran, Algeria", flag: "🇩🇿" }, 27 + ACC: { code: "ACC", location: "Accra, Ghana", flag: "🇬🇭" }, 28 + GDL: { code: "GDL", location: "Guadalajara, Mexico", flag: "🇲🇽" }, 29 + SJK: { code: "SJK", location: "São José dos Campos, Brazil", flag: "🇧🇷" }, 30 + AAE: { code: "AAE", location: "Annaba, Algeria", flag: "🇩🇿" }, 31 + MDL: { code: "MDL", location: "Mandalay, Myanmar", flag: "🇲🇲" }, 32 + DPS: { code: "DPS", location: "Denpasar, Indonesia", flag: "🇮🇩" }, 33 + VIX: { code: "VIX", location: "Vitória, Brazil", flag: "🇧🇷" }, 34 + FUK: { code: "FUK", location: "Fukuoka, Japan", flag: "🇯🇵" }, 35 + CNN: { code: "CNN", location: "Chengdu, China", flag: "🇨🇳" }, 36 + LYS: { code: "LYS", location: "Lyon, France", flag: "🇫🇷" }, 37 + XAP: { code: "XAP", location: "Chapecó, Brazil", flag: "🇧🇷" }, 38 + CAI: { code: "CAI", location: "Cairo, Egypt", flag: "🇪🇬" }, 39 + RDU: { code: "RDU", location: "Raleigh-Durham, USA", flag: "🇺🇸" }, 40 + CAW: { code: "CAW", location: "Campos dos Goytacazes, Brazil", flag: "🇧🇷" }, 41 + OKC: { code: "OKC", location: "Oklahoma City, USA", flag: "🇺🇸" }, 42 + SDQ: { 43 + code: "SDQ", 44 + location: "Santo Domingo, Dominican Republic", 45 + flag: "🇩🇴", 46 + }, 47 + OUA: { code: "OUA", location: "Ouagadougou, Burkina Faso", flag: "🇧🇫" }, 48 + YHZ: { code: "YHZ", location: "Halifax, Canada", flag: "🇨🇦" }, 49 + ANC: { code: "ANC", location: "Anchorage, USA", flag: "🇺🇸" }, 50 + LPB: { code: "LPB", location: "La Paz, Bolivia", flag: "🇧🇴" }, 51 + SUV: { code: "SUV", location: "Suva, Fiji", flag: "🇫🇯" }, 52 + BGR: { code: "BGR", location: "Bangor, USA", flag: "🇺🇸" }, 53 + SJU: { code: "SJU", location: "San Juan, Puerto Rico", flag: "🇵🇷" }, 54 + BBI: { code: "BBI", location: "Bhubaneswar, India", flag: "🇮🇳" }, 55 + ALG: { code: "ALG", location: "Algiers, Algeria", flag: "🇩🇿" }, 56 + LAD: { code: "LAD", location: "Luanda, Angola", flag: "🇦🇴" }, 57 + EZE: { code: "EZE", location: "Buenos Aires, Argentina", flag: "🇦🇷" }, 58 + NQN: { code: "NQN", location: "Neuquén, Argentina", flag: "🇦🇷" }, 59 + EVN: { code: "EVN", location: "Yerevan, Armenia", flag: "🇦🇲" }, 60 + ADL: { code: "ADL", location: "Adelaide, Australia", flag: "🇦🇺" }, 61 + BNE: { code: "BNE", location: "Brisbane, Australia", flag: "🇦🇺" }, 62 + CBR: { code: "CBR", location: "Canberra, Australia", flag: "🇦🇺" }, 63 + HBA: { code: "HBA", location: "Hobart, Australia", flag: "🇦🇺" }, 64 + MEL: { code: "MEL", location: "Melbourne, Australia", flag: "🇦🇺" }, 65 + PER: { code: "PER", location: "Perth, Australia", flag: "🇦🇺" }, 66 + SYD: { code: "SYD", location: "Sydney, Australia", flag: "🇦🇺" }, 67 + VIE: { code: "VIE", location: "Vienna, Austria", flag: "🇦🇹" }, 68 + LLK: { code: "LLK", location: "Luleå, Sweden", flag: "🇸🇪" }, 69 + GYD: { code: "GYD", location: "Baku, Azerbaijan", flag: "🇦🇿" }, 70 + BAH: { code: "BAH", location: "Manama, Bahrain", flag: "🇧🇭" }, 71 + CGP: { code: "CGP", location: "Chittagong, Bangladesh", flag: "🇧🇩" }, 72 + DAC: { code: "DAC", location: "Dhaka, Bangladesh", flag: "🇧🇩" }, 73 + JSR: { code: "JSR", location: "Jessore, Bangladesh", flag: "🇧🇩" }, 74 + MSQ: { code: "MSQ", location: "Minsk, Belarus", flag: "🇧🇾" }, 75 + BRU: { code: "BRU", location: "Brussels, Belgium", flag: "🇧🇪" }, 76 + PBH: { code: "PBH", location: "Paro, Bhutan", flag: "🇧🇹" }, 77 + GBE: { code: "GBE", location: "Gaborone, Botswana", flag: "🇧🇼" }, 78 + QWJ: { code: "QWJ", location: "Bouake, Ivory Coast", flag: "🇨🇮" }, 79 + CNF: { code: "CNF", location: "Belo Horizonte, Brazil", flag: "🇧🇷" }, 80 + BEL: { code: "BEL", location: "Belém, Brazil", flag: "🇧🇷" }, 81 + BNU: { code: "BNU", location: "Blumenau, Brazil", flag: "🇧🇷" }, 82 + BSB: { code: "BSB", location: "Brasília, Brazil", flag: "🇧🇷" }, 83 + VCP: { code: "VCP", location: "Campinas, Brazil", flag: "🇧🇷" }, 84 + CFC: { code: "CFC", location: "Cafelândia, Brazil", flag: "🇧🇷" }, 85 + CWB: { code: "CWB", location: "Curitiba, Brazil", flag: "🇧🇷" }, 86 + FLN: { code: "FLN", location: "Florianópolis, Brazil", flag: "🇧🇷" }, 87 + FOR: { code: "FOR", location: "Fortaleza, Brazil", flag: "🇧🇷" }, 88 + GYN: { code: "GYN", location: "Goiânia, Brazil", flag: "🇧🇷" }, 89 + ITJ: { code: "ITJ", location: "Itajaí, Brazil", flag: "🇧🇷" }, 90 + JOI: { code: "JOI", location: "Joinville, Brazil", flag: "🇧🇷" }, 91 + JDO: { code: "JDO", location: "Juazeiro do Norte, Brazil", flag: "🇧🇷" }, 92 + MAO: { code: "MAO", location: "Manaus, Brazil", flag: "🇧🇷" }, 93 + POA: { code: "POA", location: "Porto Alegre, Brazil", flag: "🇧🇷" }, 94 + RAO: { code: "RAO", location: "Ribeirão Preto, Brazil", flag: "🇧🇷" }, 95 + GIG: { code: "GIG", location: "Rio de Janeiro, Brazil", flag: "🇧🇷" }, 96 + SSA: { code: "SSA", location: "Salvador, Brazil", flag: "🇧🇷" }, 97 + SOD: { code: "SOD", location: "Sorocaba, Brazil", flag: "🇧🇷" }, 98 + SJP: { code: "SJP", location: "São José do Rio Preto, Brazil", flag: "🇧🇷" }, 99 + GRU: { code: "GRU", location: "São Paulo, Brazil", flag: "🇧🇷" }, 100 + BWN: { code: "BWN", location: "Bandar Seri Begawan, Brunei", flag: "🇧🇳" }, 101 + SOF: { code: "SOF", location: "Sofia, Bulgaria", flag: "🇧🇬" }, 102 + PNH: { code: "PNH", location: "Phnom Penh, Cambodia", flag: "🇰🇭" }, 103 + YYC: { code: "YYC", location: "Calgary, Canada", flag: "🇨🇦" }, 104 + YUL: { code: "YUL", location: "Montreal, Canada", flag: "🇨🇦" }, 105 + YOW: { code: "YOW", location: "Ottawa, Canada", flag: "🇨🇦" }, 106 + YXE: { code: "YXE", location: "Saskatoon, Canada", flag: "🇨🇦" }, 107 + YYZ: { code: "YYZ", location: "Toronto, Canada", flag: "🇨🇦" }, 108 + YVR: { code: "YVR", location: "Vancouver, Canada", flag: "🇨🇦" }, 109 + YWG: { code: "YWG", location: "Winnipeg, Canada", flag: "🇨🇦" }, 110 + ARI: { code: "ARI", location: "Arica, Chile", flag: "🇨🇱" }, 111 + SCL: { code: "SCL", location: "Santiago, Chile", flag: "🇨🇱" }, 112 + HKG: { code: "HKG", location: "Hong Kong, Hong Kong", flag: "🇭🇰" }, 113 + MFM: { code: "MFM", location: "Macau, Macau", flag: "🇲🇴" }, 114 + TPE: { code: "TPE", location: "Taipei, Taiwan", flag: "🇹🇼" }, 115 + BOG: { code: "BOG", location: "Bogotá, Colombia", flag: "🇨🇴" }, 116 + MDE: { code: "MDE", location: "Medellín, Colombia", flag: "🇨🇴" }, 117 + SJO: { code: "SJO", location: "San José, Costa Rica", flag: "🇨🇷" }, 118 + ZAG: { code: "ZAG", location: "Zagreb, Croatia", flag: "🇭🇷" }, 119 + LCA: { code: "LCA", location: "Larnaca, Cyprus", flag: "🇨🇾" }, 120 + PRG: { code: "PRG", location: "Prague, Czech Republic", flag: "🇨🇿" }, 121 + CPH: { code: "CPH", location: "Copenhagen, Denmark", flag: "🇩🇰" }, 122 + JIB: { code: "JIB", location: "Djibouti", flag: "🇩🇯" }, 123 + GYE: { code: "GYE", location: "Guayaquil, Ecuador", flag: "🇪🇨" }, 124 + UIO: { code: "UIO", location: "Quito, Ecuador", flag: "🇪🇨" }, 125 + TLL: { code: "TLL", location: "Tallinn, Estonia", flag: "🇪🇪" }, 126 + HEL: { code: "HEL", location: "Helsinki, Finland", flag: "🇫🇮" }, 127 + MRS: { code: "MRS", location: "Marseille, France", flag: "🇫🇷" }, 128 + CDG: { code: "CDG", location: "Paris, France", flag: "🇫🇷" }, 129 + RUN: { code: "RUN", location: "Saint-Denis, Réunion", flag: "🇷🇪" }, 130 + TBS: { code: "TBS", location: "Tbilisi, Georgia", flag: "🇬🇪" }, 131 + TXL: { code: "TXL", location: "Berlin, Germany", flag: "🇩🇪" }, 132 + DUS: { code: "DUS", location: "Düsseldorf, Germany", flag: "🇩🇪" }, 133 + FRA: { code: "FRA", location: "Frankfurt, Germany", flag: "🇩🇪" }, 134 + HAM: { code: "HAM", location: "Hamburg, Germany", flag: "🇩🇪" }, 135 + MUC: { code: "MUC", location: "Munich, Germany", flag: "🇩🇪" }, 136 + ATH: { code: "ATH", location: "Athens, Greece", flag: "🇬🇷" }, 137 + SKG: { code: "SKG", location: "Thessaloniki, Greece", flag: "🇬🇷" }, 138 + GUM: { code: "GUM", location: "Hagåtña, Guam", flag: "🇬🇺" }, 139 + GUA: { code: "GUA", location: "Guatemala City, Guatemala", flag: "🇬🇹" }, 140 + GEO: { code: "GEO", location: "Georgetown, Guyana", flag: "🇬🇾" }, 141 + TGU: { code: "TGU", location: "Tegucigalpa, Honduras", flag: "🇭🇳" }, 142 + BUD: { code: "BUD", location: "Budapest, Hungary", flag: "🇭🇺" }, 143 + KEF: { code: "KEF", location: "Reykjavík, Iceland", flag: "🇮🇸" }, 144 + AMD: { code: "AMD", location: "Ahmedabad, India", flag: "🇮🇳" }, 145 + BLR: { code: "BLR", location: "Bengaluru, India", flag: "🇮🇳" }, 146 + IXC: { code: "IXC", location: "Chandigarh, India", flag: "🇮🇳" }, 147 + MAA: { code: "MAA", location: "Chennai, India", flag: "🇮🇳" }, 148 + HYD: { code: "HYD", location: "Hyderabad, India", flag: "🇮🇳" }, 149 + KNU: { code: "KNU", location: "Kanpur, India", flag: "🇮🇳" }, 150 + CCU: { code: "CCU", location: "Kolkata, India", flag: "🇮🇳" }, 151 + BOM: { code: "BOM", location: "Mumbai, India", flag: "🇮🇳" }, 152 + NAG: { code: "NAG", location: "Nagpur, India", flag: "🇮🇳" }, 153 + DEL: { code: "DEL", location: "New Delhi, India", flag: "🇮🇳" }, 154 + PAT: { code: "PAT", location: "Patna, India", flag: "🇮🇳" }, 155 + CGK: { code: "CGK", location: "Jakarta, Indonesia", flag: "🇮🇩" }, 156 + JOG: { code: "JOG", location: "Yogyakarta, Indonesia", flag: "🇮🇩" }, 157 + BGW: { code: "BGW", location: "Baghdad, Iraq", flag: "🇮🇶" }, 158 + BSR: { code: "BSR", location: "Basra, Iraq", flag: "🇮🇶" }, 159 + EBL: { code: "EBL", location: "Erbil, Iraq", flag: "🇮🇶" }, 160 + NJF: { code: "NJF", location: "Najaf, Iraq", flag: "🇮🇶" }, 161 + XNH: { code: "XNH", location: "Nouadhibou, Mauritania", flag: "🇲🇷" }, 162 + ORK: { code: "ORK", location: "Cork, Ireland", flag: "🇮🇪" }, 163 + DUB: { code: "DUB", location: "Dublin, Ireland", flag: "🇮🇪" }, 164 + HFA: { code: "HFA", location: "Haifa, Israel", flag: "🇮🇱" }, 165 + TLV: { code: "TLV", location: "Tel-Aviv, Israel", flag: "🇮🇱" }, 166 + MXP: { code: "MXP", location: "Milan, Italy", flag: "🇮🇹" }, 167 + PMO: { code: "PMO", location: "Palermo, Italy", flag: "🇮🇹" }, 168 + FCO: { code: "FCO", location: "Rome, Italy", flag: "🇮🇹" }, 169 + OKA: { code: "OKA", location: "Okinawa, Japan", flag: "🇯🇵" }, 170 + KIX: { code: "KIX", location: "Osaka, Japan", flag: "🇯🇵" }, 171 + NRT: { code: "NRT", location: "Tokyo, Japan", flag: "🇯🇵" }, 172 + AMM: { code: "AMM", location: "Amman, Jordan", flag: "🇯🇴" }, 173 + ALA: { code: "ALA", location: "Almaty, Kazakhstan", flag: "🇰🇿" }, 174 + MBA: { code: "MBA", location: "Mombasa, Kenya", flag: "🇰🇪" }, 175 + NBO: { code: "NBO", location: "Nairobi, Kenya", flag: "🇰🇪" }, 176 + KWI: { code: "KWI", location: "Kuwait City, Kuwait", flag: "🇰🇼" }, 177 + VTE: { code: "VTE", location: "Vientiane, Laos", flag: "🇱🇦" }, 178 + RIX: { code: "RIX", location: "Riga, Latvia", flag: "🇱🇻" }, 179 + VNO: { code: "VNO", location: "Vilnius, Lithuania", flag: "🇱🇹" }, 180 + LUX: { code: "LUX", location: "Luxembourg", flag: "🇱🇺" }, 181 + TNR: { code: "TNR", location: "Antananarivo, Madagascar", flag: "🇲🇬" }, 182 + JHB: { code: "JHB", location: "Johor Bahru, Malaysia", flag: "🇲🇾" }, 183 + KUL: { code: "KUL", location: "Kuala Lumpur, Malaysia", flag: "🇲🇾" }, 184 + MLE: { code: "MLE", location: "Malé, Maldives", flag: "🇲🇻" }, 185 + MRU: { code: "MRU", location: "Port Louis, Mauritius", flag: "🇲🇺" }, 186 + MEX: { code: "MEX", location: "Mexico City, Mexico", flag: "🇲🇽" }, 187 + QRO: { code: "QRO", location: "Querétaro, Mexico", flag: "🇲🇽" }, 188 + KIV: { code: "KIV", location: "Chișinău, Moldova", flag: "🇲🇩" }, 189 + ULN: { code: "ULN", location: "Ulaanbaatar, Mongolia", flag: "🇲🇳" }, 190 + CMN: { code: "CMN", location: "Casablanca, Morocco", flag: "🇲🇦" }, 191 + MPM: { code: "MPM", location: "Maputo, Mozambique", flag: "🇲🇿" }, 192 + RGN: { code: "RGN", location: "Yangon, Myanmar", flag: "🇲🇲" }, 193 + KTM: { code: "KTM", location: "Kathmandu, Nepal", flag: "🇳🇵" }, 194 + AMS: { code: "AMS", location: "Amsterdam, Netherlands", flag: "🇳🇱" }, 195 + NOU: { code: "NOU", location: "Nouméa, New Caledonia", flag: "🇳🇨" }, 196 + AKL: { code: "AKL", location: "Auckland, New Zealand", flag: "🇳🇿" }, 197 + LOS: { code: "LOS", location: "Lagos, Nigeria", flag: "🇳🇬" }, 198 + OSL: { code: "OSL", location: "Oslo, Norway", flag: "🇳🇴" }, 199 + MCT: { code: "MCT", location: "Muscat, Oman", flag: "🇴🇲" }, 200 + ISB: { code: "ISB", location: "Islamabad, Pakistan", flag: "🇵🇰" }, 201 + KHI: { code: "KHI", location: "Karachi, Pakistan", flag: "🇵🇰" }, 202 + LHE: { code: "LHE", location: "Lahore, Pakistan", flag: "🇵🇰" }, 203 + ZDM: { code: "ZDM", location: "Zahedan, Iran", flag: "🇮🇷" }, 204 + PTY: { code: "PTY", location: "Panama City, Panama", flag: "🇵🇦" }, 205 + ASU: { code: "ASU", location: "Asunción, Paraguay", flag: "🇵🇾" }, 206 + LIM: { code: "LIM", location: "Lima, Peru", flag: "🇵🇪" }, 207 + CGY: { code: "CGY", location: "Cagayan de Oro, Philippines", flag: "🇵🇭" }, 208 + CEB: { code: "CEB", location: "Cebu, Philippines", flag: "🇵🇭" }, 209 + MNL: { code: "MNL", location: "Manila, Philippines", flag: "🇵🇭" }, 210 + WAW: { code: "WAW", location: "Warsaw, Poland", flag: "🇵🇱" }, 211 + LIS: { code: "LIS", location: "Lisbon, Portugal", flag: "🇵🇹" }, 212 + DOH: { code: "DOH", location: "Doha, Qatar", flag: "🇶🇦" }, 213 + OTP: { code: "OTP", location: "Bucharest, Romania", flag: "🇷🇴" }, 214 + LED: { code: "LED", location: "Saint Petersburg, Russia", flag: "🇷🇺" }, 215 + DME: { code: "DME", location: "Moscow, Russia", flag: "🇷🇺" }, 216 + KZN: { code: "KZN", location: "Kazan, Russia", flag: "🇷🇺" }, 217 + KRR: { code: "KRR", location: "Krasnodar, Russia", flag: "🇷🇺" }, 218 + SVX: { code: "SVX", location: "Yekaterinburg, Russia", flag: "🇷🇺" }, 219 + KGL: { code: "KGL", location: "Kigali, Rwanda", flag: "🇷🇼" }, 220 + JED: { code: "JED", location: "Jeddah, Saudi Arabia", flag: "🇸🇦" }, 221 + RUH: { code: "RUH", location: "Riyadh, Saudi Arabia", flag: "🇸🇦" }, 222 + DKR: { code: "DKR", location: "Dakar, Senegal", flag: "🇸🇳" }, 223 + BEG: { code: "BEG", location: "Belgrade, Serbia", flag: "🇷🇸" }, 224 + SGP: { code: "SGP", location: "Singapore", flag: "🇸🇬" }, 225 + LJU: { code: "LJU", location: "Ljubljana, Slovenia", flag: "🇸🇮" }, 226 + CPT: { code: "CPT", location: "Cape Town, South Africa", flag: "🇿🇦" }, 227 + DUR: { code: "DUR", location: "Durban, South Africa", flag: "🇿🇦" }, 228 + JNB: { code: "JNB", location: "Johannesburg, South Africa", flag: "🇿🇦" }, 229 + ICN: { code: "ICN", location: "Seoul, South Korea", flag: "🇰🇷" }, 230 + MAD: { code: "MAD", location: "Madrid, Spain", flag: "🇪🇸" }, 231 + BCN: { code: "BCN", location: "Barcelona, Spain", flag: "🇪🇸" }, 232 + GOT: { code: "GOT", location: "Gothenburg, Sweden", flag: "🇸🇪" }, 233 + ARN: { code: "ARN", location: "Stockholm, Sweden", flag: "🇸🇪" }, 234 + BSL: { code: "BSL", location: "Basel, Switzerland", flag: "🇨🇭" }, 235 + GVA: { code: "GVA", location: "Geneva, Switzerland", flag: "🇨🇭" }, 236 + ZRH: { code: "ZRH", location: "Zürich, Switzerland", flag: "🇨🇭" }, 237 + DAR: { code: "DAR", location: "Dar es Salaam, Tanzania", flag: "🇹🇿" }, 238 + BKK: { code: "BKK", location: "Bangkok, Thailand", flag: "🇹🇭" }, 239 + IST: { code: "IST", location: "Istanbul, Turkey", flag: "🇹🇷" }, 240 + TUN: { code: "TUN", location: "Tunis, Tunisia", flag: "🇹🇳" }, 241 + KBP: { code: "KBP", location: "Kyiv, Ukraine", flag: "🇺🇦" }, 242 + DXB: { code: "DXB", location: "Dubai, United Arab Emirates", flag: "🇦🇪" }, 243 + EDI: { code: "EDI", location: "Edinburgh, United Kingdom", flag: "🇬🇧" }, 244 + LHR: { code: "LHR", location: "London, United Kingdom", flag: "🇬🇧" }, 245 + MAN: { code: "MAN", location: "Manchester, United Kingdom", flag: "🇬🇧" }, 246 + IAD: { code: "IAD", location: "Washington, USA", flag: "🇺🇸" }, 247 + ATL: { code: "ATL", location: "Atlanta, USA", flag: "🇺🇸" }, 248 + BOS: { code: "BOS", location: "Boston, USA", flag: "🇺🇸" }, 249 + BUF: { code: "BUF", location: "Buffalo, USA", flag: "🇺🇸" }, 250 + CLT: { code: "CLT", location: "Charlotte, USA", flag: "🇺🇸" }, 251 + ORD: { code: "ORD", location: "Chicago, USA", flag: "🇺🇸" }, 252 + CMH: { code: "CMH", location: "Columbus, USA", flag: "🇺🇸" }, 253 + DFW: { code: "DFW", location: "Dallas, USA", flag: "🇺🇸" }, 254 + DEN: { code: "DEN", location: "Denver, USA", flag: "🇺🇸" }, 255 + DTW: { code: "DTW", location: "Detroit, USA", flag: "🇺🇸" }, 256 + HNL: { code: "HNL", location: "Honolulu, USA", flag: "🇺🇸" }, 257 + IAH: { code: "IAH", location: "Houston, USA", flag: "🇺🇸" }, 258 + JAX: { code: "JAX", location: "Jacksonville, USA", flag: "🇺🇸" }, 259 + MCI: { code: "MCI", location: "Kansas City, USA", flag: "🇺🇸" }, 260 + LAS: { code: "LAS", location: "Las Vegas, USA", flag: "🇺🇸" }, 261 + LAX: { code: "LAX", location: "Los Angeles, USA", flag: "🇺🇸" }, 262 + MFE: { code: "MFE", location: "McAllen, USA", flag: "🇺🇸" }, 263 + MEM: { code: "MEM", location: "Memphis, USA", flag: "🇺🇸" }, 264 + MIA: { code: "MIA", location: "Miami, USA", flag: "🇺🇸" }, 265 + MSP: { code: "MSP", location: "Minneapolis, USA", flag: "🇺🇸" }, 266 + MGM: { code: "MGM", location: "Montgomery, USA", flag: "🇺🇸" }, 267 + BNA: { code: "BNA", location: "Nashville, USA", flag: "🇺🇸" }, 268 + EWR: { code: "EWR", location: "Newark, USA", flag: "🇺🇸" }, 269 + OMA: { code: "OMA", location: "Omaha, USA", flag: "🇺🇸" }, 270 + PHL: { code: "PHL", location: "Philadelphia, USA", flag: "🇺🇸" }, 271 + PHX: { code: "PHX", location: "Phoenix, USA", flag: "🇺🇸" }, 272 + PIT: { code: "PIT", location: "Pittsburgh, USA", flag: "🇺🇸" }, 273 + PDX: { code: "PDX", location: "Portland, USA", flag: "🇺🇸" }, 274 + RIC: { code: "RIC", location: "Richmond, USA", flag: "🇺🇸" }, 275 + SMF: { code: "SMF", location: "Sacramento, USA", flag: "🇺🇸" }, 276 + SLC: { code: "SLC", location: "Salt Lake City, USA", flag: "🇺🇸" }, 277 + SAN: { code: "SAN", location: "San Diego, USA", flag: "🇺🇸" }, 278 + SFO: { code: "SFO", location: "San Francisco, USA", flag: "🇺🇸" }, 279 + SJC: { code: "SJC", location: "San Jose, USA", flag: "🇺🇸" }, 280 + SEA: { code: "SEA", location: "Seattle, USA", flag: "🇺🇸" }, 281 + IND: { code: "IND", location: "Indianapolis, USA", flag: "🇺🇸" }, 282 + STL: { code: "STL", location: "St. Louis, USA", flag: "🇺🇸" }, 283 + TLH: { code: "TLH", location: "Tallahassee, USA", flag: "🇺🇸" }, 284 + TPA: { code: "TPA", location: "Tampa, USA", flag: "🇺🇸" }, 285 + TAS: { code: "TAS", location: "Tashkent, Uzbekistan", flag: "🇺🇿" }, 286 + HAN: { code: "HAN", location: "Hanoi, Vietnam", flag: "🇻🇳" }, 287 + SGN: { code: "SGN", location: "Ho Chi Minh City, Vietnam", flag: "🇻🇳" }, 288 + HRE: { code: "HRE", location: "Harare, Zimbabwe", flag: "🇿🇼" }, 289 + };
+40
packages/header-analysis/src/regions/fly.ts
··· 1 + import type { Region } from "../types"; 2 + 3 + // https://fly.io/docs/reference/regions/ 4 + export const regions: Record<string, Region> = { 5 + ams: { code: "ams", location: "Amsterdam, Netherlands", flag: "🇳🇱" }, 6 + arn: { code: "arn", location: "Stockholm, Sweden", flag: "🇸🇪" }, 7 + atl: { code: "atl", location: "Atlanta, Georgia (US)", flag: "🇺🇸" }, 8 + bog: { code: "bog", location: "Bogotá, Colombia", flag: "🇨🇴" }, 9 + bom: { code: "bom", location: "Mumbai, India", flag: "🇮🇳" }, 10 + bos: { code: "bos", location: "Boston, Massachusetts (US)", flag: "🇺🇸" }, 11 + cdg: { code: "cdg", location: "Paris, France", flag: "🇫🇷" }, 12 + den: { code: "den", location: "Denver, Colorado (US)", flag: "🇺🇸" }, 13 + dfw: { code: "dfw", location: "Dallas, Texas (US)", flag: "🇺🇸" }, 14 + ewr: { code: "ewr", location: "Secaucus, NJ (US)", flag: "🇺🇸" }, 15 + eze: { code: "eze", location: "Ezeiza, Argentina", flag: "🇦🇷" }, 16 + fra: { code: "fra", location: "Frankfurt, Germany", flag: "🇩🇪" }, 17 + gdl: { code: "gdl", location: "Guadalajara, Mexico", flag: "🇲🇽" }, 18 + gig: { code: "gig", location: "Rio de Janeiro, Brazil", flag: "🇧🇷" }, 19 + gru: { code: "gru", location: "Sao Paulo, Brazil", flag: "🇧🇷" }, 20 + hkg: { code: "hkg", location: "Hong Kong, Hong Kong", flag: "🇭🇰" }, 21 + iad: { code: "iad", location: "Ashburn, Virginia (US)", flag: "🇺🇸" }, 22 + jnb: { code: "jnb", location: "Johannesburg, South Africa", flag: "🇿🇦" }, 23 + lax: { code: "lax", location: "Los Angeles, California (US)", flag: "🇺🇸" }, 24 + lhr: { code: "lhr", location: "London, United Kingdom", flag: "🇬🇧" }, 25 + mad: { code: "mad", location: "Madrid, Spain", flag: "🇪🇸" }, 26 + mia: { code: "mia", location: "Miami, Florida (US)", flag: "🇺🇸" }, 27 + nrt: { code: "nrt", location: "Tokyo, Japan", flag: "🇯🇵" }, 28 + ord: { code: "ord", location: "Chicago, Illinois (US)", flag: "🇺🇸" }, 29 + otp: { code: "otp", location: "Bucharest, Romania", flag: "🇷🇴" }, 30 + phx: { code: "phx", location: "Phoenix, Arizona (US)", flag: "🇺🇸" }, 31 + qro: { code: "qro", location: "Querétaro, Mexico", flag: "🇲🇽" }, 32 + scl: { code: "scl", location: "Santiago, Chile", flag: "🇨🇱" }, 33 + sea: { code: "sea", location: "Seattle, Washington (US)", flag: "🇺🇸" }, 34 + sin: { code: "sin", location: "Singapore, Singapore", flag: "🇸🇬" }, 35 + sjc: { code: "sjc", location: "San Jose, California (US)", flag: "🇺🇸" }, 36 + syd: { code: "syd", location: "Sydney, Australia", flag: "🇦🇺" }, 37 + waw: { code: "waw", location: "Warsaw, Poland", flag: "🇵🇱" }, 38 + yul: { code: "yul", location: "Montreal, Canada", flag: "🇨🇦" }, 39 + yyz: { code: "yyz", location: "Toronto, Canada", flag: "🇨🇦" }, 40 + };
+23
packages/header-analysis/src/regions/vercel.ts
··· 1 + import type { Region } from "../types"; 2 + 3 + // https://vercel.com/docs/edge-network/regions 4 + export const regions: Record<string, Region> = { 5 + arn1: { code: "arn1", location: "Stockholm, Sweden", flag: "🇸🇪" }, 6 + bom1: { code: "bom1", location: "Mumbai, India", flag: "🇮🇳" }, 7 + cdg1: { code: "cdg1", location: "Paris, France", flag: "🇫🇷" }, 8 + cle1: { code: "cle1", location: "Cleveland, USA", flag: "🇺🇸" }, 9 + cpt1: { code: "cpt1", location: "Cape Town, South Africa", flag: "🇿🇦" }, 10 + dub1: { code: "dub1", location: "Dublin, Ireland", flag: "🇮🇪" }, 11 + fra1: { code: "fra1", location: "Frankfurt, Germany", flag: "🇩🇪" }, 12 + gru1: { code: "gru1", location: "São Paulo, Brazil", flag: "🇧🇷" }, 13 + hkg1: { code: "hkg1", location: "Hong Kong", flag: "🇭🇰" }, 14 + hnd1: { code: "hnd1", location: "Tokyo, Japan", flag: "🇯🇵" }, 15 + iad1: { code: "iad1", location: "Washington, D.C., USA", flag: "🇺🇸" }, 16 + icn1: { code: "icn1", location: "Seoul, South Korea", flag: "🇰🇷" }, 17 + kix1: { code: "kix1", location: "Osaka, Japan", flag: "🇯🇵" }, 18 + lhr1: { code: "lhr1", location: "London, United Kingdom", flag: "🇬🇧" }, 19 + pdx1: { code: "pdx1", location: "Portland, USA", flag: "🇺🇸" }, 20 + sfo1: { code: "sfo1", location: "San Francisco, USA", flag: "🇺🇸" }, 21 + sin1: { code: "sin1", location: "Singapore", flag: "🇸🇬" }, 22 + syd1: { code: "syd1", location: "Sydney, Australia", flag: "🇦🇺" }, 23 + };
+9
packages/header-analysis/src/types/index.ts
··· 1 + export type Region = { 2 + code: string; 3 + location: string; 4 + flag: string; 5 + }; 6 + 7 + export type ParserReturn<T> = 8 + | { status: "success"; data: T } 9 + | { status: "failed"; error: Error };
+7
packages/header-analysis/tsconfig.json
··· 1 + { 2 + "extends": "@openstatus/tsconfig/base.json", 3 + "include": ["src", "*.ts"], 4 + "compilerOptions": { 5 + "resolveJsonModule": true 6 + } 7 + }
+12
pnpm-lock.yaml
··· 260 260 '@openstatus/emails': 261 261 specifier: workspace:* 262 262 version: link:../../packages/emails 263 + '@openstatus/header-analysis': 264 + specifier: workspace:* 265 + version: link:../../packages/header-analysis 263 266 '@openstatus/next-monitoring': 264 267 specifier: 0.0.2 265 268 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) ··· 731 734 react: 732 735 specifier: 18.2.0 733 736 version: 18.2.0 737 + typescript: 738 + specifier: 5.4.5 739 + version: 5.4.5 740 + 741 + packages/header-analysis: 742 + devDependencies: 743 + '@openstatus/tsconfig': 744 + specifier: workspace:* 745 + version: link:../tsconfig 734 746 typescript: 735 747 specifier: 5.4.5 736 748 version: 5.4.5