From f4691d5ec16d9be0aba27cd350a16ad8433ac394 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Wed, 23 Sep 2026 00:14:28 -0700 Subject: [PATCH] perf(api,desktop,host-service): cut api traffic that no one needed The api is 93% of a ~$5.3k/month Vercel bill, and most of its requests turned out to be avoidable: - proxy.ts ran on every request only to add CORS headers, billing each request as two invocations. Its matcher now covers just the paths a browser fetches (tRPC, auth minus jwks/oauth2/token, desktop version, Linear image proxy); webhooks and server callers skip it. - host-service retried a refresh token the api had already rejected on every call it made, so a host with a dead login hammered /oauth2/token forever (every sampled response was 400 or 429). A rejected token is now not sent again until the config holds a new one. - /api/auth/jwks is now CDN-cacheable for five minutes. The relay's Workers isolates refetched it constantly; the key never rotates. - getByBranches polled every 30s from every desktop, with an empty ref list for everyone without cloud workspaces. - The sidebar automations failure badge polled latestRuns + list every two minutes and on every focus, in every window. The badge is gone; the paywall decision fetches the list once on click, the getting started card stops polling, and the hook moves next to the page, its only user. The badge's seen-failures store is deleted and its key added to DEAD_KEYS. --- apps/api/src/app/api/auth/[...all]/route.ts | 10 ++ apps/api/src/proxy.ts | 6 +- .../persisted-key-registry.test-data.ts | 4 - .../lib/persisted-keys/persisted-keys.ts | 2 + .../AutomationRow/AutomationRow.tsx | 2 +- .../hooks/useFailedAutomations/index.ts | 0 .../useFailedAutomations.ts | 53 ++++++++ .../_dashboard/automations/page.tsx | 10 +- .../DashboardSidebarHeader.tsx | 56 +++------ .../useGettingStartedCard.tsx | 1 - .../useSidebarCloudPullRequests.ts | 2 +- .../useFailedAutomations.ts | 119 ------------------ .../stores/automation-failures/index.ts | 4 - .../stores/automation-failures/store.test.ts | 30 ----- .../stores/automation-failures/store.ts | 40 ------ .../ConfigFileSessionTokenSource.test.ts | 62 +++++++++ .../ConfigFileSessionTokenSource.ts | 8 +- packages/i18n/locales/cs/messages.po | 9 -- packages/i18n/locales/de/messages.po | 9 -- packages/i18n/locales/en/messages.po | 9 -- packages/i18n/locales/es/messages.po | 9 -- packages/i18n/locales/fr/messages.po | 9 -- packages/i18n/locales/id/messages.po | 9 -- packages/i18n/locales/it/messages.po | 9 -- packages/i18n/locales/ja/messages.po | 9 -- packages/i18n/locales/ko/messages.po | 9 -- packages/i18n/locales/nl/messages.po | 9 -- packages/i18n/locales/pl/messages.po | 9 -- packages/i18n/locales/pt-BR/messages.po | 9 -- packages/i18n/locales/ru/messages.po | 9 -- packages/i18n/locales/tr/messages.po | 9 -- packages/i18n/locales/vi/messages.po | 9 -- packages/i18n/locales/zh-CN/messages.po | 9 -- packages/i18n/locales/zh-TW/messages.po | 9 -- 34 files changed, 160 insertions(+), 402 deletions(-) rename apps/desktop/src/renderer/routes/_authenticated/_dashboard/{ => automations}/hooks/useFailedAutomations/index.ts (100%) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/hooks/useFailedAutomations/useFailedAutomations.ts delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/hooks/useFailedAutomations/useFailedAutomations.ts delete mode 100644 apps/desktop/src/renderer/stores/automation-failures/index.ts delete mode 100644 apps/desktop/src/renderer/stores/automation-failures/store.test.ts delete mode 100644 apps/desktop/src/renderer/stores/automation-failures/store.ts diff --git a/apps/api/src/app/api/auth/[...all]/route.ts b/apps/api/src/app/api/auth/[...all]/route.ts index f63970478c4..db83c220b2c 100644 --- a/apps/api/src/app/api/auth/[...all]/route.ts +++ b/apps/api/src/app/api/auth/[...all]/route.ts @@ -25,6 +25,16 @@ const GET = async (req: Request) => { } } } + if (url.pathname.endsWith("/jwks")) { + const response = await _GET(req); + if (!response.ok) return response; + const headers = new Headers(response.headers); + headers.set( + "Cache-Control", + "public, s-maxage=300, stale-while-revalidate=3600", + ); + return new Response(response.body, { status: response.status, headers }); + } return _GET(req); }; diff --git a/apps/api/src/proxy.ts b/apps/api/src/proxy.ts index b53ecfcefae..8e864e8de9b 100644 --- a/apps/api/src/proxy.ts +++ b/apps/api/src/proxy.ts @@ -57,7 +57,9 @@ export default function proxy(req: NextRequest) { export const config = { matcher: [ - "/((?!_next|ingest|monitoring|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", - "/(api|trpc)(.*)", + "/api/trpc/(.*)", + "/api/auth/((?!jwks$|oauth2/token$).*)", + "/api/desktop/version", + "/api/proxy/linear-image", ], }; diff --git a/apps/desktop/src/renderer/lib/persisted-keys/persisted-key-registry.test-data.ts b/apps/desktop/src/renderer/lib/persisted-keys/persisted-key-registry.test-data.ts index 57732d12969..a45ef7d1a02 100644 --- a/apps/desktop/src/renderer/lib/persisted-keys/persisted-key-registry.test-data.ts +++ b/apps/desktop/src/renderer/lib/persisted-keys/persisted-key-registry.test-data.ts @@ -108,10 +108,6 @@ export const PERSISTED_KEY_REGISTRY: ReadonlyArray< "src/renderer/stores/terminal-close-confirm/store.ts", ["terminal-close-confirm-v1"], ], - [ - "src/renderer/stores/automation-failures/store.ts", - ["automation-failures-v1"], - ], [ "src/renderer/stores/app-version-history/store.ts", ["app-version-history-v1"], diff --git a/apps/desktop/src/renderer/lib/persisted-keys/persisted-keys.ts b/apps/desktop/src/renderer/lib/persisted-keys/persisted-keys.ts index 9a872efb382..7cddf91c844 100644 --- a/apps/desktop/src/renderer/lib/persisted-keys/persisted-keys.ts +++ b/apps/desktop/src/renderer/lib/persisted-keys/persisted-keys.ts @@ -42,6 +42,8 @@ export const DEAD_KEYS: DeadKey[] = [ { key: "leaderboard-auto-publish-v1", match: "exact" }, // Launch-time "join the leaderboard" prompt removed; joining lives in settings { key: "leaderboard-asked-v1", match: "exact" }, + // Sidebar automations failure badge removed; it polled for every window + { key: "automation-failures-v1", match: "exact" }, ]; function matchesDeadKey(key: string): boolean { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/components/AutomationRow/AutomationRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/components/AutomationRow/AutomationRow.tsx index 886e27c5228..66b6084b31a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/components/AutomationRow/AutomationRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/components/AutomationRow/AutomationRow.tsx @@ -26,10 +26,10 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { cn } from "@superset/ui/utils"; import { useNavigate } from "@tanstack/react-router"; import { LuEllipsis, LuPlay, LuRotateCw } from "react-icons/lu"; -import type { AutomationLastRun } from "renderer/routes/_authenticated/_dashboard/hooks/useFailedAutomations"; import type { ProjectOption } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/types"; import { ProjectThumbnail } from "renderer/routes/_authenticated/components/ProjectThumbnail"; import { useCopyAutomationLink } from "../../hooks/useCopyAutomationLink"; +import type { AutomationLastRun } from "../../hooks/useFailedAutomations"; import { AutomationActionsMenuItems } from "./components/AutomationActionsMenuItems"; type AutomationListItem = RouterOutputs["automation"]["list"][number]; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/hooks/useFailedAutomations/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/hooks/useFailedAutomations/index.ts similarity index 100% rename from apps/desktop/src/renderer/routes/_authenticated/_dashboard/hooks/useFailedAutomations/index.ts rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/hooks/useFailedAutomations/index.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/hooks/useFailedAutomations/useFailedAutomations.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/hooks/useFailedAutomations/useFailedAutomations.ts new file mode 100644 index 00000000000..352e9c73cf6 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/hooks/useFailedAutomations/useFailedAutomations.ts @@ -0,0 +1,53 @@ +import type { SelectAutomationRun } from "@superset/db/schema"; +import { useMemo } from "react"; +import { cloudTrpc } from "renderer/lib/cloud-trpc"; + +const LATEST_RUNS_POLL_MS = 120_000; + +const FAILED_STATUSES: SelectAutomationRun["status"][] = [ + "skipped_offline", + "dispatch_failed", +]; + +export interface AutomationLastRun { + status: SelectAutomationRun["status"]; + /** createdAt as epoch ms; NaN-free (unparseable rows are dropped). */ + at: number; + v2WorkspaceId: string | null; + chatSessionId: string | null; + terminalSessionId: string | null; +} + +interface FailedAutomations { + /** Most recent run per automation, with its workspace/session links. */ + lastRunById: Map; + /** Automations whose most recent run failed. */ + failedIds: Set; +} + +export function useFailedAutomations(): FailedAutomations { + const { data: runRows = [] } = cloudTrpc.automation.latestRuns.useQuery( + undefined, + { refetchInterval: LATEST_RUNS_POLL_MS, staleTime: 30_000 }, + ); + + return useMemo(() => { + const lastRunById = new Map(); + for (const run of runRows) { + const at = new Date(run.createdAt).getTime(); + if (!Number.isFinite(at)) continue; + lastRunById.set(run.automationId, { + status: run.status, + at, + v2WorkspaceId: run.v2WorkspaceId ?? null, + chatSessionId: run.chatSessionId ?? null, + terminalSessionId: run.terminalSessionId ?? null, + }); + } + const failedIds = new Set(); + for (const [id, run] of lastRunById) { + if (FAILED_STATUSES.includes(run.status)) failedIds.add(id); + } + return { lastRunById, failedIds }; + }, [runRows]); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx index 9b44a680c03..74c1ea7c0bf 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx @@ -62,13 +62,13 @@ import { SortableHeader, type SortDirection, } from "renderer/routes/_authenticated/_dashboard/components/SortableHeader"; -import { useFailedAutomations } from "renderer/routes/_authenticated/_dashboard/hooks/useFailedAutomations"; import { AGENT_STORAGE_KEY } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/types"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; import { AutomationRow } from "./components/AutomationRow"; import { AutomationStatCards } from "./components/AutomationStatCards"; import { AutomationsEmptyState } from "./components/AutomationsEmptyState"; import { HostOfflineRunDialog } from "./components/HostOfflineRunDialog"; +import { useFailedAutomations } from "./hooks/useFailedAutomations"; import type { AutomationTemplate } from "./templates"; import { matchAgentChoice, portableAgentValue } from "./utils/agentIdentity"; import { dispatchErrorCode, runErrorHelp } from "./utils/runErrorHelp"; @@ -301,15 +301,9 @@ function AutomationsPage() { undefined, {}, ); - const { lastRunById, failedIds, markMyFailuresSeen } = useFailedAutomations(); + const { lastRunById, failedIds } = useFailedAutomations(); const now = useNow(30_000); - // Opening the page clears the sidebar failure badge; failures that sync in - // while it stays open are marked seen too, until a newer run fails. - useEffect(() => { - markMyFailuresSeen(); - }, [markMyFailuresSeen]); - const recentProjects = useRecentProjects(); const usersById = useMemo( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarHeader/DashboardSidebarHeader.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarHeader/DashboardSidebarHeader.tsx index ccba2220811..e5d53d55982 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarHeader/DashboardSidebarHeader.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarHeader/DashboardSidebarHeader.tsx @@ -40,13 +40,13 @@ import { } from "renderer/hooks/useOpenNewWorkspace"; import { useZoomFactor } from "renderer/hooks/useZoomFactor"; import { useHotkeyDisplay } from "renderer/hotkeys"; +import { cloudTrpc } from "renderer/lib/cloud-trpc"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { useFolderFirstImport } from "renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/hooks/useFolderFirstImport"; import { AppMenuButton } from "renderer/routes/_authenticated/_dashboard/components/AppMenuButton"; import { NavigationControls } from "renderer/routes/_authenticated/_dashboard/components/NavigationControls"; import { SidebarToggle } from "renderer/routes/_authenticated/_dashboard/components/SidebarToggle"; import { TopBarPortsDropdown } from "renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown"; -import { useFailedAutomations } from "renderer/routes/_authenticated/_dashboard/hooks/useFailedAutomations"; import { pullRequestsSearchFromFilters, usePullRequestsFilterStore, @@ -140,7 +140,7 @@ export function DashboardSidebarHeader({ const isMac = platform === undefined || platform === "darwin"; const zoomFactor = useZoomFactor(); const matchRoute = useMatchRoute(); - const { gateFeature } = usePaywall(); + const { gateFeature, hasAccess } = usePaywall(); const isWorkspacesListOpen = !!matchRoute({ to: "/v2-workspaces" }); const v2WorkspaceMatch = matchRoute({ to: "/v2-workspace/$workspaceId", @@ -169,8 +169,7 @@ export function DashboardSidebarHeader({ const isPluginsEnabled = (useFeatureFlagEnabled(FEATURE_FLAGS.PLUGINS) ?? false) || env.NODE_ENV === "development"; - const { myFailedCount, hasAutomations, automationsPending } = - useFailedAutomations(); + const cloudUtils = cloudTrpc.useUtils(); const { tab: lastTab, @@ -197,11 +196,18 @@ export function DashboardSidebarHeader({ // Automations are Pro, but an org that already has some (a downgrade) can // still reach the list to pause, edit, or delete them; the page gates the // actions that need the plan. A Free org with none meets the paywall here. - // While the list is still loading the answer is unknown, so let the click + // If the list can't be read the answer is unknown, so let the click // through: an empty list page gates every action itself, and a wrong // paywall on a downgraded org would be the worse mistake. - const handleAutomationsClick = () => { - if (hasAutomations || automationsPending) { + const handleAutomationsClick = async () => { + if (hasAccess(GATED_FEATURES.AUTOMATIONS)) { + navigate({ to: "/automations" }); + return; + } + const automations = await cloudUtils.automation.list + .fetch() + .catch(() => null); + if (automations === null || automations.length > 0) { navigate({ to: "/automations" }); return; } @@ -342,37 +348,21 @@ export function DashboardSidebarHeader({ - {myFailedCount > 0 ? ( - Automations ({myFailedCount} failing) - ) : ( - Automations - )} + Automations @@ -648,16 +638,6 @@ export function DashboardSidebarHeader({ Automations - {myFailedCount > 0 && ( - - {myFailedCount > 9 ? "9+" : myFailedCount} - - )}