From d316fd2216809946166f2e101861087609f53516 Mon Sep 17 00:00:00 2001
From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com>
Date: Wed, 29 Jul 2026 17:13:50 +0200
Subject: [PATCH 1/2] fix: merge region+chain pills into one flex row, no
labels
---
src/components/data-api-bench-groups.tsx | 102 +++++++++--------------
1 file changed, 39 insertions(+), 63 deletions(-)
diff --git a/src/components/data-api-bench-groups.tsx b/src/components/data-api-bench-groups.tsx
index f746488b..30c4237a 100644
--- a/src/components/data-api-bench-groups.tsx
+++ b/src/components/data-api-bench-groups.tsx
@@ -165,19 +165,9 @@ function BenchRow({
{/* Region / chain breakdown */}
{showRegionChain ? (
-
- {hasRegions && (
-
- Region
-
-
- )}
- {hasChains && (
-
- Chain
-
-
- )}
+
+ {hasRegions && }
+ {hasChains && }
) : (
global
@@ -200,7 +190,7 @@ function RegionStrip({
);
return (
-
+ <>
{sorted.map((l) => (
))}
-
+ >
);
}
@@ -240,14 +230,14 @@ function ChainStrip({
const uniqueWinners = new Set(leaders.map((l) => l.providerSlug)).size;
const isSpecialist = uniqueWinners === leaders.length && leaders.length > 3;
- if (isSpecialist) {
- // Each chain has its own specialist — compact icon-only chips
- const MAX = 5;
- const visible = leaders.slice(0, MAX);
- const overflow = leaders.length - MAX;
- return (
-
- {visible.map((l) => (
+ const MAX = isSpecialist ? 5 : 3;
+ const visible = leaders.slice(0, MAX);
+ const overflow = leaders.length - MAX;
+
+ return (
+ <>
+ {visible.map((l) =>
+ isSpecialist ? (
- ))}
- {overflow > 0 && (
- +{overflow}
- )}
-
- );
- }
-
- // Some provider dominates multiple chains — full pills capped at 3
- const MAX = 3;
- const visible = leaders.slice(0, MAX);
- const overflow = leaders.length - MAX;
-
- return (
-
- {visible.map((l) => (
-
-
- {l.label}
-
-
-
- {l.providerName.split(" ")[0]}
-
-
- {fmtDataValue(l.p50, unit)}
-
-
- ))}
+
+ {l.label}
+
+
+
+ {l.providerName.split(" ")[0]}
+
+
+ {fmtDataValue(l.p50, unit)}
+
+
+ )
+ )}
{overflow > 0 && (
+{overflow}
)}
-
+ >
);
}
From d10b30d915fbabd8a22ef40da99e81104efe5df6 Mon Sep 17 00:00:00 2001
From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com>
Date: Wed, 29 Jul 2026 17:14:31 +0200
Subject: [PATCH 2/2] fix: stale KV fallback + never cache null on /perps
cohort fetch
---
src/lib/perp-stats.ts | 57 +++++++++++++++++++++++++++++++++++++------
1 file changed, 49 insertions(+), 8 deletions(-)
diff --git a/src/lib/perp-stats.ts b/src/lib/perp-stats.ts
index 81cfed26..70cf55b5 100644
--- a/src/lib/perp-stats.ts
+++ b/src/lib/perp-stats.ts
@@ -314,12 +314,13 @@ async function fetchPerpCohortRaw(): Promise {
const snapshot = await readCohortSnapshot(
PERP_COHORT_KEY,
);
- // Only use the snapshot if it was written within the last 2 minutes.
- // Without this check, the 24h KV TTL means stale Prometheus data is
- // served indefinitely even though the harness publishes every minute.
+ // Fresh snapshot (written within the last 2 min) is the fast path.
if (snapshot && snapshot.ageMs < 2 * 60 * 1000) {
return snapshot.data;
}
+ // Snapshot stale or missing — try live Prom. On Vercel, promUrl()
+ // returns null (VPS Prom is not publicly exposed), so this only
+ // succeeds from the worker process.
const fresh = await fetchPerpCohortFresh();
if (fresh) {
try {
@@ -331,18 +332,41 @@ async function fetchPerpCohortRaw(): Promise {
}`,
);
}
+ return fresh;
}
- return fresh;
+ // Both paths failed. Serve the stale snapshot as a last resort so a
+ // brief worker hiccup or Vercel cache-miss race doesn't blank the
+ // page. Cap at 10 min: beyond that we surface "unavailable" so a
+ // sustained worker outage is still visible.
+ if (snapshot && snapshot.ageMs < 10 * 60 * 1000) {
+ console.warn(
+ `perp-cohort: serving stale snapshot (${Math.round(snapshot.ageMs / 1000)}s old)`,
+ );
+ return snapshot.data;
+ }
+ return null;
}
const fetchPerpCohortCached = unstable_cache(
- fetchPerpCohortRaw,
+ async () => {
+ const data = await fetchPerpCohortRaw();
+ // Throw on null so unstable_cache does not store a cached null —
+ // a single worker hiccup would otherwise blank the page for the
+ // full 60 s revalidate window. The public wrapper catches and
+ // returns null so the page still renders its fallback banner.
+ if (data === null) throw new Error("perp-cohort-unavailable");
+ return data;
+ },
["perp-cohort-v1"],
{ revalidate: 60, tags: ["perp-cohort"] },
);
export async function fetchPerpCohort(): Promise {
- return fetchPerpCohortCached();
+ try {
+ return await fetchPerpCohortCached();
+ } catch {
+ return null;
+ }
}
/**
@@ -453,18 +477,35 @@ async function fetchPerpByAssetMatrixRaw(): Promise {
}`,
);
}
+ return fresh;
+ }
+ // Prom failed — serve stale snapshot up to 10 min rather than empty.
+ if (snapshot && snapshot.ageMs < 10 * 60 * 1000 && Array.isArray(snapshot.data) && snapshot.data.length > 0) {
+ console.warn(
+ `perp-by-asset: serving stale snapshot (${Math.round(snapshot.ageMs / 1000)}s old)`,
+ );
+ return snapshot.data;
}
return fresh;
}
const fetchPerpByAssetMatrixCached = unstable_cache(
- fetchPerpByAssetMatrixRaw,
+ async () => {
+ const data = await fetchPerpByAssetMatrixRaw();
+ // Don't cache an empty result — same rationale as perp-cohort above.
+ if (data.length === 0) throw new Error("perp-by-asset-unavailable");
+ return data;
+ },
["perp-by-asset-matrix-v2"],
{ revalidate: 120, tags: ["perp-by-asset"] },
);
export async function fetchPerpByAssetMatrix(): Promise {
- return fetchPerpByAssetMatrixCached();
+ try {
+ return await fetchPerpByAssetMatrixCached();
+ } catch {
+ return [];
+ }
}
/**
|