From 91c63be9c9691b720ef2240fdbdb04f5db20079e Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Sat, 20 Jul 2024 00:47:23 +0800 Subject: [PATCH] Surface error from fillTimeseries to the UI (#28086) * Surface error from fillTimeseries to the UI * rm consolelog --------- Co-authored-by: Jordi Enric <37541088+jordienr@users.noreply.github.com> --- .../interfaces/Home/ProjectUsage.tsx | 4 +- .../Reports/renderers/ApiRenderers.tsx | 60 ++++++-- .../interfaces/Settings/Logs/Logs.utils.ts | 2 +- .../analytics/useFillTimeseriesSorted.ts | 31 +++- .../studio/hooks/analytics/useLogsPreview.tsx | 6 +- .../[ref]/functions/[functionSlug]/index.tsx | 139 ++++++++++++------ 6 files changed, 170 insertions(+), 72 deletions(-) diff --git a/apps/studio/components/interfaces/Home/ProjectUsage.tsx b/apps/studio/components/interfaces/Home/ProjectUsage.tsx index ed2efedecb6..b872fc4657c 100644 --- a/apps/studio/components/interfaces/Home/ProjectUsage.tsx +++ b/apps/studio/components/interfaces/Home/ProjectUsage.tsx @@ -9,7 +9,7 @@ import { useParams } from 'common' import BarChart from 'components/ui/Charts/BarChart' import Panel from 'components/ui/Panel' import { UsageApiCounts, useProjectLogStatsQuery } from 'data/analytics/project-log-stats-query' -import useFillTimeseriesSorted from 'hooks/analytics/useFillTimeseriesSorted' +import { useFillTimeseriesSorted } from 'hooks/analytics/useFillTimeseriesSorted' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import type { ChartIntervals } from 'types' import { @@ -53,7 +53,7 @@ const ProjectUsage = () => { selectedInterval.startUnit as dayjs.ManipulateType ) const endDateLocal = dayjs() - const charts = useFillTimeseriesSorted( + const { data: charts } = useFillTimeseriesSorted( data?.result || [], 'timestamp', [ diff --git a/apps/studio/components/interfaces/Reports/renderers/ApiRenderers.tsx b/apps/studio/components/interfaces/Reports/renderers/ApiRenderers.tsx index a666c19fbd4..18f848a3938 100644 --- a/apps/studio/components/interfaces/Reports/renderers/ApiRenderers.tsx +++ b/apps/studio/components/interfaces/Reports/renderers/ApiRenderers.tsx @@ -9,12 +9,19 @@ import { } from 'components/interfaces/Settings/Logs/LogsFormatters' import Table from 'components/to-be-cleaned/Table' import BarChart from 'components/ui/Charts/BarChart' -import useFillTimeseriesSorted from 'hooks/analytics/useFillTimeseriesSorted' -import { Button, Collapsible } from 'ui' +import { useFillTimeseriesSorted } from 'hooks/analytics/useFillTimeseriesSorted' +import { + Alert_Shadcn_, + AlertDescription_Shadcn_, + AlertTitle_Shadcn_, + Button, + Collapsible, +} from 'ui' import { queryParamsToObject } from '../Reports.utils' import { ReportWidgetProps, ReportWidgetRendererProps } from '../ReportWidget' import AlertError from 'components/ui/AlertError' import { ResponseError } from 'types' +import { WarningIcon } from 'ui-patterns/Icons/StatusIcons' export const NetworkTrafficRenderer = ( props: ReportWidgetProps<{ @@ -23,7 +30,7 @@ export const NetworkTrafficRenderer = ( egress: number }> ) => { - const data = useFillTimeseriesSorted( + const { data, error, isError } = useFillTimeseriesSorted( props.data, 'timestamp', ['ingress_mb', 'egress_mb'], @@ -31,6 +38,7 @@ export const NetworkTrafficRenderer = ( props.params?.iso_timestamp_start, props.params?.iso_timestamp_end ) + const totalIngress = sumBy(props.data, 'ingress_mb') const totalEgress = sumBy(props.data, 'egress_mb') @@ -38,11 +46,19 @@ export const NetworkTrafficRenderer = ( return valueInMb < 0.001 ? 7 : totalIngress > 1 ? 2 : 4 } - if (props.error !== null && props.error !== undefined) { + if (!!props.error) { const error = ( typeof props.error === 'string' ? { message: props.error } : props.error ) as ResponseError return + } else if (isError) { + return ( + + + Failed to retrieve network traffic + {error.message} + + ) } return ( @@ -85,7 +101,7 @@ export const TotalRequestsChartRenderer = ( const total = props.data.reduce((acc, datum) => { return acc + datum.count }, 0) - const data = useFillTimeseriesSorted( + const { data, error, isError } = useFillTimeseriesSorted( props.data, 'timestamp', 'count', @@ -94,11 +110,19 @@ export const TotalRequestsChartRenderer = ( props.params?.iso_timestamp_end ) - if (props.error !== null && props.error !== undefined) { + if (!!props.error) { const error = ( typeof props.error === 'string' ? { message: props.error } : props.error ) as ResponseError return + } else if (isError) { + return ( + + + Failed to retrieve total requests + {error.message} + + ) } return ( @@ -225,7 +249,7 @@ export const ErrorCountsChartRenderer = ( return acc + datum.count }, 0) - const data = useFillTimeseriesSorted( + const { data, error, isError } = useFillTimeseriesSorted( props.data, 'timestamp', 'count', @@ -234,11 +258,19 @@ export const ErrorCountsChartRenderer = ( props.params?.iso_timestamp_end ) - if (props.error !== null && props.error !== undefined) { + if (!!props.error) { const error = ( typeof props.error === 'string' ? { message: props.error } : props.error ) as ResponseError return + } else if (isError) { + return ( + + + Failed to retrieve request errors + {error.message} + + ) } return ( @@ -266,7 +298,7 @@ export const ResponseSpeedChartRenderer = ( avg: datum.avg, })) - const data = useFillTimeseriesSorted( + const { data, error, isError } = useFillTimeseriesSorted( transformedData, 'timestamp', 'avg', @@ -277,11 +309,19 @@ export const ResponseSpeedChartRenderer = ( const lastAvg = props.data[props.data.length - 1]?.avg - if (props.error !== null && props.error !== undefined) { + if (!!props.error) { const error = ( typeof props.error === 'string' ? { message: props.error } : props.error ) as ResponseError return + } else if (isError) { + return ( + + + Failed to retrieve response speeds + {error.message} + + ) } return ( diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts b/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts index f7929ab01d7..3410dc3da7b 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts @@ -466,7 +466,7 @@ export const fillTimeseries = ( // Intentional throwing of error here to be caught by Sentry, as this would indicate a bug since charts shouldn't be rendering more than 10k data points if (diff > 10000) { throw new Error( - 'Data error, filling timeseries dynamically with more than 10k data points degrades performance.' + 'The selected date range will render more than 10,000 data points within the charts, which will degrade browser performance. Please select a smaller date range.' ) } diff --git a/apps/studio/hooks/analytics/useFillTimeseriesSorted.ts b/apps/studio/hooks/analytics/useFillTimeseriesSorted.ts index 1af62443c46..af81240c765 100644 --- a/apps/studio/hooks/analytics/useFillTimeseriesSorted.ts +++ b/apps/studio/hooks/analytics/useFillTimeseriesSorted.ts @@ -4,15 +4,32 @@ import { useMemo } from 'react' /** * Convenience hook for memoized filling of timeseries data. */ -const useFillTimeseriesSorted = (...args: Parameters) => { +export const useFillTimeseriesSorted = (...args: Parameters) => { return useMemo(() => { const [data, timestampKey] = args - if (!data[0]?.[timestampKey]) return data + if (!data[0]?.[timestampKey]) + return { + data, + error: undefined, + isError: false, + } - const filled = fillTimeseries(...args) - return filled.sort((a, b) => { - return (new Date(a[args[1]]) as any) - (new Date(b[args[1]]) as any) - }) + try { + const filled = fillTimeseries(...args) + + return { + data: filled.sort((a, b) => { + return (new Date(a[args[1]]) as any) - (new Date(b[args[1]]) as any) + }), + error: undefined, + isError: false, + } + } catch (error: any) { + return { + data: [], + error, + isError: true, + } + } }, [JSON.stringify(args[0]), ...args]) } -export default useFillTimeseriesSorted diff --git a/apps/studio/hooks/analytics/useLogsPreview.tsx b/apps/studio/hooks/analytics/useLogsPreview.tsx index fb1236358e9..30b14092d2d 100644 --- a/apps/studio/hooks/analytics/useLogsPreview.tsx +++ b/apps/studio/hooks/analytics/useLogsPreview.tsx @@ -24,7 +24,7 @@ import { } from 'components/interfaces/Settings/Logs/Logs.utils' import { get, isResponseOk } from 'lib/common/fetch' import { API_URL } from 'lib/constants' -import useFillTimeseriesSorted from './useFillTimeseriesSorted' +import { useFillTimeseriesSorted } from './useFillTimeseriesSorted' import useTimeseriesUnixToIso from './useTimeseriesUnixToIso' interface LogsPreviewHook { @@ -194,7 +194,7 @@ function useLogsPreview( 'timestamp' ) - const eventChartData = useFillTimeseriesSorted( + const { data: eventChartData, error: eventChartError } = useFillTimeseriesSorted( normalizedEventChartData, 'timestamp', 'count', @@ -209,7 +209,7 @@ function useLogsPreview( logData, isLoading: isLoading || isRefetching, isLoadingOlder: isFetchingNextPage, - error, + error: error || eventChartError, filters, params, oldestTimestamp: oldestTimestamp ? String(oldestTimestamp) : undefined, diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx index 4c3631dc4ad..3f57756e7a8 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx @@ -14,10 +14,11 @@ import NoPermission from 'components/ui/NoPermission' import { useFunctionsReqStatsQuery } from 'data/analytics/functions-req-stats-query' import { useFunctionsResourceUsageQuery } from 'data/analytics/functions-resource-usage-query' import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query' -import useFillTimeseriesSorted from 'hooks/analytics/useFillTimeseriesSorted' +import { useFillTimeseriesSorted } from 'hooks/analytics/useFillTimeseriesSorted' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import type { ChartIntervals, NextPageWithLayout } from 'types' -import { Button } from 'ui' +import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui' +import { WarningIcon } from 'ui-patterns/Icons/StatusIcons' const CHART_INTERVALS: ChartIntervals[] = [ { @@ -98,7 +99,11 @@ const PageLayout: NextPageWithLayout = () => { return [start, end] }, [selectedInterval]) - const execTimeChartData = useFillTimeseriesSorted( + const { + data: execTimeChartData, + error: execTimeError, + isError: isErrorExecTime, + } = useFillTimeseriesSorted( reqStatsData, 'timestamp', ['avg_execution_time'], @@ -107,7 +112,11 @@ const PageLayout: NextPageWithLayout = () => { endDate.toISOString() ) - const invocationsChartData = useFillTimeseriesSorted( + const { + data: invocationsChartData, + error: invocationsError, + isError: isErrorInvocations, + } = useFillTimeseriesSorted( reqStatsData, 'timestamp', ['count', 'success_count', 'redirect_count', 'client_err_count', 'server_err_count'], @@ -116,7 +125,11 @@ const PageLayout: NextPageWithLayout = () => { endDate.toISOString() ) - const resourceUsageChartData = useFillTimeseriesSorted( + const { + data: resourceUsageChartData, + error: resourceUsageError, + isError: isErrorResourceUsage, + } = useFillTimeseriesSorted( resourceUsageData, 'timestamp', ['avg_cpu_time_used', 'avg_memory_used'], @@ -172,7 +185,13 @@ const PageLayout: NextPageWithLayout = () => { data={execTimeChartData} isLoading={reqStatsResult.isLoading} renderer={(props) => { - return ( + return isErrorExecTime ? ( + + + Failed to reterieve execution time + {execTimeError.message} + + ) : ( { data={invocationsChartData} isLoading={reqStatsResult.isLoading} renderer={(props) => { - const data = props.data - .map((d: any) => [ - { - status: '2xx', - count: d.success_count, - timestamp: d.timestamp, - }, - { - status: '3xx', - count: d.redirect_count, - timestamp: d.timestamp, - }, - { - status: '4xx', - count: d.client_err_count, - timestamp: d.timestamp, - }, - { - status: '5xx', - count: d.server_err_count, - timestamp: d.timestamp, - }, - ]) - .flat() + if (isErrorInvocations) { + return ( + + + Failed to reterieve invocations + {invocationsError.message} + + ) + } else { + const data = props.data + .map((d: any) => [ + { + status: '2xx', + count: d.success_count, + timestamp: d.timestamp, + }, + { + status: '3xx', + count: d.redirect_count, + timestamp: d.timestamp, + }, + { + status: '4xx', + count: d.client_err_count, + timestamp: d.timestamp, + }, + { + status: '5xx', + count: d.server_err_count, + timestamp: d.timestamp, + }, + ]) + .flat() - return ( - { - router.push( - `/project/${projectRef}/functions/${functionSlug}/invocations?its=${startDate.toISOString()}` - ) - }} - /> - ) + return ( + { + router.push( + `/project/${projectRef}/functions/${functionSlug}/invocations?its=${startDate.toISOString()}` + ) + }} + /> + ) + } }} /> { data={resourceUsageChartData} isLoading={resourceUsageResult.isLoading} renderer={(props) => { - return ( + return isErrorResourceUsage ? ( + + + Failed to retrieve CPU time + {resourceUsageError.message} + + ) : ( { data={resourceUsageChartData} isLoading={resourceUsageResult.isLoading} renderer={(props) => { - return ( + return isErrorResourceUsage ? ( + + + Failed to retrieve memory usage + {resourceUsageError.message} + + ) : (