From 8839245e119b5d3e93997ed51194c688d3576507 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 14 Apr 2026 19:30:58 +0000 Subject: [PATCH 1/2] fix(crux): Degrade invalid CrUX URLs to no-data Treat CrUX 400 responses for unsupported or uncovered URLs as\nrecoverable so the analysis workflow returns the existing empty-state\noutput instead of capturing an application exception.\n\nKeep non-recoverable CrUX failures blocking so infrastructure and auth\nproblems still surface for investigation.\n\nFixes WEBVITALS-2S\nCo-Authored-By: Claude Co-authored-by: Sergiy Dybskiy --- ai/tools/real-world-performance.ts | 80 +++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/ai/tools/real-world-performance.ts b/ai/tools/real-world-performance.ts index 058b41a..1161fe4 100644 --- a/ai/tools/real-world-performance.ts +++ b/ai/tools/real-world-performance.ts @@ -7,6 +7,19 @@ import type { RealWorldPerformanceOutput, } from "@/types/real-world-performance"; +class RecoverableCruxApiError extends Error { + constructor(message: string) { + super(message); + this.name = "RecoverableCruxApiError"; + } +} + +function isRecoverableCruxApiError( + error: unknown, +): error is RecoverableCruxApiError { + return error instanceof RecoverableCruxApiError; +} + const realWorldPerformanceInputSchema = z.object({ url: z.string().describe("The URL to analyze for real-world performance"), devices: z @@ -66,7 +79,7 @@ async function fetchPerformanceData( if (!response.ok) { if (response.status === 400) { - throw new Error( + throw new RecoverableCruxApiError( `CrUX API failed for ${strategy} (${response.status}): URL may be invalid, not in CrUX dataset, or contains query parameters. URL: ${url}`, ); } @@ -188,7 +201,8 @@ async function getRealWorldPerformance( const results = await Promise.allSettled(promises); // Track errors for better error messages - const errors: { device: string; error: string }[] = []; + const errors: { device: string; error: string; recoverable: boolean }[] = + []; const mobileData = deviceOrder.includes("mobile") ? (() => { @@ -205,19 +219,29 @@ async function getRealWorldPerformance( mobileResult.reason instanceof Error ? mobileResult.reason.message : "Unknown error"; - errors.push({ device: "mobile", error: errorMessage }); + const isRecoverableError = isRecoverableCruxApiError( + mobileResult.reason, + ); + errors.push({ + device: "mobile", + error: errorMessage, + recoverable: isRecoverableError, + }); Sentry.logger.warn("Mobile CrUX data fetch failed", { url: normalizedUrl, error: errorMessage, + recoverable: isRecoverableError, }); - Sentry.captureException(mobileResult.reason, { - tags: { - component: "real-world-performance-tool", - operation: "fetchPerformanceData", - strategy: "mobile", - }, - extra: { url: normalizedUrl }, - }); + if (!isRecoverableError) { + Sentry.captureException(mobileResult.reason, { + tags: { + component: "real-world-performance-tool", + operation: "fetchPerformanceData", + strategy: "mobile", + }, + extra: { url: normalizedUrl }, + }); + } return null; })() : null; @@ -237,19 +261,29 @@ async function getRealWorldPerformance( desktopResult.reason instanceof Error ? desktopResult.reason.message : "Unknown error"; - errors.push({ device: "desktop", error: errorMessage }); + const isRecoverableError = isRecoverableCruxApiError( + desktopResult.reason, + ); + errors.push({ + device: "desktop", + error: errorMessage, + recoverable: isRecoverableError, + }); Sentry.logger.warn("Desktop CrUX data fetch failed", { url: normalizedUrl, error: errorMessage, + recoverable: isRecoverableError, }); - Sentry.captureException(desktopResult.reason, { - tags: { - component: "real-world-performance-tool", - operation: "fetchPerformanceData", - strategy: "desktop", - }, - extra: { url: normalizedUrl }, - }); + if (!isRecoverableError) { + Sentry.captureException(desktopResult.reason, { + tags: { + component: "real-world-performance-tool", + operation: "fetchPerformanceData", + strategy: "desktop", + }, + extra: { url: normalizedUrl }, + }); + } return null; })() : null; @@ -310,11 +344,11 @@ async function getRealWorldPerformance( const requestedOnlyDesktop = !devices.includes("mobile") && devices.includes("desktop"); - // Only throw if we had actual API errors (not just empty metrics) - const hasActualErrors = errors.length > 0; + // Invalid / unsupported CrUX URLs should degrade to the empty-state UI. + const hasBlockingErrors = errors.some((error) => !error.recoverable); if ( - hasActualErrors && + hasBlockingErrors && ((requestedBothDevices && !hasMobileData && !hasDesktopData) || (requestedOnlyMobile && !hasMobileData) || (requestedOnlyDesktop && !hasDesktopData)) From cc72a2775ae7af48ce3b3d784082fe930ef7b068 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 14 Apr 2026 19:32:26 +0000 Subject: [PATCH 2/2] style(crux): Format recoverable error handling Align the CrUX 400 fallback change with the repository formatter so\nreviewers can isolate the behavior change from unrelated lint noise.\n\nRefs WEBVITALS-2S\nCo-Authored-By: Claude Co-authored-by: Sergiy Dybskiy --- ai/tools/real-world-performance.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ai/tools/real-world-performance.ts b/ai/tools/real-world-performance.ts index 1161fe4..a2ec50d 100644 --- a/ai/tools/real-world-performance.ts +++ b/ai/tools/real-world-performance.ts @@ -201,8 +201,11 @@ async function getRealWorldPerformance( const results = await Promise.allSettled(promises); // Track errors for better error messages - const errors: { device: string; error: string; recoverable: boolean }[] = - []; + const errors: { + device: string; + error: string; + recoverable: boolean; + }[] = []; const mobileData = deviceOrder.includes("mobile") ? (() => {