Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/lib/answers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import { cache } from "react";
import yaml from "js-yaml";
import { z } from "zod";
import { loadBenchmark } from "@/lib/spec";
import {
REMOVED_ANSWER_SLUGS,
REMOVED_BENCH_SLUGS,
} from "@/lib/removed-benches";
import type { Benchmark } from "@/types/benchmark";

const ANSWERS_DIR = path.join(process.cwd(), "answers");
Expand Down Expand Up @@ -85,6 +89,15 @@ export const loadAllAnswers = cache(async (): Promise<Answer[]> => {
);
return parsed
.filter((a): a is Answer => a !== null && a.status === "live")
// Prod-only gate: answers built on staging-pipeline benches never
// reach the prod listing, sitemap or tag clouds. Direct URL hits
// get a 410 from middleware.
.filter(
(a) =>
process.env.VERCEL_ENV !== "production" ||
(!REMOVED_ANSWER_SLUGS.has(a.slug) &&
!REMOVED_BENCH_SLUGS.has(a.benchmark)),
)
.sort((a, b) => a.slug.localeCompare(b.slug));
});

Expand Down
10 changes: 10 additions & 0 deletions src/lib/removed-benches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@
* Moving a bench to production = remove its slug here, bump the
* bench-set cache keys in src/lib/spec.ts, ship dev to main.
*/
/**
* Answer pages (answers/<slug>.yml) whose referenced benchmark is in
* REMOVED_BENCH_SLUGS. Same treatment: 410 on prod direct hits, dropped
* from the answers listing and sitemap on prod, normal on staging.
*/
export const REMOVED_ANSWER_SLUGS = new Set([
"which-evm-aggregator-has-the-fastest-quote",
"which-solana-rpc-lands-the-most-transactions",
]);

export const REMOVED_BENCH_SLUGS = new Set([
// retired for good
"bridge-revenue",
Expand Down
38 changes: 35 additions & 3 deletions src/lib/rpc-hub-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
filterSig,
loadSpecsUncached,
} from "@/lib/materialize/load";
import { REMOVED_BENCH_SLUGS } from "@/lib/removed-benches";
import { readMaterialized, storeConfigured } from "@/lib/materialize/store";
import { chainLabelForSlug } from "@/lib/chains";
import type { Benchmark, ProviderResult } from "@/types/benchmark";
Expand Down Expand Up @@ -381,11 +382,40 @@ export async function buildRpcHubSnapshotFresh(): Promise<RpcHubSnapshot | null>
* KV-only; the Vercel side never touches Prometheus. Null means the
* page renders its "warming up" empty state.
*/
/** Prod-only gate on the worker-written snapshot. The worker builds the
* cohort blob from the FULL spec set (it runs outside Vercel, no
* VERCEL_ENV), so staging-pipeline chains like monad-rpc reach the
* shared KV; drop them at read time and recompute the pivot + totals
* so provider aggregates only span the chains actually shown. */
function gateSnapshotForProd(snap: RpcHubSnapshot): RpcHubSnapshot {
if (process.env.VERCEL_ENV !== "production") return snap;
const chains = snap.chains.filter((c) => !REMOVED_BENCH_SLUGS.has(c.slug));
if (chains.length === snap.chains.length) return snap;
const providersPivot = buildPivot(chains);
return {
...snap,
chains,
providersPivot,
totals: {
...snap.totals,
chains: chains.length,
uniqueProviders: providersPivot.length,
},
};
}

async function fetchRpcHubRaw(): Promise<RpcHubSnapshot | null> {
const snapshot = await readCohortSnapshot<RpcHubSnapshot>(RPC_HUB_KEY);
if (snapshot) return snapshot.data;
if (snapshot) return gateSnapshotForProd(snapshot.data);
const fresh = await buildRpcHubSnapshotFresh().catch(() => null);
if (fresh && cohortSnapshotConfigured()) {
// No writeback on production: fresh is built from the prod-gated spec
// set there, and the KV blob is shared with staging (worker + preview
// deployments own its full-set content).
if (
fresh &&
cohortSnapshotConfigured() &&
process.env.VERCEL_ENV !== "production"
) {
try {
await writeCohortSnapshot(RPC_HUB_KEY, fresh);
} catch (err) {
Expand All @@ -401,10 +431,12 @@ async function fetchRpcHubRaw(): Promise<RpcHubSnapshot | null> {

const fetchRpcHubCached = unstable_cache(
fetchRpcHubRaw,
// v4: prod-only gate drops staging-pipeline chains from the shared
// worker blob at read time; env in key since the set differs per env.
// v3: pivot rows gained medianSuccessPct/errors24h + per-chain
// successPct/sampleSize; chains gained unresponsive[] rows.
// v2: chains gained unresponsiveCount (unresponsive provider rows).
["rpc-hub-cohort-v3"],
["rpc-hub-cohort-v4", process.env.VERCEL_ENV === "production" ? "prod" : "all"],
{ revalidate: 60, tags: ["rpc-cohort"] },
);

Expand Down
13 changes: 11 additions & 2 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,14 @@ const CANONICAL_NO_QUERY = new Set([
// own module (not here) so the spec loader and the materialize worker
// can import it without pulling next/server. Re-exported for the
// sitemap, which historically imports it from "@/middleware".
import { REMOVED_BENCH_SLUGS } from "@/lib/removed-benches";
import {
REMOVED_ANSWER_SLUGS,
REMOVED_BENCH_SLUGS,
} from "@/lib/removed-benches";
export { REMOVED_BENCH_SLUGS };

const BENCH_PATH = /^\/benchmarks\/([a-z0-9][a-z0-9-]{0,79})\/?$/;
const ANSWER_PATH = /^\/answers\/([a-z0-9][a-z0-9-]{0,79})\/?$/;
// `/compare/<a>-vs-<b>` with both sides as standard provider slug
// shapes (lowercase alphanumeric + hyphens). The `-vs-` delimiter is
// matched literally; provider slugs themselves can contain hyphens
Expand All @@ -70,7 +74,11 @@ export function middleware(req: NextRequest) {

if (process.env.VERCEL_ENV === "production") {
const m = pathname.match(BENCH_PATH);
if (m && REMOVED_BENCH_SLUGS.has(m[1])) {
const a = pathname.match(ANSWER_PATH);
if (
(m && REMOVED_BENCH_SLUGS.has(m[1])) ||
(a && REMOVED_ANSWER_SLUGS.has(a[1]))
) {
return new NextResponse(
`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>410 Gone</title><meta name="robots" content="noindex"></head><body><h1>410 Gone</h1><p>This benchmark has been retired. See the <a href="/benchmarks">current catalog</a>.</p></body></html>`,
{ status: 410, headers: { "Content-Type": "text/html; charset=utf-8" } },
Expand Down Expand Up @@ -111,6 +119,7 @@ export const config = {
"/api/freshness",
"/api/openapi.json",
"/benchmarks/:slug*",
"/answers/:slug*",
"/compare/:slug*",
],
};
Loading