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
12 changes: 12 additions & 0 deletions .github/workflows/prod-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ jobs:
| xargs -P 4 -I{} curl -s -o /dev/null -w "%{http_code} %{time_total}s {}\n" --max-time 120 {} \
|| echo "warm-up failed (non-blocking)"

# Warm pages that can cold-render 500: products/polymarket triggers the
# degraded-read tripwire on empty store; alternatives/* for perp venues
# run a live Prom cohort fetch on first hit. Both self-heal after one
# successful warm render, so hitting them here pre-empts the smoke gate.
- name: Warm ISR-sensitive pages
run: |
base="https://openchainbench.com"
for path in /products/polymarket /alternatives/dydx /alternatives/gains /alternatives/hyperliquid /alternatives/polymarket; do
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 120 "$base$path" || echo "000")
echo "$code $base$path"
done

# Warm /api/citable explicitly: the aggregator cache is independent
# from per-bench caches, and the bench-page warm-up only populates
# the latter. Without this step, /api/citable's first hit after
Expand Down
8 changes: 3 additions & 5 deletions src/app/alternatives/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@ import { ProviderTypeBadge } from "@/components/provider-type-badge";
import { isRegion } from "@/lib/brand";
import { PERP_VENUE_META, benchRowsForVenue } from "@/lib/perp-venue-context";
import { fetchPerpCohort } from "@/lib/perp-stats";
import { loadBenchFromBlob } from "@/lib/bench-blob";
import { PerpVenueBenchCards } from "@/components/perp-venue-bench-cards";

export const revalidate = 60;
export const dynamic = "force-dynamic";

// Same budget as /products/[slug]: on-demand renders span the whole bench
// catalog and the 60s default killed them mid-flight.
Expand Down Expand Up @@ -69,17 +68,16 @@ export default async function AlternativePage({
const { slug } = await params;
const isPerpVenue = slug in PERP_VENUE_META;

const [alt, cohort, feesAtSizeBench] = await Promise.all([
const [alt, cohort] = await Promise.all([
loadAlternative(slug),
isPerpVenue ? fetchPerpCohort() : Promise.resolve(null),
isPerpVenue ? loadBenchFromBlob("perp-fees-at-size") : Promise.resolve(null),
]);
if (!alt) notFound();

const venueRow = cohort?.venues.find((v) => v.slug === slug) ?? null;
const perpBenchRows =
cohort && venueRow
? benchRowsForVenue(cohort, venueRow, feesAtSizeBench)
? benchRowsForVenue(cohort, venueRow, null)
: [];

const { bench } = alt;
Expand Down
22 changes: 13 additions & 9 deletions src/app/products/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import { getPmVenueContext } from "@/lib/pm-venue-context";
import { fetchPmDataFeedKpis } from "@/lib/pm-venue-data";
import { fetchPerpVenueKpis } from "@/lib/perp-venue-data";
import { hasRpcProviderData } from "@/lib/rpc-hub-stats";
import { loadBenchFromBlob } from "@/lib/bench-blob";
import { PmVenueSection } from "@/components/pm-venue-section";
import {
getPerpVenueContext,
Expand Down Expand Up @@ -204,14 +203,20 @@ export default async function ProviderPage({
a.result.availability === "live" &&
a.result.unresponsive !== true,
);
const anyMeasuredAppearance = p.appearances.some(
(a) => (a.result.ms?.p50 ?? 0) > 0,
// Exclude appearances with insufficient data confidence from the
// "any measured" check: a new provider whose every bench appearance is
// still warming up (insufficient sample count) legitimately has rank=0
// everywhere — that is not a degraded store read, it is warm-up state.
const anyConfidentAppearance = p.appearances.some(
(a) =>
(a.result.ms?.p50 ?? 0) > 0 &&
a.result.dataConfidence !== "insufficient",
);
if (
p.appearances.length >= 8 &&
p.appearances.every((a) => a.rank === 0) &&
allClaimLive &&
anyMeasuredAppearance
anyConfidentAppearance
) {
throw new Error(`degraded store read for /products/${slug}: ${p.appearances.length} appearances, all unranked`);
}
Expand All @@ -232,11 +237,10 @@ export default async function ProviderPage({
const pmContext = await getPmVenueContext(p.slug);

// Perp DEX cohort dashboard. Same pattern as the PM context above.
const isPerpSlug = PERP_PRODUCT_PILL_SLUGS.has(p.slug);
const feesAtSizeBench = isPerpSlug
? await loadBenchFromBlob("perp-fees-at-size")
: null;
const perpContext = await getPerpVenueContext(p.slug, feesAtSizeBench);
// perp-fees-at-size bench was removed; pass null so loadBenchFromBlob
// (cache: "no-store") is not called from an ISR page (it would trigger
// Next.js "Page changed from static to dynamic" for polymarket).
const perpContext = await getPerpVenueContext(p.slug, null);

// KPI-domain availability, resolved upfront so the pill bar knows
// every domain before rendering. A pill must never open onto an empty
Expand Down
6 changes: 6 additions & 0 deletions src/lib/sitemap-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
isHlBuilderSlug,
isHlBuilderWithHistory,
} from "@/lib/hl-builder-stats";
import { PERP_PRODUCT_PILL_SLUGS } from "@/lib/perp-venue-context";
import { loadAllAlternatives } from "@/lib/alternatives";
import { loadAllAnswers } from "@/lib/answers";
import { CHAIN_BY_SLUG, CHAINS, getBenchmarksForChain } from "@/lib/chains";
Expand Down Expand Up @@ -240,6 +241,9 @@ async function buildFullSitemap(): Promise<MetadataRoute.Sitemap> {
// Emitting them in the sitemap advertises URLs the middleware will
// then reject, failing the deploy-time sitemap-smoke gate.
if (REMOVED_BENCH_SLUGS.has(b.slug)) return [];
// Benches that are editorially live but have no Prom data yet render
// with <meta robots noindex>. Skip until data lands.
if (b.status === "draft" && b.editorialStatus === "live") return [];
const last = b.lastRunAt ? new Date(b.lastRunAt) : BUILD_TIME;
const entries: MetadataRoute.Sitemap = [
{
Expand Down Expand Up @@ -307,6 +311,8 @@ async function buildFullSitemap(): Promise<MetadataRoute.Sitemap> {
providerSlugs.map(async (slug) => {
if (CHAIN_BY_SLUG.has(slug)) return null;
if (await isHlBuilderSlug(slug)) return null;
// Perp venue slugs (except polymarket) 308 to /perp/<slug>.
if (PERP_PRODUCT_PILL_SLUGS.has(slug) && slug !== "polymarket") return null;
const p = await getProvider(slug);
return p ? slug : null;
}),
Expand Down
Loading