mirror of
https://github.com/supabase/supabase.git
synced 2026-09-22 13:37:53 +08:00
c228ca4f61
## Problem The Edge Function overview converted the numeric deployment timestamp to a string before parsing it as a date. This produced an invalid date, so the invocations chart omitted the deployment annotation. ## Fix Preserve the numeric timestamp returned by the API and allow the annotation helper to parse both numeric and string timestamps. Show deployment details in an accessible tooltip when hovering or focusing the rocket marker, and add regression coverage for numeric timestamps and the existing invalid, missing, and out-of-range cases. ## How to test - Run the focused EdgeFunctionOverview utility test suite. - Open an Edge Function whose latest deployment is within the selected chart interval. - Hover or focus the rocket marker. - Expected result: the invocations chart shows the dashed deployment line and rocket marker, and the marker tooltip shows the deployment time. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Invocation update annotations now display correctly when function update times are provided as numbers. - Timestamps at the start of the Unix epoch are now supported. - Annotations no longer appear when update times are invalid or chart data is incomplete. - **Accessibility** - Deployment markers in invocation charts are now keyboard-focusable and include accessible labels. - Deployment timestamps are available in a tooltip on hover or focus. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
255 lines
8.1 KiB
TypeScript
255 lines
8.1 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { IS_PLATFORM, useParams } from 'common'
|
|
import { ExternalLink } from 'lucide-react'
|
|
import { useRouter } from 'next/router'
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
|
|
import { EdgeFunctionInvocationsSection } from './EdgeFunctionInvocationsSection'
|
|
import {
|
|
EDGE_FUNCTION_CHART_INTERVALS,
|
|
getBucketedTimeRange,
|
|
getExecutionMetrics,
|
|
getInvocationChartData,
|
|
getInvocationChartNavigationUrl,
|
|
getInvocationTotals,
|
|
getInvocationUpdateAnnotation,
|
|
getRollingTimeRange,
|
|
getUsageMetrics,
|
|
toEdgeFunctionChartData,
|
|
} from './EdgeFunctionOverview.utils'
|
|
import type { EdgeFunctionChartRawDatum } from './EdgeFunctionOverview.utils'
|
|
import { EdgeFunctionPerformanceSection } from './EdgeFunctionPerformanceSection'
|
|
import { EdgeFunctionRecentErrors } from './EdgeFunctionRecentErrors'
|
|
import { EdgeFunctionUsageSection } from './EdgeFunctionUsageSection'
|
|
import { useEdgeFunctionOverviewShortcuts } from './useEdgeFunctionOverviewShortcuts'
|
|
import { useUnifiedLogsPreview } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
|
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'
|
|
|
|
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,
|
|
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]
|
|
)
|
|
|
|
useEdgeFunctionOverviewShortcuts({
|
|
onSetInterval: setInterval,
|
|
onRefresh: () => {
|
|
combinedStatsResults.refetch()
|
|
},
|
|
onOpenLogs: () => {
|
|
router.push(
|
|
`/project/${projectRef}/functions/${functionSlug}/${
|
|
isUnifiedLogsEnabled ? 'logs' : 'invocations'
|
|
}`
|
|
)
|
|
},
|
|
})
|
|
|
|
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={(timestamp) => {
|
|
if (!projectRef || !functionSlug) return
|
|
|
|
router.push(
|
|
getInvocationChartNavigationUrl({
|
|
projectRef,
|
|
functionSlug,
|
|
isUnifiedLogsEnabled,
|
|
rangeStart: startDate.toISOString(),
|
|
rangeEnd: endDate.toISOString(),
|
|
clickedTimestamp: timestamp,
|
|
})
|
|
)
|
|
}}
|
|
updateAnnotation={invocationUpdateAnnotation}
|
|
/>
|
|
|
|
<EdgeFunctionRecentErrors
|
|
functionId={id}
|
|
functionSlug={functionSlug as string}
|
|
projectRef={projectRef as string}
|
|
updatedAt={selectedFunction?.updated_at}
|
|
/>
|
|
|
|
<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}
|
|
/>
|
|
</>
|
|
)
|
|
}
|