From 3fcf980b0a71afbb3a3be1a28a92a4b794754f60 Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Thu, 2 Jul 2026 02:38:17 -0600 Subject: [PATCH] fix(studio): batch of production Sentry crash fixes (array/null guards) (#47460) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a batch of production Studio crashes from Sentry (all caught by the global error boundary). Most are missing array/null guards where an endpoint typed as an array — or with a nested array field — returned a non-array body in production; a few are one-off render crashes. Resolves FE-3748. ## Issues fixed | Sentry | Error | Fix | | --- | --- | --- | | [J7R](https://supabase.sentry.io/issues/7492997940/) | Maximum update depth exceeded | Disable RadialBar animation in disk-cooldown countdown | | [JR5](https://supabase.sentry.io/issues/7548484681/) | resourceWarnings.find is not a function | Guard in ResourceExhaustionWarningBanner | | [JCJ](https://supabase.sentry.io/issues/7506024989/) | resourceWarnings.find is not a function | Guard in ProjectLayout + normalize query | | [K1Y](https://supabase.sentry.io/issues/7584792331/) | snippet.name on undefined | Optional-chain SQL editor download filename | | [B3K](https://supabase.sentry.io/issues/7141649636/) | pagination.count on undefined | Guard pagination in projects infinite query | | [JVP](https://supabase.sentry.io/issues/7560437621/) | schemas.some / extensions.find | Coerce pg-meta lists to arrays in useInstalledIntegrations | | [JR2](https://supabase.sentry.io/issues/7548339272/) | extensions.find is not a function | (same fix as JVP) | | [JQR](https://supabase.sentry.io/issues/7547163939/) | lints.filter is not a function | Normalize project lints query | | [JR3](https://supabase.sentry.io/issues/7548433501/) | entitlements.find is not a function | Guard call sites + normalize entitlements query | | [JQS](https://supabase.sentry.io/issues/7547557098/) | selected_addons.find is not a function | Normalize addons query arrays | ## Summary by CodeRabbit * **Bug Fixes** * Improved stability across several Studio screens by handling missing or unexpected data more safely. * Downloads now use a fallback name when a snippet name isn’t available. * Project, entitlement, schema, addon, warning, and extension views are less likely to break when data is missing or not in the expected format. * Pagination and countdown visuals now behave more consistently, with reduced chance of runtime errors or animation-related glitches. --------- Co-authored-by: Ivan Vasilov --- .../Integrations/Landing/useInstalledIntegrations.tsx | 2 +- .../interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx | 2 +- apps/studio/components/layouts/ProjectLayout/index.tsx | 2 +- .../components/ui/CountdownTimer/CountdownTimerRadial.tsx | 4 ++-- .../data/database-extensions/database-extensions-query.ts | 3 ++- apps/studio/data/database/schemas-query.ts | 3 ++- apps/studio/data/entitlements/entitlements-query.ts | 6 +++++- apps/studio/data/fdw/fdws-query.ts | 3 ++- apps/studio/data/lint/lint-query.ts | 3 ++- apps/studio/data/projects/projects-infinite-query.ts | 4 ++-- apps/studio/data/subscriptions/project-addons-query.ts | 7 ++++++- apps/studio/data/usage/resource-warnings-query.ts | 3 ++- apps/studio/hooks/misc/useCheckEntitlements.ts | 4 ++-- 13 files changed, 30 insertions(+), 16 deletions(-) diff --git a/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx b/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx index e17ce5e617c..32359359859 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx @@ -69,7 +69,7 @@ export const useInstalledIntegrations = () => { connectionString: project?.connectionString, }) - const isHooksEnabled = schemas?.some((schema) => schema.name === 'supabase_functions') + const isHooksEnabled = schemas.some((schema) => schema.name === 'supabase_functions') const installedIntegrations = useMemo(() => { return allIntegrations diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx index 98af2875443..66b536def8e 100644 --- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx @@ -153,7 +153,7 @@ export const UtilityPanel = ({ track('sql_editor_result_download_csv_clicked')} onCopyAsMarkdown={() => track('sql_editor_result_copy_markdown_clicked')} onCopyAsJSON={() => track('sql_editor_result_copy_json_clicked')} diff --git a/apps/studio/components/layouts/ProjectLayout/index.tsx b/apps/studio/components/layouts/ProjectLayout/index.tsx index 55bc499d8d6..c083583c8fb 100644 --- a/apps/studio/components/layouts/ProjectLayout/index.tsx +++ b/apps/studio/components/layouts/ProjectLayout/index.tsx @@ -142,7 +142,7 @@ export const ProjectLayout = forwardRef w.project === selectedProject?.ref ) const isComputeNearExhaustion = diff --git a/apps/studio/components/ui/CountdownTimer/CountdownTimerRadial.tsx b/apps/studio/components/ui/CountdownTimer/CountdownTimerRadial.tsx index 487d36c618e..dc91a0ebc9a 100644 --- a/apps/studio/components/ui/CountdownTimer/CountdownTimerRadial.tsx +++ b/apps/studio/components/ui/CountdownTimer/CountdownTimerRadial.tsx @@ -35,7 +35,7 @@ const CountdownTimerRadial = ({ progress }: CountdownTimerRadialProps) => { className="first:fill-foreground-muted/50 last:fill-background-200" polarRadius={[16, 11]} /> - + @@ -48,7 +48,7 @@ const CountdownTimerRadial = ({ progress }: CountdownTimerRadialProps) => { innerRadius={14} outerRadius={5} > - + diff --git a/apps/studio/data/database-extensions/database-extensions-query.ts b/apps/studio/data/database-extensions/database-extensions-query.ts index 5e293a0dcfe..4de92df2958 100644 --- a/apps/studio/data/database-extensions/database-extensions-query.ts +++ b/apps/studio/data/database-extensions/database-extensions-query.ts @@ -6,6 +6,7 @@ import { databaseExtensionsKeys } from './keys' import { executeSql } from '@/data/sql/execute-sql-mutation' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { PROJECT_STATUS } from '@/lib/constants' +import { EMPTY_ARR } from '@/lib/void' import type { ResponseError, UseCustomQueryOptions } from '@/types' export type DatabaseExtension = components['schemas']['PostgresExtension'] & { @@ -26,7 +27,7 @@ export async function getDatabaseExtensions( { projectRef, connectionString, sql, queryKey: ['database-extensions'] }, signal ) - return result as DatabaseExtension[] + return (Array.isArray(result) ? result : EMPTY_ARR) as DatabaseExtension[] } export type DatabaseExtensionsData = Awaited> diff --git a/apps/studio/data/database/schemas-query.ts b/apps/studio/data/database/schemas-query.ts index 9cdced293f7..fe2a816fdcf 100644 --- a/apps/studio/data/database/schemas-query.ts +++ b/apps/studio/data/database/schemas-query.ts @@ -4,6 +4,7 @@ import { z } from 'zod' import { databaseKeys } from './keys' import { executeSql } from '@/data/sql/execute-sql-mutation' +import { EMPTY_ARR } from '@/lib/void' import { ResponseError, UseCustomQueryOptions } from '@/types' export type SchemasVariables = { @@ -32,7 +33,7 @@ export async function getSchemas( signal ) - return result + return Array.isArray(result) ? result : EMPTY_ARR } export const useSchemasQuery = ( diff --git a/apps/studio/data/entitlements/entitlements-query.ts b/apps/studio/data/entitlements/entitlements-query.ts index 832ad627abe..697fdcdbb46 100644 --- a/apps/studio/data/entitlements/entitlements-query.ts +++ b/apps/studio/data/entitlements/entitlements-query.ts @@ -3,6 +3,7 @@ import type { components } from 'api-types' import { get, handleError } from '@/data/fetchers' import { organizationKeys } from '@/data/organizations/keys' +import { EMPTY_ARR } from '@/lib/void' import { UseCustomQueryOptions } from '@/types' import { ResponseError } from '@/types/base' @@ -32,7 +33,10 @@ export async function getEntitlements( }) if (error) handleError(error) - return data + return { + ...data, + entitlements: Array.isArray(data?.entitlements) ? data.entitlements : EMPTY_ARR, + } } export type EntitlementsData = Awaited> diff --git a/apps/studio/data/fdw/fdws-query.ts b/apps/studio/data/fdw/fdws-query.ts index 66005773fb8..b081cbfd78b 100644 --- a/apps/studio/data/fdw/fdws-query.ts +++ b/apps/studio/data/fdw/fdws-query.ts @@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query' import { fdwKeys } from './keys' import { executeSql } from '@/data/sql/execute-sql-mutation' +import { EMPTY_ARR } from '@/lib/void' import { ResponseError, UseCustomQueryOptions } from '@/types' export type FDWColumn = { @@ -43,7 +44,7 @@ export async function getFDWs( signal ) - return result as FDW[] + return (Array.isArray(result) ? result : EMPTY_ARR) as FDW[] } export type FDWsData = Awaited> diff --git a/apps/studio/data/lint/lint-query.ts b/apps/studio/data/lint/lint-query.ts index 06280a23079..44c7db0c0f6 100644 --- a/apps/studio/data/lint/lint-query.ts +++ b/apps/studio/data/lint/lint-query.ts @@ -5,6 +5,7 @@ import { lintKeys } from './keys' import { get, handleError } from '@/data/fetchers' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { PROJECT_STATUS } from '@/lib/constants' +import { EMPTY_ARR } from '@/lib/void' import type { ResponseError, UseCustomQueryOptions } from '@/types' type ProjectLintsVariables = { @@ -24,7 +25,7 @@ export async function getProjectLints({ projectRef }: ProjectLintsVariables, sig if (error) handleError(error) - return data + return Array.isArray(data) ? data : EMPTY_ARR } export type ProjectLintsData = Awaited> diff --git a/apps/studio/data/projects/projects-infinite-query.ts b/apps/studio/data/projects/projects-infinite-query.ts index fe114fa911e..0e3b4dfcaf5 100644 --- a/apps/studio/data/projects/projects-infinite-query.ts +++ b/apps/studio/data/projects/projects-infinite-query.ts @@ -70,9 +70,9 @@ export const useProjectsInfiniteQuery = ( const page = pages.length const currentTotalCount = page * limit // @ts-ignore [Joshen] API type issue for Version 2 endpoints - const totalCount = lastPage.pagination.count + const totalCount = lastPage?.pagination?.count - if (currentTotalCount >= totalCount) return undefined + if (totalCount === undefined || currentTotalCount >= totalCount) return undefined return page }, ...options, diff --git a/apps/studio/data/subscriptions/project-addons-query.ts b/apps/studio/data/subscriptions/project-addons-query.ts index 5f98d4c310e..28a7b7cd25c 100644 --- a/apps/studio/data/subscriptions/project-addons-query.ts +++ b/apps/studio/data/subscriptions/project-addons-query.ts @@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query' import { subscriptionKeys } from './keys' import { get, handleError } from '@/data/fetchers' import { IS_PLATFORM } from '@/lib/constants' +import { EMPTY_ARR } from '@/lib/void' import type { ResponseError, UseCustomQueryOptions } from '@/types' export type ProjectAddonsVariables = { @@ -24,7 +25,11 @@ export async function getProjectAddons( }) if (error) handleError(error) - return data + return { + ...data, + selected_addons: Array.isArray(data?.selected_addons) ? data.selected_addons : EMPTY_ARR, + available_addons: Array.isArray(data?.available_addons) ? data.available_addons : EMPTY_ARR, + } } export type ProjectAddonsData = Awaited> diff --git a/apps/studio/data/usage/resource-warnings-query.ts b/apps/studio/data/usage/resource-warnings-query.ts index df7385ea546..73ef9ca1565 100644 --- a/apps/studio/data/usage/resource-warnings-query.ts +++ b/apps/studio/data/usage/resource-warnings-query.ts @@ -4,6 +4,7 @@ import { IS_PLATFORM } from 'common' import { usageKeys } from './keys' import type { components } from '@/data/api' import { get, handleError } from '@/data/fetchers' +import { EMPTY_ARR } from '@/lib/void' import type { ResponseError, UseCustomQueryOptions } from '@/types' export type ResourceWarningsVariables = { @@ -26,7 +27,7 @@ export async function getResourceWarnings( }) if (error) handleError(error) - return data + return Array.isArray(data) ? data : EMPTY_ARR } export type ResourceWarning = components['schemas']['ProjectResourceWarningsResponse'] diff --git a/apps/studio/hooks/misc/useCheckEntitlements.ts b/apps/studio/hooks/misc/useCheckEntitlements.ts index 9d4765c7dbf..5e749b3c7cd 100644 --- a/apps/studio/hooks/misc/useCheckEntitlements.ts +++ b/apps/studio/hooks/misc/useCheckEntitlements.ts @@ -69,7 +69,7 @@ export function useHasEntitlementAccess(organizationSlug?: string) { return useCallback( (key: string) => IS_PLATFORM - ? (entitlementsData?.entitlements.find((e) => e.feature.key === key)?.hasAccess ?? false) + ? (entitlementsData?.entitlements?.find((e) => e.feature.key === key)?.hasAccess ?? false) : true, [entitlementsData] ) @@ -107,7 +107,7 @@ export function useCheckEntitlements( // If no organization slug, no access if (!finalOrgSlug) return { entitlement: null } - const entitlement = entitlementsData?.entitlements.find( + const entitlement = entitlementsData?.entitlements?.find( (entitlement) => entitlement.feature.key === featureKey )