diff --git a/components/OrchestratingView/index.tsx b/components/OrchestratingView/index.tsx index e86cd4fb..b99a8aa2 100644 --- a/components/OrchestratingView/index.tsx +++ b/components/OrchestratingView/index.tsx @@ -97,7 +97,7 @@ const Index = ({ currentRound, transcoder, isActive }: Props) => { const maxScore = useMemo(() => { const topTransData = Object.keys(scores?.scores ?? {}).reduce( (prev, curr) => { - const score = scores?.scores[curr]; + const score = scores?.scores?.[curr]; const region = knownRegions?.regions?.find((r) => r.id === curr)?.name ?? "N/A"; if ( @@ -107,7 +107,7 @@ const Index = ({ currentRound, transcoder, isActive }: Props) => { ) { return { region: region, - score: scores?.scores[curr], + score, }; } return prev; @@ -128,8 +128,9 @@ const Index = ({ currentRound, transcoder, isActive }: Props) => { precision: 1, })} - ${maxScore.transcoding.region}` : ""; - return outputTrans ? transcodingInfo : "N/A"; - }, [maxScore]); + if (outputTrans) return transcodingInfo; + return scores?.scores === null ? "Unavailable" : "N/A"; + }, [maxScore, scores]); const maxAIScoreOutput = useMemo(() => { const outputAI = maxScore.ai?.value && maxScore.ai?.value > 0; @@ -148,8 +149,11 @@ const Index = ({ currentRound, transcoder, isActive }: Props) => { score: aiInfo, modelText: `. The pipeline and model for this Orchestrator was '${maxScore.ai?.pipeline}' and '${maxScore.ai?.model}'`, } - : { score: "N/A", modelText: "" }; - }, [knownRegions?.regions, maxScore]); + : { + score: scores?.topAIScore === null ? "Unavailable" : "N/A", + modelText: "", + }; + }, [knownRegions?.regions, maxScore, scores]); return ( { className="masonry-grid_item" label="Price / Pixel" tooltip="The most recent price for transcoding which the orchestrator is currently advertising off-chain to gateways. This may be different from on-chain pricing." - value={scores ? `${formatNumber(scores.pricePerPixel)} WEI` : "N/A"} + value={ + !scores + ? "N/A" + : scores.pricePerPixel === null + ? "Unavailable" + : `${formatNumber(scores.pricePerPixel)} WEI` + } /> {/* (url: string): Promise => { + try { + const response = await fetchWithRetry(url); + + if (!response.ok) { + console.error( + `Fetch error: ${url}`, + response.status, + (await response.text()).slice(0, 500) + ); + return null; + } + + return await response.json(); + } catch (err) { + console.error( + `Fetch error: ${url}`, + err instanceof Error ? err.message : err + ); + return null; + } +}; + const handler = async ( req: NextApiRequest, res: NextApiResponse @@ -63,8 +84,6 @@ const handler = async ( if (method === "GET") { const { address } = req.query; - res.setHeader("Cache-Control", getCacheControlHeader("hour")); - if (!!address && !Array.isArray(address) && isAddress(address)) { const transcoderId = address.toLowerCase(); @@ -72,54 +91,32 @@ const handler = async ( const metricsUrl = `${process.env.NEXT_PUBLIC_METRICS_SERVER_URL}/api/aggregated_stats?orchestrator=${transcoderId}`; const pricingUrl = `${CHAIN_INFO[DEFAULT_CHAIN_ID].pricingUrl}?excludeUnavailable=False`; - const [topScoreResponse, metricsResponse, priceResponse] = - await Promise.all([ - fetchWithRetry(topScoreUrl), - fetchWithRetry(metricsUrl), - fetchWithRetry(pricingUrl), - ]); - - if (!topScoreResponse.ok) { - const errorText = await topScoreResponse.text(); - console.error( - "Top AI score fetch error:", - topScoreResponse.status, - errorText - ); - return externalApiError(res, "AI metrics server"); - } + const [topAIScore, metrics, transcodersWithPrice] = await Promise.all([ + fetchJson(topScoreUrl), + fetchJson(metricsUrl), + fetchJson(pricingUrl), + ]); - if (!metricsResponse.ok) { - const errorText = await metricsResponse.text(); - console.error( - "Metrics fetch error:", - metricsResponse.status, - errorText - ); - return externalApiError(res, "metrics server"); + // Every upstream being down is never a legitimate empty state, so fail + // loudly rather than rendering a healthy-looking orchestrator with no data. + if (!topAIScore && !metrics && !transcodersWithPrice) { + return externalApiError(res, "all metrics servers"); } - if (!priceResponse.ok) { - const errorText = await priceResponse.text(); - console.error( - "Transcoder price fetch error:", - priceResponse.status, - errorText - ); - return externalApiError(res, "pricing server"); - } - - const topAIScore: ScoreResponse = await topScoreResponse.json(); - const metrics: MetricsResponse = await metricsResponse.json(); - const transcodersWithPrice: PriceResponse = await priceResponse.json(); + // Expire quickly so upstream recovery is not hidden for an hour. + const degraded = !topAIScore || !metrics || !transcodersWithPrice; + res.setHeader( + "Cache-Control", + getCacheControlHeader(degraded ? "minute" : "hour") + ); - const transcoderWithPrice = transcodersWithPrice.find((t) => + const transcoderWithPrice = transcodersWithPrice?.find((t) => checkAddressEquality(t.Address, transcoderId) ); const uniqueRegions = (() => { const keys = new Set(); - Object.values(metrics).forEach((metric) => { + Object.values(metrics ?? {}).forEach((metric) => { if (metric) { Object.keys(metric).forEach((key) => keys.add(key)); } @@ -130,8 +127,10 @@ const handler = async ( const createMetricsObject = ( metricKey: keyof Metric, transcoderId: string, - metrics: MetricsResponse - ): RegionalValues => { + metrics: MetricsResponse | null + ): RegionalValues | null => { + if (!metrics) return null; + const metricsObject: RegionalValues = uniqueRegions.reduce( (acc, metricsRegionKey) => { const value = @@ -153,7 +152,9 @@ const handler = async ( }; const combined: PerformanceMetrics = { - pricePerPixel: transcoderWithPrice?.PricePerPixel ?? 0, + pricePerPixel: transcodersWithPrice + ? transcoderWithPrice?.PricePerPixel ?? 0 + : null, successRates: createMetricsObject( "success_rate", transcoderId,