Skip to content
Draft
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
83 changes: 60 additions & 23 deletions ai/tools/real-world-performance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}`,
);
}
Expand Down Expand Up @@ -188,7 +201,11 @@ 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")
? (() => {
Expand All @@ -205,19 +222,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;
Expand All @@ -237,19 +264,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;
Expand Down Expand Up @@ -310,11 +347,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))
Expand Down
Loading