Targets whose home page redirects are checked at /robots.txt (closes #194)
check / check (push) Canceled after 0s

A check follows a redirect and times the request to the new URL too. So the AWS Console, the Google Cloud Console, Akamai, B2 and the eight S3 regions are now checked at /robots.txt on their own host, which answers without a redirect. Before, the S3 and B2 figures also timed a page on another host. The S3 regions are checked with HEAD, because Chrome closes the connection after a GET of their /robots.txt and the figure would then include connection setup every time. README.md says what a figure includes.

Reports name a target by its URL, so these targets' samples are stored under the new URLs.

Model: opus-5-5
This commit was merged in pull request #193.
This commit is contained in:
2026-10-26 04:11:48 +01:00
parent a72797a403
commit 8a62f12802
5 changed files with 153 additions and 41 deletions
+38 -21
View File
@@ -67,51 +67,66 @@ export const CONFIG = {
// WAN endpoints to monitor. These are used for the aggregate health/stats
// display (min/max/avg). Ordered: personal, LLM APIs, big-3 cloud, then
// CDN/hosting/other.
// CDN/hosting/other. A check follows a redirect and times the request to
// the new URL too. So a target whose home page redirects is checked at
// /robots.txt on its own host. That URL answers without a redirect.
const WAN_HOSTS = [
{ name: "datavi.be", url: "https://datavi.be" },
{ name: "Anthropic API", url: "https://api.anthropic.com" },
{ name: "OpenAI API", url: "https://api.openai.com" },
{ name: "AWS Console", url: "https://console.aws.amazon.com" },
{ name: "Google Cloud Console", url: "https://console.cloud.google.com" },
{ name: "AWS Console", url: "https://console.aws.amazon.com/robots.txt" },
{
name: "Google Cloud Console",
url: "https://console.cloud.google.com/robots.txt",
},
{ name: "Microsoft Azure", url: "https://portal.azure.com" },
{ name: "Cloudflare", url: "https://www.cloudflare.com" },
{ name: "Fastly CDN", url: "https://www.fastly.com" },
{ name: "Akamai", url: "https://www.akamai.com" },
{ name: "Akamai", url: "https://www.akamai.com/robots.txt" },
{ name: "Google", url: "https://www.google.com" },
{ name: "GitHub", url: "https://github.com" },
{ name: "B2", url: "https://api.backblazeb2.com" },
{ name: "B2", url: "https://api.backblazeb2.com/robots.txt" },
// Chrome keeps the connection to an S3 region open after a HEAD check,
// but closes it after most GET checks.
{
name: "S3 af-south-1 (Cape Town)",
url: "https://s3.af-south-1.amazonaws.com",
url: "https://s3.af-south-1.amazonaws.com/robots.txt",
method: "HEAD",
},
{
name: "S3 eu-west-2 (London)",
url: "https://s3.eu-west-2.amazonaws.com",
url: "https://s3.eu-west-2.amazonaws.com/robots.txt",
method: "HEAD",
},
{
name: "S3 me-south-1 (Bahrain)",
url: "https://s3.me-south-1.amazonaws.com",
url: "https://s3.me-south-1.amazonaws.com/robots.txt",
method: "HEAD",
},
{
name: "S3 ap-northeast-1 (Tokyo)",
url: "https://s3.ap-northeast-1.amazonaws.com",
url: "https://s3.ap-northeast-1.amazonaws.com/robots.txt",
method: "HEAD",
},
{
name: "S3 ap-southeast-1 (Singapore)",
url: "https://s3.ap-southeast-1.amazonaws.com",
url: "https://s3.ap-southeast-1.amazonaws.com/robots.txt",
method: "HEAD",
},
{
name: "S3 ap-southeast-2 (Sydney)",
url: "https://s3.ap-southeast-2.amazonaws.com",
url: "https://s3.ap-southeast-2.amazonaws.com/robots.txt",
method: "HEAD",
},
{
name: "S3 us-west-2 (Oregon)",
url: "https://s3.us-west-2.amazonaws.com",
url: "https://s3.us-west-2.amazonaws.com/robots.txt",
method: "HEAD",
},
{
name: "S3 sa-east-1 (São Paulo)",
url: "https://s3.sa-east-1.amazonaws.com",
url: "https://s3.sa-east-1.amazonaws.com/robots.txt",
method: "HEAD",
},
// Hetzner regional speed test servers — genuine per-DC endpoints
{
@@ -271,6 +286,7 @@ export class HostState {
constructor(host, pinned = false) {
this.name = host.name;
this.url = host.url;
this.method = host.method;
// Each entry is either a check's result, { timestamp, latency,
// error }, or a round skipped while paused, { timestamp,
// latency: null, paused: true }.
@@ -567,17 +583,18 @@ export class Reporter {
// --- Latency Measurement -----------------------------------------------------
// Checks one target. The check times out after CONFIG.requestTimeout; the
// caller can give it up sooner through the optional signal, which also ends
// it as a timeout. A failed check's reason says what went wrong and after
// how long; a check that answered has none.
// Checks one target with the given HTTP method, GET when none is given. The
// check times out after CONFIG.requestTimeout; the caller can give it up
// sooner through the optional signal, which also ends it as a timeout. A
// failed check's reason says what went wrong and after how long; a check
// that answered has none.
//
// The Hetzner speed-test servers close the connection without an answer
// when the URL has a query string or the request has a Referer header. So
// the URL is fetched as written, and referrerPolicy: "no-referrer" sends
// no Referer header, wherever the page is served from. cache: "no-store"
// keeps the browser's cache out of the measurement.
export async function measureLatency(url, signal) {
export async function measureLatency(url, signal, method = "GET") {
const controller = new AbortController();
const timeoutId = setTimeout(
() => controller.abort(),
@@ -589,7 +606,7 @@ export async function measureLatency(url, signal) {
try {
await fetch(url, {
method: "GET",
method,
mode: "no-cors",
cache: "no-store",
referrerPolicy: "no-referrer",
@@ -1292,7 +1309,7 @@ export async function tick(state, signal, onOffline) {
// the round started.
await Promise.all(
state.allHosts.map(async (host) => {
const r = await measureLatency(host.url, signal);
const r = await measureLatency(host.url, signal, host.method);
if (state.paused || signal.aborted || state.tickCount === 0) {
return;
}
@@ -1365,7 +1382,7 @@ function startRecoveryProbe(state, startRounds) {
const checks = new AbortController();
state._recoveryProbeChecks = checks;
for (const host of canaries) {
measureLatency(host.url, checks.signal).then((r) => {
measureLatency(host.url, checks.signal, host.method).then((r) => {
if (r.error !== null || checks.signal.aborted) return;
log.notice("Recovery probe: connectivity detected");
stopRecoveryProbe(state);