Always check timing data even on fetch error (4xx/5xx)

Resource Timing API records latency even for error responses.
Now we check for timing data regardless of fetch success/failure,
only reporting unreachable if no timing data is available.
This commit is contained in:
Jeffrey Paul 2026-01-30 22:25:32 -08:00
parent 8be7002ad9
commit 0038f23460

View File

@ -32,6 +32,25 @@ const hostState = HOSTS.map(host => ({
status: 'pending', // 'online', 'offline', 'pending', 'error' status: 'pending', // 'online', 'offline', 'pending', 'error'
})) }))
// Extract timing from Resource Timing API
function getTimingForUrl(url) {
const entries = performance.getEntriesByName(url, 'resource')
if (entries.length > 0) {
const entry = entries[entries.length - 1]
// Use responseEnd - requestStart for actual network time
// Fall back to duration if detailed timing not available (cross-origin)
let latency
if (entry.requestStart > 0 && entry.responseEnd > 0) {
latency = entry.responseEnd - entry.requestStart
} else {
// For opaque responses, duration includes queuing time but is still more accurate
latency = entry.duration
}
return Math.round(latency)
}
return null
}
// Measure latency using HEAD request with Resource Timing API // Measure latency using HEAD request with Resource Timing API
async function measureLatency(url) { async function measureLatency(url) {
const controller = new AbortController() const controller = new AbortController()
@ -43,6 +62,8 @@ async function measureLatency(url) {
targetUrl.searchParams.set('_cb', cacheBuster) targetUrl.searchParams.set('_cb', cacheBuster)
const finalUrl = targetUrl.toString() const finalUrl = targetUrl.toString()
let fetchError = null
try { try {
await fetch(finalUrl, { await fetch(finalUrl, {
method: 'HEAD', method: 'HEAD',
@ -50,39 +71,32 @@ async function measureLatency(url) {
cache: 'no-store', cache: 'no-store',
signal: controller.signal, signal: controller.signal,
}) })
clearTimeout(timeoutId)
// Get accurate timing from Resource Timing API
const entries = performance.getEntriesByName(finalUrl, 'resource')
if (entries.length > 0) {
const entry = entries[entries.length - 1]
// Use responseEnd - requestStart for actual network time
// Fall back to duration if detailed timing not available (cross-origin)
let latency
if (entry.requestStart > 0 && entry.responseEnd > 0) {
latency = entry.responseEnd - entry.requestStart
} else {
// For opaque responses, duration includes queuing time but is still more accurate
latency = entry.duration
}
// Clean up the entry
performance.clearResourceTimings()
return { latency: Math.round(latency), error: null }
}
// Fallback if no timing entry found
return { latency: null, error: null }
} catch (err) { } catch (err) {
clearTimeout(timeoutId) fetchError = err
performance.clearResourceTimings() }
if (err.name === 'AbortError') { clearTimeout(timeoutId)
return { latency: null, error: 'timeout' }
}
// Always check timing - even 4xx/5xx responses have timing data
const latency = getTimingForUrl(finalUrl)
performance.clearResourceTimings()
// If we got timing data, return it regardless of fetch error
if (latency !== null) {
return { latency, error: null }
}
// No timing data - determine error type
if (fetchError?.name === 'AbortError') {
return { latency: null, error: 'timeout' }
}
if (fetchError) {
return { latency: null, error: 'unreachable' } return { latency: null, error: 'unreachable' }
} }
// Fetch succeeded but no timing (shouldn't happen)
return { latency: null, error: null }
} }
// Get latency color based on value // Get latency color based on value