From ea093fa1e6e57e3e95e472a8667526c3400ab20c Mon Sep 17 00:00:00 2001 From: Mallya Date: Fri, 22 May 2026 03:06:22 +0530 Subject: [PATCH] fix: show rate limit error with reset time in CI Analytics (#244) - Check for 403 status before generic error handling - Parse X-RateLimit-Reset header and convert to local time - Show yellow warning with reset time instead of silent failure - Disable Refresh button until rate limit resets - Auto-clears error and re-enables button after reset time passes --- src/components/CIAnalytics.tsx | 104 ++++++++++++++++++++++++++++----- 1 file changed, 88 insertions(+), 16 deletions(-) diff --git a/src/components/CIAnalytics.tsx b/src/components/CIAnalytics.tsx index e6ad0778..8fbdd53e 100644 --- a/src/components/CIAnalytics.tsx +++ b/src/components/CIAnalytics.tsx @@ -16,8 +16,32 @@ export default function CIAnalytics() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [rateLimitResetTime, setRateLimitResetTime] = useState(null); + const [isRateLimited, setIsRateLimited] = useState(false); + + // Clear rate limit state once reset time passes + useEffect(() => { + if (!rateLimitResetTime) return; + + const msUntilReset = rateLimitResetTime.getTime() - Date.now(); + if (msUntilReset <= 0) { + setIsRateLimited(false); + setRateLimitResetTime(null); + return; + } + + const timer = setTimeout(() => { + setIsRateLimited(false); + setRateLimitResetTime(null); + setError(null); + }, msUntilReset); + + return () => clearTimeout(timer); + }, [rateLimitResetTime]); const fetchCIAnalytics = useCallback(() => { + if (isRateLimited) return; + setLoading(true); setError(null); @@ -28,17 +52,46 @@ export default function CIAnalytics() { fetch(`/api/metrics/ci${accountParam}`) .then((res) => { + if (res.status === 403) { + // Read reset time from header + const resetHeader = res.headers.get("X-RateLimit-Reset"); + if (resetHeader) { + const resetDate = new Date(parseInt(resetHeader, 10) * 1000); + const resetTimeStr = resetDate.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }); + setRateLimitResetTime(resetDate); + setIsRateLimited(true); + throw new Error( + `GitHub API rate limit reached. Resets at ${resetTimeStr}. Try again later.` + ); + } + throw new Error( + "GitHub API rate limit reached. Please try again later." + ); + } + if (!res.ok) { throw new Error("API error"); } + return res.json(); }) - .then((payload: CIAnalyticsData) => setData(payload)) - .catch(() => - setError("CI data unavailable - ensure Actions are enabled on your repos") - ) + .then((payload: CIAnalyticsData) => { + setData(payload); + setIsRateLimited(false); + setRateLimitResetTime(null); + }) + .catch((err: Error) => { + setError( + err.message.includes("rate limit") + ? err.message + : "CI data unavailable - ensure Actions are enabled on your repos" + ); + }) .finally(() => setLoading(false)); - }, [selectedAccount]); + }, [selectedAccount, isRateLimited]); useEffect(() => { fetchCIAnalytics(); @@ -53,6 +106,15 @@ export default function CIAnalytics() { ] : []; + const refreshLabel = isRateLimited + ? rateLimitResetTime + ? `Retry at ${rateLimitResetTime.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })}` + : "Rate limited" + : "Refresh"; + return (
@@ -67,9 +129,11 @@ export default function CIAnalytics() {
@@ -90,15 +154,23 @@ export default function CIAnalytics() { ))}
) : error ? ( -
+

{error}

- + {!isRateLimited && ( + + )}
) : data ? (
@@ -133,4 +205,4 @@ export default function CIAnalytics() { ) : null}
); -} +} \ No newline at end of file