From 5486442aa8541beb8bdc7f0ec80800adad7dac76 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Fri, 10 Jul 2026 16:09:18 +0200 Subject: [PATCH] chains: RPC summary panel behind KPI pill toggle --- src/app/chains/[slug]/page.tsx | 52 ++++++++++--- src/components/chain-rpc-section.tsx | 106 +++++++++++++++++++++++++++ src/lib/rpc-hub-stats.ts | 20 +++++ 3 files changed, 166 insertions(+), 12 deletions(-) create mode 100644 src/components/chain-rpc-section.tsx diff --git a/src/app/chains/[slug]/page.tsx b/src/app/chains/[slug]/page.tsx index 75fba5a0..88e1e7c5 100644 --- a/src/app/chains/[slug]/page.tsx +++ b/src/app/chains/[slug]/page.tsx @@ -14,6 +14,12 @@ import { hasAnyKpi, } from "@/lib/chain-kpis"; import { ChainKpiStrip } from "@/components/chain-kpi-strip"; +import { ChainRpcSection } from "@/components/chain-rpc-section"; +import { + VenueKpiToggle, + type VenueToggleSection, +} from "@/components/venue-kpi-toggle"; +import { getRpcChainRow } from "@/lib/rpc-hub-stats"; import { Breadcrumb } from "@/components/breadcrumb"; import { Pill } from "@/components/pill"; import { ProviderLogo } from "@/components/provider-logo"; @@ -96,14 +102,46 @@ export default async function ChainPage({ // TVL full history is fetched directly from DefiLlama (single ~50 KB // call, revalidated every 15 min). Client renders the active range // (7D/30D/90D/1Y/All) without extra network round-trips. - const [kpis, tvlHistory] = await Promise.all([ + const [kpis, tvlHistory, rpcRow] = await Promise.all([ fetchChainKpis(slug), fetchChainTvlFullHistory(slug), + getRpcChainRow(slug), ]); const byCategory = groupByCategory(benches); const url = `${SITE.url}/chains/${slug}`; + // KPI domain pill bar (same pattern as /products/): the existing + // Live KPIs strip (including the TVL chart) becomes the "Chain KPIs" + // section, and chains covered by a -rpc bench gain an "RPC" + // section. Chains without a hub row keep a single "Chain KPIs" pill; + // the strip's own gating (kpis && hasAnyKpi) is unchanged. + const kpiSections: VenueToggleSection[] = []; + if (kpis && hasAnyKpi(kpis)) { + kpiSections.push({ + id: "chain-kpis", + label: "Chain KPIs", + content: ( +
+ +
+ ), + }); + } + if (rpcRow) { + kpiSections.push({ + id: "rpc", + label: "RPC", + content: , + }); + } + // ItemList of Datasets, one entry per benchmark that touches this chain. // Previously CollectionPage — Google's rich result validator doesn't // recognise CollectionPage as a supported type and flagged every chain @@ -183,17 +221,7 @@ export default async function ChainPage({ the chain scoped bench page when one exists.

- {kpis && hasAnyKpi(kpis) && ( -
- -
- )} +
{Array.from(byCategory.entries()).map(([category, list]) => ( diff --git a/src/components/chain-rpc-section.tsx b/src/components/chain-rpc-section.tsx new file mode 100644 index 00000000..2c3e155b --- /dev/null +++ b/src/components/chain-rpc-section.tsx @@ -0,0 +1,106 @@ +import Link from "next/link"; +import { ArrowUpRight } from "lucide-react"; +import { + fetchRpcHub, + getRpcChainRow, + RPC_REGION_KEYS, + type RpcRegionKey, +} from "@/lib/rpc-hub-stats"; + +/** + * Compact RPC summary panel for /chains/: the chain centric cut of + * the rpc-hub cohort snapshot (the provider centric cut lives in + * RpcProviderChainsSection on product pages). Shows the 24h p50 leader, + * the best provider per probe region, and links out to the full + * -rpc bench page for the complete leaderboard. + * + * Server component, snapshot only (fetchRpcHub reads the worker written + * cohort blob via unstable_cache; zero Prometheus traffic). Returns null + * when the chain has no rpc-hub row, so the page can render it + * unconditionally, though the chain page gates the pill upfront via + * getRpcChainRow (same cached read, no extra roundtrip). + */ + +const REGION_LABELS: Record = { + "us-east": "US East", + "eu-west": "EU West", + sgp: "Singapore", +}; + +function fmtMs(v: number): string { + if (v < 1000) return `${Math.round(v)} ms`; + return `${(v / 1000).toFixed(2)} s`; +} + +function fmtUtc(iso: string): string | null { + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return null; + return `${d.toISOString().slice(0, 16).replace("T", " ")} UTC`; +} + +export async function ChainRpcSection({ chainSlug }: { chainSlug: string }) { + const [snapshot, row] = await Promise.all([ + fetchRpcHub(), + getRpcChainRow(chainSlug), + ]); + if (!snapshot || !row || !row.best) return null; + + const asOf = snapshot.generatedAt ? fmtUtc(snapshot.generatedAt) : null; + + return ( +
+

+ Fastest public RPC on {row.name} +

+

+ The fastest public RPC endpoint on {row.name} right now is{" "} + {row.best.providerName}{" "} + at{" "} + + {fmtMs(row.best.p50Ms)} + {" "} + p50 over the last 24 hours, counting successful calls only, across{" "} + {row.providerCount} live provider + {row.providerCount === 1 ? "" : "s"} measured from 3 probe regions. +

+
+ {RPC_REGION_KEYS.map((region) => { + const best = row.regions[region]; + return ( +
+
+ {REGION_LABELS[region]} +
+
+ {best ? ( + <> + + {best.providerName} + {" "} + + {fmtMs(best.p50Ms)} + + + ) : ( + no data + )} +
+
+ ); + })} +
+
+ + Full {row.name} RPC benchmark + + + {asOf && ( + Data as of {asOf} + )} +
+
+ ); +} diff --git a/src/lib/rpc-hub-stats.ts b/src/lib/rpc-hub-stats.ts index 82a5f423..5912361e 100644 --- a/src/lib/rpc-hub-stats.ts +++ b/src/lib/rpc-hub-stats.ts @@ -31,6 +31,7 @@ import { import { REMOVED_BENCH_SLUGS } from "@/lib/removed-benches"; import { readMaterialized, storeConfigured } from "@/lib/materialize/store"; import { chainLabelForSlug } from "@/lib/chains"; +import { matchesChainSlug } from "@/lib/chain-aliases"; import type { Benchmark, ProviderResult } from "@/types/benchmark"; import type { Spec } from "@/lib/spec-schema"; @@ -452,6 +453,25 @@ export async function fetchRpcHub(): Promise { * a page that also renders the section costs no extra roundtrip. Used * to decide upfront whether the RPC domain gets a KPI pill. */ +/** + * Chain centric availability helper for /chains/: the hub row for + * a site chain slug, or null when the chain has no `-rpc` bench + * in the snapshot. Alias aware (matchesChainSlug), so a renamed site + * slug still lands on a hub row carrying the legacy slug. Reads the + * same cached snapshot as ChainRpcSection, so gating the RPC KPI pill + * costs no extra roundtrip. + */ +export async function getRpcChainRow( + siteChainSlug: string, +): Promise { + const snapshot = await fetchRpcHub(); + if (!snapshot) return null; + return ( + snapshot.chains.find((c) => matchesChainSlug(c.chain, siteChainSlug)) ?? + null + ); +} + export async function hasRpcProviderData(slug: string): Promise { const snapshot = await fetchRpcHub(); if (!snapshot) return false;