Files
supabase/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.tsx
T
Saxon FletcherandAli Waseem e21088144d Edge function overview (#44009)
<img width="1575" height="1134" alt="image"
src="https://github.com/user-attachments/assets/994b1113-717f-44a2-89a4-13bc0182db20"
/>

Attempts to improve our edge function overview pages to provide stronger
insights into the health of a function, including reliability (error
rates), performance (execution times) and usage (cpu and memory).

As part of this work it refactors existing charts to use our new chart
components.

main consideration is the collective performance of error queries
https://github.com/supabase/supabase/pull/44009/changes#diff-2a79cf61c5397a8ef363c333229fa7729a2efc90a4d8e0806e49c212d5aa97e7

## To test:
1. Create an edge function that errors out randomly across requests. You
can use cron to poll this function every second.
2. View the edge function and on the overview page confirm that errors
are showing and grouped correctly in recent failed invocations sections.

---------

Co-authored-by: Ali Waseem <waseema393@gmail.com>
2026-04-01 14:59:12 +10:00

238 lines
7.7 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { IS_PLATFORM, useParams } from 'common'
import { useUnifiedLogsPreview } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import {
EDGE_FUNCTION_CHART_INTERVALS,
getBucketedTimeRange,
getExecutionMetrics,
getInvocationChartData,
getInvocationTotals,
getInvocationUpdateAnnotation,
getRollingTimeRange,
getUsageMetrics,
toEdgeFunctionChartData,
} from './EdgeFunctionOverview.utils'
import type { EdgeFunctionChartRawDatum } from './EdgeFunctionOverview.utils'
import NoPermission from 'components/ui/NoPermission'
import {
FunctionsCombinedStatsVariables,
useFunctionsCombinedStatsQuery,
} from 'data/analytics/functions-combined-stats-query'
import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query'
import { useFillTimeseriesSorted } from 'hooks/analytics/useFillTimeseriesSorted'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { ExternalLink } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
import { EdgeFunctionInvocationsSection } from './EdgeFunctionInvocationsSection'
import { EdgeFunctionPerformanceSection } from './EdgeFunctionPerformanceSection'
import { EdgeFunctionRecentErrors } from './EdgeFunctionRecentErrors'
import { EdgeFunctionUsageSection } from './EdgeFunctionUsageSection'
export const EdgeFunctionOverview = () => {
const router = useRouter()
const { ref: projectRef, functionSlug } = useParams()
const { isEnabled: isUnifiedLogsEnabled } = useUnifiedLogsPreview()
const [interval, setInterval] = useState<string>('15min')
const selectedInterval =
EDGE_FUNCTION_CHART_INTERVALS.find((item) => item.key === interval) ||
EDGE_FUNCTION_CHART_INTERVALS[1]
const {
data: selectedFunction,
error: functionError,
isPending: isLoadingFunction,
isError: isErrorFunction,
} = useEdgeFunctionQuery({
projectRef,
slug: functionSlug,
})
const id = selectedFunction?.id
const combinedStatsResults = useFunctionsCombinedStatsQuery(
{
projectRef,
functionId: id,
interval: selectedInterval.key as FunctionsCombinedStatsVariables['interval'],
},
{
enabled: IS_PLATFORM,
}
)
const combinedStatsData = useMemo(
() => (combinedStatsResults.data?.result as EdgeFunctionChartRawDatum[] | undefined) || [],
[combinedStatsResults.data]
)
const [startDate, endDate] = useMemo(
() => getBucketedTimeRange(selectedInterval),
[selectedInterval]
)
const [selectedWindowStart, selectedWindowEnd] = useMemo(
() => getRollingTimeRange(selectedInterval),
[selectedInterval]
)
const dateTimeFormat = selectedInterval.format ?? 'MMM D, h:mma'
const {
data: combinedStatsChartData,
error: combinedStatsError,
isError: isErrorCombinedStats,
} = useFillTimeseriesSorted({
data: combinedStatsData,
timestampKey: 'timestamp',
valueKey: [
'requests_count',
'log_count',
'log_info_count',
'log_warn_count',
'log_error_count',
'success_count',
'redirect_count',
'client_err_count',
'server_err_count',
'avg_cpu_time_used',
'avg_memory_used',
'avg_execution_time',
'max_execution_time',
'avg_heap_memory_used',
'avg_external_memory_used',
'max_cpu_time_used',
],
defaultValue: 0,
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
})
const chartData = useMemo(
() => toEdgeFunctionChartData(combinedStatsChartData),
[combinedStatsChartData]
)
const invocationChartData = useMemo(() => getInvocationChartData(chartData), [chartData])
const { totalInvocationCount, totalWarningCount, totalErrorCount } = useMemo(
() => getInvocationTotals(invocationChartData),
[invocationChartData]
)
const { averageExecutionTime, maxExecutionTime } = useMemo(
() => getExecutionMetrics(chartData),
[chartData]
)
const {
averageCpuTime,
maxCpuTime,
averageMemoryUsage,
totalHeapMemory,
totalExternalMemory,
totalMemoryByType,
} = useMemo(() => getUsageMetrics(chartData), [chartData])
const invocationUpdateAnnotation = useMemo(
() =>
getInvocationUpdateAnnotation({
updatedAt:
selectedFunction?.updated_at === undefined
? undefined
: String(selectedFunction.updated_at),
invocationChartData,
windowStart: selectedWindowStart,
windowEnd: selectedWindowEnd,
}),
[invocationChartData, selectedFunction?.updated_at, selectedWindowEnd, selectedWindowStart]
)
const invocationActions = useMemo(
() => [
{
label: isUnifiedLogsEnabled ? 'Open logs' : 'Open invocations',
href: `/project/${projectRef}/functions/${functionSlug}/${
isUnifiedLogsEnabled ? 'logs' : 'invocations'
}`,
icon: <ExternalLink size={12} />,
},
],
[functionSlug, isUnifiedLogsEnabled, projectRef]
)
const { isLoading: permissionsLoading, can: canReadFunction } = useAsyncCheckPermissions(
PermissionAction.FUNCTIONS_READ,
functionSlug as string
)
useEffect(() => {
if (!IS_PLATFORM && projectRef && functionSlug) {
router.replace(`/project/${projectRef}/functions/${functionSlug}/details`)
}
}, [functionSlug, projectRef, router])
if (!canReadFunction && !permissionsLoading) {
return <NoPermission isFullPage resourceText="access this edge function" />
}
if (!IS_PLATFORM) {
return null
}
return (
<>
<EdgeFunctionInvocationsSection
interval={interval}
onIntervalChange={setInterval}
selectedInterval={selectedInterval}
actions={invocationActions}
totalInvocationCount={totalInvocationCount}
totalErrorCount={totalErrorCount}
totalWarningCount={totalWarningCount}
isLoadingFunction={isLoadingFunction}
isErrorFunction={isErrorFunction}
functionError={functionError}
isLoadingChart={combinedStatsResults.isLoading}
isErrorChart={isErrorCombinedStats}
chartErrorMessage={combinedStatsError?.message ?? 'Unknown error'}
chartData={invocationChartData}
onChartClick={() => {
router.push(
`/project/${projectRef}/functions/${functionSlug}/${
isUnifiedLogsEnabled ? 'logs' : 'invocations'
}${isUnifiedLogsEnabled ? '' : `?its=${startDate.toISOString()}`}`
)
}}
updateAnnotation={invocationUpdateAnnotation}
/>
<EdgeFunctionRecentErrors
functionId={id}
functionSlug={functionSlug as string}
projectRef={projectRef as string}
isoTimestampStart={selectedWindowStart.toISOString()}
isoTimestampEnd={selectedWindowEnd.toISOString()}
/>
<EdgeFunctionPerformanceSection
data={chartData}
dateTimeFormat={dateTimeFormat}
isLoading={combinedStatsResults.isLoading}
isError={isErrorCombinedStats}
errorMessage={combinedStatsError?.message ?? 'Unknown error'}
averageExecutionTime={averageExecutionTime}
maxExecutionTime={maxExecutionTime}
/>
<EdgeFunctionUsageSection
data={chartData}
dateTimeFormat={dateTimeFormat}
isLoading={combinedStatsResults.isLoading}
isError={isErrorCombinedStats}
errorMessage={combinedStatsError?.message ?? 'Unknown error'}
averageCpuTime={averageCpuTime}
maxCpuTime={maxCpuTime}
averageMemoryUsage={averageMemoryUsage}
totalHeapMemory={totalHeapMemory}
totalExternalMemory={totalExternalMemory}
totalMemoryByType={totalMemoryByType}
/>
</>
)
}
export default EdgeFunctionOverview