diff --git a/src/main.js b/src/main.js index c38fd66..13a7d09 100644 --- a/src/main.js +++ b/src/main.js @@ -32,6 +32,25 @@ const hostState = HOSTS.map(host => ({ 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 async function measureLatency(url) { const controller = new AbortController() @@ -43,6 +62,8 @@ async function measureLatency(url) { targetUrl.searchParams.set('_cb', cacheBuster) const finalUrl = targetUrl.toString() + let fetchError = null + try { await fetch(finalUrl, { method: 'HEAD', @@ -50,39 +71,32 @@ async function measureLatency(url) { cache: 'no-store', 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) { - clearTimeout(timeoutId) - performance.clearResourceTimings() + fetchError = err + } - if (err.name === 'AbortError') { - return { latency: null, error: 'timeout' } - } + clearTimeout(timeoutId) + // 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' } } + + // Fetch succeeded but no timing (shouldn't happen) + return { latency: null, error: null } } // Get latency color based on value