diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionChartEmptyState.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionChartEmptyState.tsx
new file mode 100644
index 00000000000..77171a44291
--- /dev/null
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionChartEmptyState.tsx
@@ -0,0 +1,20 @@
+import { BarChart2 } from 'lucide-react'
+import { ChartEmptyState } from 'ui-patterns/Chart'
+
+interface EdgeFunctionChartEmptyStateProps {
+ title: string
+ description?: string
+}
+
+export const EdgeFunctionChartEmptyState = ({
+ title,
+ description,
+}: EdgeFunctionChartEmptyStateProps) => {
+ return (
+ }
+ title={title}
+ description={description ?? 'It may take up to 24 hours for data to refresh'}
+ />
+ )
+}
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionInvocationsChart.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionInvocationsChart.tsx
new file mode 100644
index 00000000000..2b578bb4f57
--- /dev/null
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionInvocationsChart.tsx
@@ -0,0 +1,114 @@
+import { Rocket } from 'lucide-react'
+import { useMemo } from 'react'
+import {
+ Bar,
+ CartesianGrid,
+ BarChart as RechartBarChart,
+ ReferenceLine,
+ XAxis,
+ YAxis,
+} from 'recharts'
+import { ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
+
+import {
+ formatChartTimestamp,
+ getChartTimeRangeLabels,
+ INVOCATION_CHART_CONFIG,
+} from './EdgeFunctionOverview.utils'
+import type { InvocationChartDatum, InvocationUpdateAnnotation } from './EdgeFunctionOverview.utils'
+
+interface EdgeFunctionInvocationsChartProps {
+ chartData: InvocationChartDatum[]
+ dateTimeFormat: string
+ onChartClick: () => void
+ updateAnnotation?: InvocationUpdateAnnotation
+}
+
+export const EdgeFunctionInvocationsChart = ({
+ chartData,
+ dateTimeFormat,
+ onChartClick,
+ updateAnnotation,
+}: EdgeFunctionInvocationsChartProps) => {
+ const timeRangeLabels = useMemo(
+ () => getChartTimeRangeLabels(chartData, dateTimeFormat),
+ [chartData, dateTimeFormat]
+ )
+
+ return (
+
+
+
+
+
+
+
+
+ formatChartTimestamp(value as string | number | undefined, dateTimeFormat)
+ }
+ indicator="dot"
+ />
+ }
+ />
+
+
+
+ {updateAnnotation && (
+
+ )}
+
+
+ {updateAnnotation && (
+
+
+
+ )}
+
+ {timeRangeLabels && (
+
+ {timeRangeLabels.start}
+ {timeRangeLabels.end}
+
+ )}
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionInvocationsSection.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionInvocationsSection.tsx
new file mode 100644
index 00000000000..9d43ec7a877
--- /dev/null
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionInvocationsSection.tsx
@@ -0,0 +1,156 @@
+import AlertError from 'components/ui/AlertError'
+import type { ComponentProps } from 'react'
+import { useMemo } from 'react'
+import type { ChartIntervals } from 'types'
+import { Button } from 'ui'
+import { Chart, ChartActions, ChartLoadingState, ChartMetric } from 'ui-patterns/Chart'
+import { PageContainer } from 'ui-patterns/PageContainer'
+import {
+ PageSection,
+ PageSectionAside,
+ PageSectionContent,
+ PageSectionMeta,
+ PageSectionSummary,
+} from 'ui-patterns/PageSection'
+import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
+
+import { EdgeFunctionChartEmptyState } from './EdgeFunctionChartEmptyState'
+import { EdgeFunctionInvocationsChart } from './EdgeFunctionInvocationsChart'
+import {
+ EDGE_FUNCTION_CHART_INTERVALS,
+ formatRate,
+ getChartEmptyStateCopy,
+ getSegmentedButtonClassName,
+} from './EdgeFunctionOverview.utils'
+import { toAlertError } from './EdgeFunctionRecentErrors.utils'
+import type { InvocationChartDatum, InvocationUpdateAnnotation } from './EdgeFunctionOverview.utils'
+
+interface EdgeFunctionInvocationsSectionProps {
+ interval: string
+ onIntervalChange: (interval: string) => void
+ selectedInterval: ChartIntervals
+ actions?: ComponentProps['actions']
+ totalInvocationCount: number
+ totalErrorCount: number
+ totalWarningCount: number
+ isLoadingFunction: boolean
+ isErrorFunction: boolean
+ functionError?: unknown
+ isLoadingChart: boolean
+ isErrorChart: boolean
+ chartErrorMessage?: string
+ chartData: InvocationChartDatum[]
+ onChartClick: () => void
+ updateAnnotation?: InvocationUpdateAnnotation
+}
+
+export const EdgeFunctionInvocationsSection = ({
+ interval,
+ onIntervalChange,
+ selectedInterval,
+ actions,
+ totalInvocationCount,
+ totalErrorCount,
+ totalWarningCount,
+ isLoadingFunction,
+ isErrorFunction,
+ functionError,
+ isLoadingChart,
+ isErrorChart,
+ chartErrorMessage,
+ chartData,
+ onChartClick,
+ updateAnnotation,
+}: EdgeFunctionInvocationsSectionProps) => {
+ const dateTimeFormat = selectedInterval.format ?? 'MMM D, h:mma'
+ const emptyStateCopy = useMemo(
+ () => getChartEmptyStateCopy('invocations', isErrorChart, chartErrorMessage),
+ [chartErrorMessage, isErrorChart]
+ )
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {EDGE_FUNCTION_CHART_INTERVALS.map((item, index) => {
+ return (
+
+ )
+ })}
+
+
+
+
+
+
+ {isLoadingFunction &&
}
+ {isErrorFunction && (
+
+ )}
+
+
+
+
+ {isLoadingChart ? (
+
+ ) : isErrorChart || chartData.length === 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionMetricTooltipDetails.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionMetricTooltipDetails.tsx
new file mode 100644
index 00000000000..17e10d0a148
--- /dev/null
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionMetricTooltipDetails.tsx
@@ -0,0 +1,93 @@
+import type { ChartLineProps } from 'ui-patterns/Chart'
+
+import type { EdgeFunctionChartDatum } from './EdgeFunctionOverview.utils'
+import { formatReferenceDelta, getMemoryTooltipDetail } from './EdgeFunctionOverview.utils'
+
+export const ExecutionTooltipDetail = ({
+ averageExecutionTime,
+ metricKey,
+ value,
+}: {
+ averageExecutionTime: number
+ metricKey: string
+ value: unknown
+}) => {
+ if (metricKey !== 'max_execution_time') return null
+
+ return (
+
+ {formatReferenceDelta(Number(value ?? 0), averageExecutionTime)}
+
+ )
+}
+
+export const CpuTooltipDetail = ({
+ averageCpuTime,
+ value,
+}: {
+ averageCpuTime: number
+ value: unknown
+}) => {
+ return (
+
+ {formatReferenceDelta(Number(value ?? 0), averageCpuTime)}
+
+ )
+}
+
+export const MemoryTooltipDetail = ({
+ averageMemoryUsage,
+ datum,
+ value,
+}: {
+ averageMemoryUsage: number
+ datum: EdgeFunctionChartDatum
+ value: unknown
+}) => {
+ return (
+ <>
+
+ {formatReferenceDelta(Number(value ?? 0), averageMemoryUsage)}
+
+
+ {getMemoryTooltipDetail(datum.avg_heap_memory_used, datum.avg_external_memory_used)}
+
+ >
+ )
+}
+
+export const getExecutionTooltipDetails = (
+ averageExecutionTime: number
+): NonNullable => {
+ return function renderExecutionTooltipDetails(_, key, value) {
+ return (
+
+ )
+ }
+}
+
+export const getCpuTooltipDetails = (
+ averageCpuTime: number
+): NonNullable => {
+ return function renderCpuTooltipDetails(_, __, value) {
+ return
+ }
+}
+
+export const getMemoryTooltipDetails = (
+ averageMemoryUsage: number
+): NonNullable => {
+ return function renderMemoryTooltipDetails(datum, _, value) {
+ return (
+
+ )
+ }
+}
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.tsx
new file mode 100644
index 00000000000..c7c5e049a2b
--- /dev/null
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.tsx
@@ -0,0 +1,237 @@
+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('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: ,
+ },
+ ],
+ [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
+ }
+
+ if (!IS_PLATFORM) {
+ return null
+ }
+
+ return (
+ <>
+ {
+ router.push(
+ `/project/${projectRef}/functions/${functionSlug}/${
+ isUnifiedLogsEnabled ? 'logs' : 'invocations'
+ }${isUnifiedLogsEnabled ? '' : `?its=${startDate.toISOString()}`}`
+ )
+ }}
+ updateAnnotation={invocationUpdateAnnotation}
+ />
+
+
+
+
+
+
+ >
+ )
+}
+
+export default EdgeFunctionOverview
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.utils.test.ts b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.utils.test.ts
new file mode 100644
index 00000000000..d05fff7a749
--- /dev/null
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.utils.test.ts
@@ -0,0 +1,219 @@
+import dayjs from 'dayjs'
+import { describe, expect, it } from 'vitest'
+
+import {
+ EDGE_FUNCTION_CHART_INTERVALS,
+ type EdgeFunctionChartRawDatum,
+ formatChartTimestamp,
+ formatMetric,
+ formatRate,
+ formatReferenceDelta,
+ getBucketedTimeRange,
+ getChartEmptyStateCopy,
+ getChartTimeRangeLabels,
+ getExecutionMetrics,
+ getInvocationChartData,
+ getInvocationTotals,
+ getInvocationUpdateAnnotation,
+ getMemoryTooltipDetail,
+ getRollingTimeRange,
+ getSegmentedButtonClassName,
+ toEdgeFunctionChartData,
+ getUsageMetrics,
+} from './EdgeFunctionOverview.utils'
+
+describe('EdgeFunctionOverview.utils', () => {
+ it('uses a full day interval for the 1 day option', () => {
+ expect(
+ EDGE_FUNCTION_CHART_INTERVALS.find((interval) => interval.key === '1day')?.startUnit
+ ).toBe('day')
+ })
+
+ it('builds invocation chart data and totals from combined stats', () => {
+ const chartData = toEdgeFunctionChartData([
+ {
+ timestamp: '2026-03-20T10:00:00.000Z',
+ success_count: 10,
+ redirect_count: 2,
+ client_err_count: 1,
+ server_err_count: 3,
+ },
+ {
+ timestamp: '2026-03-20T10:15:00.000Z',
+ success_count: '4',
+ redirect_count: '1',
+ client_err_count: '0',
+ server_err_count: '2',
+ },
+ ] satisfies EdgeFunctionChartRawDatum[])
+ const data = getInvocationChartData(chartData)
+
+ expect(data).toEqual([
+ {
+ timestamp: '2026-03-20T10:00:00.000Z',
+ ok_count: 10,
+ warning_count: 3,
+ error_count: 3,
+ },
+ {
+ timestamp: '2026-03-20T10:15:00.000Z',
+ ok_count: 4,
+ warning_count: 1,
+ error_count: 2,
+ },
+ ])
+
+ expect(chartData[1]).toEqual({
+ timestamp: '2026-03-20T10:15:00.000Z',
+ success_count: 4,
+ redirect_count: 1,
+ client_err_count: 0,
+ server_err_count: 2,
+ avg_execution_time: 0,
+ max_execution_time: 0,
+ avg_cpu_time_used: 0,
+ max_cpu_time_used: 0,
+ avg_memory_used: 0,
+ avg_heap_memory_used: 0,
+ avg_external_memory_used: 0,
+ })
+
+ expect(getInvocationTotals(data)).toEqual({
+ totalInvocationCount: 23,
+ totalWarningCount: 4,
+ totalErrorCount: 5,
+ })
+ })
+
+ it('builds segmented button classes and chart range labels', () => {
+ expect(getSegmentedButtonClassName(0, 4)).toBe('rounded-tr-none rounded-br-none')
+ expect(getSegmentedButtonClassName(1, 4)).toBe('rounded-none')
+ expect(getSegmentedButtonClassName(3, 4)).toBe('rounded-tl-none rounded-bl-none')
+
+ expect(
+ getChartTimeRangeLabels(
+ [{ timestamp: '2026-03-20T10:00:00.000Z' }, { timestamp: '2026-03-20T11:00:00.000Z' }],
+ 'MMM D, h:mma'
+ )
+ ).toEqual({
+ start: dayjs('2026-03-20T10:00:00.000Z').format('MMM D, h:mma'),
+ end: dayjs('2026-03-20T11:00:00.000Z').format('MMM D, h:mma'),
+ })
+ expect(getChartTimeRangeLabels([], 'MMM D')).toBeUndefined()
+ expect(formatChartTimestamp('2026-03-20T10:00:00.000Z', 'MMM D, h:mma')).toBe(
+ dayjs('2026-03-20T10:00:00.000Z').format('MMM D, h:mma')
+ )
+ })
+
+ it('computes execution and usage metrics', () => {
+ const stats = [
+ {
+ timestamp: '2026-03-20T10:00:00.000Z',
+ success_count: 0,
+ redirect_count: 0,
+ client_err_count: 0,
+ server_err_count: 0,
+ avg_execution_time: 10,
+ max_execution_time: 18,
+ avg_cpu_time_used: 5,
+ max_cpu_time_used: 8,
+ avg_memory_used: 100,
+ avg_heap_memory_used: 75,
+ avg_external_memory_used: 25,
+ },
+ {
+ timestamp: '2026-03-20T10:15:00.000Z',
+ success_count: 0,
+ redirect_count: 0,
+ client_err_count: 0,
+ server_err_count: 0,
+ avg_execution_time: 30,
+ max_execution_time: 45,
+ avg_cpu_time_used: 15,
+ max_cpu_time_used: 20,
+ avg_memory_used: 200,
+ avg_heap_memory_used: 150,
+ avg_external_memory_used: 50,
+ },
+ ]
+
+ expect(getExecutionMetrics(stats)).toEqual({
+ averageExecutionTime: 20,
+ maxExecutionTime: 45,
+ })
+
+ expect(getUsageMetrics(stats)).toEqual({
+ averageCpuTime: 10,
+ maxCpuTime: 20,
+ averageMemoryUsage: 150,
+ totalHeapMemory: 225,
+ totalExternalMemory: 75,
+ totalMemoryByType: 300,
+ })
+ })
+
+ it('returns a snapped deploy annotation when updated_at falls within the selected window', () => {
+ const annotation = getInvocationUpdateAnnotation({
+ updatedAt: '2026-03-20T10:16:30.000Z',
+ invocationChartData: [
+ { timestamp: '2026-03-20T10:00:00.000Z', ok_count: 3, warning_count: 0, error_count: 0 },
+ { timestamp: '2026-03-20T10:15:00.000Z', ok_count: 5, warning_count: 1, error_count: 1 },
+ { timestamp: '2026-03-20T10:30:00.000Z', ok_count: 2, warning_count: 0, error_count: 0 },
+ ],
+ windowStart: new Date('2026-03-20T09:45:00.000Z'),
+ windowEnd: new Date('2026-03-20T10:45:00.000Z'),
+ })
+
+ expect(annotation?.timestamp).toBe('2026-03-20T10:15:00.000Z')
+ expect(annotation?.position).toBeCloseTo(50)
+ expect(annotation?.updatedAt.toISOString()).toBe('2026-03-20T10:16:30.000Z')
+ })
+
+ it('hides the deploy annotation when updated_at is outside the selected window', () => {
+ const annotation = getInvocationUpdateAnnotation({
+ updatedAt: '2026-03-20T11:05:00.000Z',
+ invocationChartData: [
+ { timestamp: '2026-03-20T10:00:00.000Z', ok_count: 3, warning_count: 0, error_count: 0 },
+ ],
+ windowStart: new Date('2026-03-20T09:45:00.000Z'),
+ windowEnd: new Date('2026-03-20T10:45:00.000Z'),
+ })
+
+ expect(annotation).toBeUndefined()
+ })
+
+ it('builds bucketed and rolling time windows from the selected interval', () => {
+ const interval = EDGE_FUNCTION_CHART_INTERVALS.find((item) => item.key === '1hr')
+ expect(interval).toBeDefined()
+
+ const now = new Date('2026-03-20T10:37:00.000Z')
+ const [bucketedStart, bucketedEnd] = getBucketedTimeRange(interval!, now)
+ const [rollingStart, rollingEnd] = getRollingTimeRange(interval!, now)
+
+ expect(bucketedStart.toISOString()).toBe('2026-03-20T09:00:00.000Z')
+ expect(bucketedEnd.toISOString()).toBe('2026-03-20T10:00:00.000Z')
+ expect(rollingStart.toISOString()).toBe('2026-03-20T09:37:00.000Z')
+ expect(rollingEnd.toISOString()).toBe('2026-03-20T10:37:00.000Z')
+ })
+
+ it('formats metric, rate, and reference deltas consistently', () => {
+ expect(formatMetric(12.34, 'MB')).toBe('12.3MB')
+ expect(formatMetric(1234, 'ms')).toBe('1,234ms')
+ expect(formatRate(1, 4)).toBe('25%')
+ expect(formatReferenceDelta(110, 100)).toBe('10% above average')
+ expect(formatReferenceDelta(90, 100)).toBe('10% below average')
+ expect(formatReferenceDelta(100, 100)).toBe('At average')
+ })
+
+ it('builds empty-state copy and tooltip detail strings', () => {
+ expect(getChartEmptyStateCopy('invocations', false, 'boom')).toEqual({
+ title: 'No data to show',
+ description: undefined,
+ })
+ expect(getChartEmptyStateCopy('CPU time', true, 'Request failed')).toEqual({
+ title: 'Unable to load CPU time',
+ description: 'Request failed',
+ })
+ expect(getMemoryTooltipDetail(12.34, 5.67)).toBe('Heap 12.3MB • External 5.7MB')
+ })
+})
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.utils.ts b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.utils.ts
new file mode 100644
index 00000000000..0bbc99017fa
--- /dev/null
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.utils.ts
@@ -0,0 +1,297 @@
+import dayjs from 'dayjs'
+import maxBy from 'lodash/maxBy'
+import meanBy from 'lodash/meanBy'
+import sumBy from 'lodash/sumBy'
+import type { ChartIntervals } from 'types'
+import type { ChartConfig } from 'ui'
+
+export type EdgeFunctionChartRawDatum = {
+ timestamp: string | number
+ success_count?: string | number
+ redirect_count?: string | number
+ client_err_count?: string | number
+ server_err_count?: string | number
+ avg_execution_time?: string | number
+ max_execution_time?: string | number
+ avg_cpu_time_used?: string | number
+ max_cpu_time_used?: string | number
+ avg_memory_used?: string | number
+ avg_heap_memory_used?: string | number
+ avg_external_memory_used?: string | number
+}
+
+export type EdgeFunctionChartDatum = {
+ timestamp: string
+ success_count: number
+ redirect_count: number
+ client_err_count: number
+ server_err_count: number
+ avg_execution_time: number
+ max_execution_time: number
+ avg_cpu_time_used: number
+ max_cpu_time_used: number
+ avg_memory_used: number
+ avg_heap_memory_used: number
+ avg_external_memory_used: number
+}
+
+export type InvocationChartDatum = {
+ timestamp: string
+ ok_count: number
+ warning_count: number
+ error_count: number
+}
+
+export type InvocationUpdateAnnotation = {
+ timestamp: string
+ position: number
+ updatedAt: Date
+}
+
+export const EDGE_FUNCTION_CHART_INTERVALS: ChartIntervals[] = [
+ {
+ key: '15min',
+ label: '15 min',
+ startValue: 15,
+ startUnit: 'minute',
+ format: 'MMM D, h:mm:ssa',
+ },
+ {
+ key: '1hr',
+ label: '1 hour',
+ startValue: 1,
+ startUnit: 'hour',
+ format: 'MMM D, h:mma',
+ },
+ {
+ key: '3hr',
+ label: '3 hours',
+ startValue: 3,
+ startUnit: 'hour',
+ format: 'MMM D, h:mma',
+ },
+ {
+ key: '1day',
+ label: '1 day',
+ startValue: 1,
+ startUnit: 'day',
+ format: 'MMM D, h:mma',
+ },
+]
+
+export const INVOCATION_CHART_CONFIG = {
+ ok_count: {
+ label: 'Ok',
+ color: 'hsl(var(--brand-default))',
+ },
+ warning_count: {
+ label: 'Warnings',
+ color: 'hsl(var(--warning-default))',
+ },
+ error_count: {
+ label: 'Errors',
+ color: 'hsl(var(--destructive-default))',
+ },
+} satisfies ChartConfig
+
+export const CPU_TIME_CHART_CONFIG = {
+ max_cpu_time_used: {
+ label: 'Max CPU Time',
+ color: 'hsl(var(--brand-default))',
+ },
+} satisfies ChartConfig
+
+export const EXECUTION_TIME_CHART_CONFIG = {
+ avg_execution_time: {
+ label: 'Average Execution Time',
+ color: 'hsl(var(--foreground-default))',
+ },
+ max_execution_time: {
+ label: 'Max Execution Time',
+ color: 'hsl(var(--brand-default))',
+ },
+} satisfies ChartConfig
+
+export const MEMORY_CHART_CONFIG = {
+ avg_memory_used: {
+ label: 'Memory Usage',
+ color: 'hsl(var(--brand-default))',
+ },
+} satisfies ChartConfig
+
+const toManipulateUnit = (unit: ChartIntervals['startUnit']) => unit as dayjs.ManipulateType
+const toNumber = (value: string | number | undefined) => Number(value ?? 0)
+
+export const getBucketedTimeRange = (
+ interval: ChartIntervals,
+ now: Date = new Date()
+): [Date, Date] => {
+ const currentTime = dayjs(now)
+ const unit = toManipulateUnit(interval.startUnit)
+ const start = currentTime.subtract(interval.startValue, unit).startOf(unit)
+ const end = currentTime.startOf(unit)
+
+ return [start.toDate(), end.toDate()]
+}
+
+export const getRollingTimeRange = (
+ interval: ChartIntervals,
+ now: Date = new Date()
+): [Date, Date] => {
+ const currentTime = dayjs(now)
+ const start = currentTime.subtract(interval.startValue, toManipulateUnit(interval.startUnit))
+
+ return [start.toDate(), currentTime.toDate()]
+}
+
+export const formatChartTimestamp = (value: Date | string | number | undefined, format: string) => {
+ return dayjs(value === undefined ? '' : value).format(format)
+}
+
+export const getChartTimeRangeLabels = (
+ data: Array<{ timestamp: string }>,
+ format: string
+): { start: string; end: string } | undefined => {
+ if (data.length === 0) return undefined
+
+ return {
+ start: formatChartTimestamp(data[0]?.timestamp, format),
+ end: formatChartTimestamp(data[data.length - 1]?.timestamp, format),
+ }
+}
+
+export const toEdgeFunctionChartData = (
+ rows: EdgeFunctionChartRawDatum[] = []
+): EdgeFunctionChartDatum[] =>
+ rows.map((row) => ({
+ timestamp: String(row.timestamp ?? ''),
+ success_count: toNumber(row.success_count),
+ redirect_count: toNumber(row.redirect_count),
+ client_err_count: toNumber(row.client_err_count),
+ server_err_count: toNumber(row.server_err_count),
+ avg_execution_time: toNumber(row.avg_execution_time),
+ max_execution_time: toNumber(row.max_execution_time),
+ avg_cpu_time_used: toNumber(row.avg_cpu_time_used),
+ max_cpu_time_used: toNumber(row.max_cpu_time_used),
+ avg_memory_used: toNumber(row.avg_memory_used),
+ avg_heap_memory_used: toNumber(row.avg_heap_memory_used),
+ avg_external_memory_used: toNumber(row.avg_external_memory_used),
+ }))
+
+export const getInvocationChartData = (data: EdgeFunctionChartDatum[]): InvocationChartDatum[] =>
+ data.map((datum) => ({
+ timestamp: datum.timestamp,
+ ok_count: datum.success_count,
+ warning_count: datum.redirect_count + datum.client_err_count,
+ error_count: datum.server_err_count,
+ }))
+
+export const getInvocationTotals = (data: InvocationChartDatum[]) => {
+ const totalInvocationCount = sumBy(data, (datum) => {
+ return datum.ok_count + datum.warning_count + datum.error_count
+ })
+
+ return {
+ totalInvocationCount,
+ totalWarningCount: sumBy(data, 'warning_count'),
+ totalErrorCount: sumBy(data, 'error_count'),
+ }
+}
+
+export const getExecutionMetrics = (data: EdgeFunctionChartDatum[]) => ({
+ averageExecutionTime: meanBy(data, 'avg_execution_time') ?? 0,
+ maxExecutionTime: maxBy(data, 'max_execution_time')?.max_execution_time ?? 0,
+})
+
+export const getUsageMetrics = (data: EdgeFunctionChartDatum[]) => {
+ const totalHeapMemory = sumBy(data, 'avg_heap_memory_used')
+ const totalExternalMemory = sumBy(data, 'avg_external_memory_used')
+
+ return {
+ averageCpuTime: meanBy(data, 'avg_cpu_time_used') ?? 0,
+ maxCpuTime: maxBy(data, 'max_cpu_time_used')?.max_cpu_time_used ?? 0,
+ averageMemoryUsage: meanBy(data, 'avg_memory_used') ?? 0,
+ totalHeapMemory,
+ totalExternalMemory,
+ totalMemoryByType: totalHeapMemory + totalExternalMemory,
+ }
+}
+
+export const getInvocationUpdateAnnotation = ({
+ updatedAt,
+ invocationChartData,
+ windowStart,
+ windowEnd,
+}: {
+ updatedAt?: string
+ invocationChartData: InvocationChartDatum[]
+ windowStart: Date
+ windowEnd: Date
+}): InvocationUpdateAnnotation | undefined => {
+ if (!updatedAt || invocationChartData.length === 0) return undefined
+
+ const updatedAtDate = new Date(updatedAt)
+ const updatedAtValue = updatedAtDate.valueOf()
+
+ if (Number.isNaN(updatedAtValue)) return undefined
+ if (updatedAtValue < windowStart.valueOf() || updatedAtValue > windowEnd.valueOf()) {
+ return undefined
+ }
+
+ const closestTimestamp = invocationChartData.reduce((closest, datum) => {
+ const datumDistance = Math.abs(new Date(datum.timestamp).valueOf() - updatedAtValue)
+ const closestDistance = Math.abs(new Date(closest.timestamp).valueOf() - updatedAtValue)
+
+ return datumDistance < closestDistance ? datum : closest
+ }).timestamp
+
+ const markerIndex = invocationChartData.findIndex((datum) => datum.timestamp === closestTimestamp)
+ if (markerIndex < 0) return undefined
+
+ return {
+ timestamp: closestTimestamp,
+ position: ((markerIndex + 0.5) / invocationChartData.length) * 100,
+ updatedAt: updatedAtDate,
+ }
+}
+
+export const getSegmentedButtonClassName = (index: number, total: number) => {
+ if (index === 0) return 'rounded-tr-none rounded-br-none'
+ if (index === total - 1) return 'rounded-tl-none rounded-bl-none'
+ return 'rounded-none'
+}
+
+export const getChartEmptyStateCopy = (
+ subject: string,
+ isError: boolean,
+ errorMessage?: string
+) => ({
+ title: isError ? `Unable to load ${subject}` : 'No data to show',
+ description: isError ? errorMessage : undefined,
+})
+
+export const formatMetric = (value?: number, unit?: string) => {
+ if (value === undefined || Number.isNaN(value)) return unit ? `0${unit}` : '0'
+
+ const formatted = unit === 'MB' ? value.toFixed(1) : Math.round(value).toLocaleString('en-US')
+ return unit ? `${formatted}${unit}` : formatted
+}
+
+export const formatRate = (count: number, total: number) =>
+ new Intl.NumberFormat('en-US', {
+ style: 'percent',
+ maximumFractionDigits: 1,
+ }).format(total === 0 ? 0 : count / total)
+
+export const formatReferenceDelta = (value: number, reference: number, label = 'average') => {
+ const difference = value - reference
+ if (Math.abs(difference) < Number.EPSILON) return `At ${label}`
+ if (reference === 0) return `${difference > 0 ? 'Above' : 'Below'} ${label}`
+
+ const percentDifference = Math.round(Math.abs((difference / reference) * 100))
+ return `${percentDifference}% ${difference > 0 ? 'above' : 'below'} ${label}`
+}
+
+export const getMemoryTooltipDetail = (heapMemory: number, externalMemory: number) => {
+ return `Heap ${formatMetric(heapMemory, 'MB')} • External ${formatMetric(externalMemory, 'MB')}`
+}
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionPerformanceSection.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionPerformanceSection.tsx
new file mode 100644
index 00000000000..1a037405d63
--- /dev/null
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionPerformanceSection.tsx
@@ -0,0 +1,104 @@
+import { useMemo } from 'react'
+import { ChartMetric } from 'ui-patterns/Chart'
+import { PageContainer } from 'ui-patterns/PageContainer'
+import {
+ PageSection,
+ PageSectionContent,
+ PageSectionMeta,
+ PageSectionSummary,
+ PageSectionTitle,
+} from 'ui-patterns/PageSection'
+
+import { getExecutionTooltipDetails } from './EdgeFunctionMetricTooltipDetails'
+import {
+ EXECUTION_TIME_CHART_CONFIG,
+ formatMetric,
+ getChartEmptyStateCopy,
+} from './EdgeFunctionOverview.utils'
+import { EdgeFunctionTimeSeriesChartCard } from './EdgeFunctionTimeSeriesChartCard'
+import type { EdgeFunctionChartDatum } from './EdgeFunctionOverview.utils'
+
+interface EdgeFunctionPerformanceSectionProps {
+ data: EdgeFunctionChartDatum[]
+ dateTimeFormat: string
+ isLoading: boolean
+ isError: boolean
+ errorMessage?: string
+ averageExecutionTime: number
+ maxExecutionTime: number
+}
+
+export const EdgeFunctionPerformanceSection = ({
+ data,
+ dateTimeFormat,
+ isLoading,
+ isError,
+ errorMessage,
+ averageExecutionTime,
+ maxExecutionTime,
+}: EdgeFunctionPerformanceSectionProps) => {
+ const emptyStateCopy = getChartEmptyStateCopy('execution time', isError, errorMessage)
+ const tooltipDetails = useMemo(
+ () => getExecutionTooltipDetails(averageExecutionTime),
+ [averageExecutionTime]
+ )
+ const metrics = (
+
+
+
+
+ )
+
+ return (
+
+
+
+
+
+
+ Performance
+
+
+
+
+ `${Math.round(value)}ms`,
+ }}
+ />
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.tsx
new file mode 100644
index 00000000000..34a6de6f936
--- /dev/null
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.tsx
@@ -0,0 +1,263 @@
+import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
+import { AiAssistantDropdown } from 'components/ui/AiAssistantDropdown'
+import AlertError from 'components/ui/AlertError'
+import useLogsQuery from 'hooks/analytics/useLogsQuery'
+import { ExternalLink } from 'lucide-react'
+import { useRouter } from 'next/router'
+import { Fragment, useMemo } from 'react'
+import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
+import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
+import {
+ Badge,
+ Button,
+ Card,
+ cn,
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from 'ui'
+import { PageContainer } from 'ui-patterns/PageContainer'
+import {
+ PageSection,
+ PageSectionAside,
+ PageSectionContent,
+ PageSectionMeta,
+ PageSectionSummary,
+ PageSectionTitle,
+} from 'ui-patterns/PageSection'
+import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
+
+import {
+ buildGroupAssistantPrompt,
+ formatLogTimestamp,
+ getStatusBadgeVariant,
+ formatSingleLineMessage,
+ getFunctionRuntimeLogsSql,
+ getRecentErrorGroups,
+ getRecentErrorGroupsBase,
+ getRecentErrorInvocationsSql,
+ getRelatedExecutionIds,
+ toAlertError,
+ type RecentErrorGroup,
+} from './EdgeFunctionRecentErrors.utils'
+
+interface EdgeFunctionRecentErrorsProps {
+ functionId?: string
+ functionSlug?: string
+ projectRef?: string
+ isoTimestampStart?: string
+ isoTimestampEnd?: string
+}
+
+export const EdgeFunctionRecentErrors = ({
+ functionId,
+ functionSlug,
+ projectRef,
+ isoTimestampStart,
+ isoTimestampEnd,
+}: EdgeFunctionRecentErrorsProps) => {
+ const router = useRouter()
+ const { openSidebar } = useSidebarManagerSnapshot()
+ const aiAssistant = useAiAssistantStateSnapshot()
+
+ const isQueryEnabled = Boolean(projectRef && functionId)
+ const recentErrorInvocationsSql = useMemo(
+ () => getRecentErrorInvocationsSql(functionId),
+ [functionId]
+ )
+
+ const {
+ logData: recentErrorInvocations,
+ isLoading: isLoadingRecentErrorInvocations,
+ error: recentErrorInvocationsError,
+ } = useLogsQuery(
+ projectRef as string,
+ {
+ sql: recentErrorInvocationsSql,
+ iso_timestamp_start: isoTimestampStart,
+ iso_timestamp_end: isoTimestampEnd,
+ },
+ isQueryEnabled
+ )
+
+ const recentErrorGroupsBase = useMemo(
+ () => getRecentErrorGroupsBase(recentErrorInvocations),
+ [recentErrorInvocations]
+ )
+
+ const relatedExecutionIds = useMemo(
+ () => getRelatedExecutionIds(recentErrorGroupsBase),
+ [recentErrorGroupsBase]
+ )
+
+ const functionRuntimeLogsSql = useMemo(
+ () => getFunctionRuntimeLogsSql({ functionId, executionIds: relatedExecutionIds }),
+ [functionId, relatedExecutionIds]
+ )
+
+ const {
+ logData: functionRuntimeLogs,
+ isLoading: isLoadingFunctionRuntimeLogs,
+ error: functionRuntimeLogsError,
+ } = useLogsQuery(
+ projectRef as string,
+ {
+ sql: functionRuntimeLogsSql,
+ iso_timestamp_start: isoTimestampStart,
+ iso_timestamp_end: isoTimestampEnd,
+ },
+ Boolean(projectRef && functionRuntimeLogsSql)
+ )
+ const queryError =
+ toAlertError(recentErrorInvocationsError) ?? toAlertError(functionRuntimeLogsError)
+
+ const recentErrorGroups = useMemo(
+ () => getRecentErrorGroups({ recentErrorGroupsBase, functionRuntimeLogs }),
+ [functionRuntimeLogs, recentErrorGroupsBase]
+ )
+
+ const handleOpenAssistant = (group: RecentErrorGroup) => {
+ openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
+ aiAssistant.newChat({
+ name: `Investigate ${functionSlug ?? 'error'}`,
+ initialMessage: buildGroupAssistantPrompt(group, functionSlug),
+ })
+ }
+
+ return (
+
+
+
+
+
+
+ Recent Failed Invocations
+
+
+ }
+ onClick={() =>
+ router.push(`/project/${projectRef}/functions/${functionSlug}/logs`)
+ }
+ >
+ View logs
+
+
+
+
+ {recentErrorInvocationsError || functionRuntimeLogsError ? (
+
+ ) : isLoadingRecentErrorInvocations || isLoadingFunctionRuntimeLogs ? (
+
+ ) : recentErrorGroups.length === 0 ? (
+
+ Recent runtime errors will appear here when this function returns a 5xx response.
+
+ ) : (
+
+
+
+
+ Error
+ Count
+ Last Seen
+ Method
+ Status
+ Duration
+ Assistant
+
+
+
+ {recentErrorGroups.map((group) => (
+
+
+
+
+ {formatSingleLineMessage(group.message)}
+
+
+ {group.count}
+
+ {formatLogTimestamp(group.lastSeen, 'relative')}
+
+
+ {group.lastMethod ?? '-'}
+
+
+ {group.lastStatusCode ? (
+
+ {group.lastStatusCode}
+
+ ) : (
+
+ Error
+
+ )}
+
+
+ {group.executionTime ?? '-'}
+
+
+
+
buildGroupAssistantPrompt(group, functionSlug)}
+ onOpenAssistant={() => handleOpenAssistant(group)}
+ />
+
+
+
+
+
+
+ {group.logs.length === 0 ? (
+
+ No related runtime logs found for this error group.
+
+ ) : (
+ group.logs.map((log) => (
+
+ [{formatLogTimestamp(log.lastSeen, 'time')}] [
+ {log.level.toUpperCase()}] [{log.count}x]{' '}
+ {formatSingleLineMessage(log.message)}
+
+ ))
+ )}
+
+
+
+
+ ))}
+
+
+
+ )}
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.utils.test.ts b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.utils.test.ts
new file mode 100644
index 00000000000..a874e18eee4
--- /dev/null
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.utils.test.ts
@@ -0,0 +1,205 @@
+import { describe, expect, it } from 'vitest'
+
+import {
+ buildGroupAssistantPrompt,
+ formatLogTimestamp,
+ formatSingleLineMessage,
+ getFunctionRuntimeLogsSql,
+ getRecentErrorGroups,
+ getRecentErrorGroupsBase,
+ getRelatedExecutionIds,
+ getStatusBadgeVariant,
+ toAlertError,
+} from './EdgeFunctionRecentErrors.utils'
+
+describe('EdgeFunctionRecentErrors.utils', () => {
+ it('normalizes alert errors and single-line messages', () => {
+ expect(toAlertError('boom')).toEqual({ message: 'boom' })
+ expect(toAlertError({ message: 'broken' })).toEqual({ message: 'broken' })
+ expect(toAlertError({ message: 123 })).toBeUndefined()
+ expect(toAlertError(null)).toBeUndefined()
+
+ expect(formatSingleLineMessage(' first line\n second\t\tline ')).toBe(
+ 'first line second line'
+ )
+ })
+
+ it('builds runtime log SQL and escapes interpolated values', () => {
+ expect(getFunctionRuntimeLogsSql({ functionId: undefined, executionIds: ['abc'] })).toBe('')
+ expect(getFunctionRuntimeLogsSql({ functionId: 'fn_123', executionIds: [] })).toBe('')
+
+ expect(
+ getFunctionRuntimeLogsSql({
+ functionId: "fn_'123",
+ executionIds: ['exec_1', "exec_'2"],
+ limit: 25,
+ })
+ )
+ .toBe(`select id, function_logs.timestamp, event_message, metadata.event_type, metadata.function_id, metadata.execution_id, metadata.level from function_logs
+cross join unnest(metadata) as metadata
+where metadata.function_id = 'fn_''123' and metadata.execution_id in ('exec_1', 'exec_''2')
+order by timestamp desc
+limit 25`)
+ })
+
+ it('groups recent failed invocations by parsed error message', () => {
+ const groups = getRecentErrorGroupsBase([
+ {
+ id: 'invocation-1',
+ event_message: 'POST | 500 | database exploded',
+ method: 'POST',
+ status_code: 500,
+ execution_id: 'exec-1',
+ execution_time_ms: 123.7,
+ timestamp: 100,
+ },
+ {
+ id: 'invocation-2',
+ event_message: 'POST | 500 | database exploded',
+ method: 'POST',
+ status_code: 500,
+ execution_id: 'exec-2',
+ execution_time_ms: 85.1,
+ timestamp: 120,
+ },
+ {
+ id: 'invocation-3',
+ event_message: '',
+ method: 'GET',
+ status_code: 503,
+ execution_id: '',
+ timestamp: 110,
+ },
+ ])
+
+ expect(groups).toEqual([
+ {
+ message: 'database exploded',
+ count: 2,
+ lastSeen: 120,
+ lastExecutionId: 'exec-2',
+ lastStatusCode: '500',
+ lastMethod: 'POST',
+ executionTime: '85ms',
+ executionIds: ['exec-1', 'exec-2'],
+ },
+ {
+ message: 'Unknown error',
+ count: 1,
+ lastSeen: 110,
+ lastExecutionId: undefined,
+ lastStatusCode: '503',
+ lastMethod: 'GET',
+ executionTime: undefined,
+ executionIds: [],
+ },
+ ])
+ })
+
+ it('deduplicates execution ids and attaches grouped runtime logs', () => {
+ const recentErrorGroupsBase = [
+ {
+ message: 'database exploded',
+ count: 2,
+ lastSeen: 120,
+ lastExecutionId: 'exec-2',
+ lastStatusCode: '500',
+ lastMethod: 'POST',
+ executionTime: '85ms',
+ executionIds: ['exec-1', 'exec-2', 'exec-1'],
+ },
+ ]
+
+ expect(getRelatedExecutionIds(recentErrorGroupsBase)).toEqual(['exec-1', 'exec-2'])
+
+ expect(
+ getRecentErrorGroups({
+ recentErrorGroupsBase,
+ functionRuntimeLogs: [
+ {
+ id: 'runtime-log-1',
+ execution_id: 'exec-1',
+ level: 'error',
+ event_message: 'stack trace',
+ timestamp: 101,
+ },
+ {
+ id: 'runtime-log-2',
+ execution_id: 'exec-2',
+ level: 'error',
+ event_message: 'stack trace',
+ timestamp: 121,
+ },
+ {
+ id: 'runtime-log-3',
+ execution_id: 'exec-2',
+ event_type: 'warn',
+ event_message: 'retrying upstream',
+ timestamp: 119,
+ },
+ {
+ id: 'runtime-log-4',
+ execution_id: '',
+ level: 'info',
+ event_message: 'ignored',
+ timestamp: 999,
+ },
+ ],
+ })
+ ).toEqual([
+ {
+ ...recentErrorGroupsBase[0],
+ logs: [
+ {
+ key: 'error:stack trace',
+ message: 'stack trace',
+ level: 'error',
+ count: 2,
+ lastSeen: 121,
+ },
+ {
+ key: 'warn:retrying upstream',
+ message: 'retrying upstream',
+ level: 'warn',
+ count: 1,
+ lastSeen: 119,
+ },
+ ],
+ },
+ ])
+ })
+
+ it('formats timestamps, prompts, and status variants', () => {
+ expect(formatLogTimestamp(undefined, 'time')).toBe('-')
+ expect(formatLogTimestamp('2026-03-20T10:15:00.000Z', 'time')).toBe('10:15:00')
+
+ expect(
+ buildGroupAssistantPrompt(
+ {
+ message: 'database exploded',
+ count: 2,
+ lastSeen: 1742465700000000,
+ lastExecutionId: 'exec-2',
+ lastStatusCode: '500',
+ lastMethod: 'POST',
+ executionTime: '85ms',
+ executionIds: ['exec-1', 'exec-2'],
+ logs: [
+ {
+ key: 'error:stack trace',
+ message: 'stack trace',
+ level: 'error',
+ count: 2,
+ lastSeen: 1742465700000000,
+ },
+ ],
+ },
+ 'my-function'
+ )
+ ).toContain('Analyze this recurring edge function error for `my-function`.')
+
+ expect(getStatusBadgeVariant()).toBe('destructive')
+ expect(getStatusBadgeVariant('500')).toBe('destructive')
+ expect(getStatusBadgeVariant('404')).toBe('default')
+ })
+})
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.utils.ts b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.utils.ts
new file mode 100644
index 00000000000..f12c9245736
--- /dev/null
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.utils.ts
@@ -0,0 +1,251 @@
+import { LOGS_TABLES } from 'components/interfaces/Settings/Logs/Logs.constants'
+import type { LogData } from 'components/interfaces/Settings/Logs/Logs.types'
+import {
+ genDefaultQuery,
+ isUnixMicro,
+ unixMicroToIsoTimestamp,
+} from 'components/interfaces/Settings/Logs/Logs.utils'
+import type { AlertErrorProps } from 'components/ui/AlertError'
+import dayjs from 'dayjs'
+import relativeTime from 'dayjs/plugin/relativeTime'
+
+import { parseEdgeFunctionEventMessage } from '../EdgeFunctionRecentInvocations.utils'
+
+dayjs.extend(relativeTime)
+
+export const MAX_RECENT_ERROR_GROUPS = 5
+export const RECENT_ERROR_INVOCATIONS_LIMIT = 50
+export const RELATED_RUNTIME_LOGS_LIMIT = 100
+
+export type GroupedRuntimeLog = {
+ key: string
+ message: string
+ level: string
+ count: number
+ lastSeen: number
+}
+
+export type RecentErrorGroup = {
+ message: string
+ count: number
+ lastSeen: number
+ lastExecutionId?: string
+ lastStatusCode?: string
+ lastMethod?: string
+ executionTime?: string
+ executionIds: string[]
+ logs: GroupedRuntimeLog[]
+}
+
+export type RecentErrorGroupBase = Omit
+
+export const escapeSqlString = (value: string) => value.replace(/'/g, "''")
+
+export const formatSingleLineMessage = (message: string) => message.replace(/\s+/g, ' ').trim()
+
+export const toAlertError = (error: unknown): AlertErrorProps['error'] | undefined => {
+ if (typeof error === 'string') return { message: error }
+
+ if (error && typeof error === 'object') {
+ const message = (error as { message?: unknown }).message
+ if (typeof message === 'string') return { message }
+ }
+
+ return undefined
+}
+
+export const formatLogTimestamp = (
+ value: string | number | undefined,
+ format: 'relative' | 'time'
+) => {
+ if (value === undefined) return '-'
+
+ const timestamp = isUnixMicro(value) ? unixMicroToIsoTimestamp(value) : String(value)
+ return format === 'relative'
+ ? dayjs.utc(timestamp).fromNow()
+ : dayjs.utc(timestamp).format('HH:mm:ss')
+}
+
+export const buildGroupMarkdown = (group: RecentErrorGroup, functionSlug?: string) => {
+ const lines = [
+ `## Recent error for \`${functionSlug ?? 'edge function'}\``,
+ '',
+ `### ${group.message}`,
+ `- Occurrences: ${group.count}`,
+ `- Last seen: ${formatLogTimestamp(group.lastSeen, 'relative')}`,
+ ]
+
+ if (group.lastMethod) lines.push(`- Last method: ${group.lastMethod}`)
+ if (group.lastStatusCode) lines.push(`- Last status: ${group.lastStatusCode}`)
+ if (group.executionTime) lines.push(`- Last execution time: ${group.executionTime}`)
+
+ lines.push('', '#### Related runtime logs')
+
+ if (group.logs.length === 0) {
+ lines.push('- No related runtime logs found for this error group.')
+ } else {
+ for (const log of group.logs) {
+ lines.push(
+ `- [${log.level}] ${log.count} occurrence${
+ log.count === 1 ? '' : 's'
+ }, last seen ${formatLogTimestamp(log.lastSeen, 'relative')}: ${log.message}`
+ )
+ }
+ }
+
+ return lines.join('\n')
+}
+
+export const buildGroupAssistantPrompt = (group: RecentErrorGroup, functionSlug?: string) => {
+ return [
+ `Analyze this recurring edge function error for \`${functionSlug ?? 'edge function'}\`.`,
+ 'Summarize the likely root cause, what the runtime logs suggest, and the next debugging steps.',
+ '',
+ buildGroupMarkdown(group, functionSlug),
+ ].join('\n')
+}
+
+export const getStatusBadgeVariant = (statusCode?: string) => {
+ if (!statusCode) return 'destructive' as const
+
+ const status = Number(statusCode)
+ if (Number.isNaN(status)) return 'destructive' as const
+ if (status >= 500) return 'destructive' as const
+
+ return 'default' as const
+}
+
+export const getRecentErrorInvocationsSql = (
+ functionId?: string,
+ limit = RECENT_ERROR_INVOCATIONS_LIMIT
+) =>
+ genDefaultQuery(
+ LOGS_TABLES.fn_edge,
+ {
+ function_id: functionId ?? '__pending__',
+ 'status_code.error': true,
+ },
+ limit
+ )
+
+export const getFunctionRuntimeLogsSql = ({
+ functionId,
+ executionIds,
+ limit = RELATED_RUNTIME_LOGS_LIMIT,
+}: {
+ functionId?: string
+ executionIds: string[]
+ limit?: number
+}) => {
+ if (!functionId || executionIds.length === 0) return ''
+
+ const escapedExecutionIds = executionIds.map((id) => `'${escapeSqlString(id)}'`).join(', ')
+
+ return `select id, function_logs.timestamp, event_message, metadata.event_type, metadata.function_id, metadata.execution_id, metadata.level from function_logs
+cross join unnest(metadata) as metadata
+where metadata.function_id = '${escapeSqlString(functionId)}' and metadata.execution_id in (${escapedExecutionIds})
+order by timestamp desc
+limit ${limit}`
+}
+
+export const getRecentErrorGroupsBase = (
+ recentErrorInvocations: LogData[]
+): RecentErrorGroupBase[] => {
+ const grouped: Record = {}
+
+ for (const item of recentErrorInvocations) {
+ const statusCode = String(item.status_code ?? '')
+ const method = String(item.method ?? '')
+ const message =
+ parseEdgeFunctionEventMessage(
+ String(item.event_message ?? ''),
+ method || undefined,
+ statusCode
+ ) || 'Unknown error'
+ const executionId = String(item.execution_id ?? '')
+ const timestamp = Number(item.timestamp ?? 0)
+ const executionTime =
+ item.execution_time_ms !== undefined
+ ? `${Math.round(Number(item.execution_time_ms))}ms`
+ : undefined
+ const current = grouped[message]
+
+ if (!current) {
+ grouped[message] = {
+ message,
+ count: 1,
+ lastSeen: timestamp,
+ lastExecutionId: executionId || undefined,
+ lastStatusCode: statusCode || undefined,
+ lastMethod: method || undefined,
+ executionTime,
+ executionIds: executionId ? [executionId] : [],
+ }
+ continue
+ }
+
+ current.count += 1
+
+ if (executionId && !current.executionIds.includes(executionId)) {
+ current.executionIds.push(executionId)
+ }
+
+ if (timestamp > current.lastSeen) {
+ current.lastSeen = timestamp
+ current.lastExecutionId = executionId || undefined
+ current.lastStatusCode = statusCode || undefined
+ current.lastMethod = method || undefined
+ current.executionTime = executionTime
+ }
+ }
+
+ return Object.values(grouped)
+ .sort((a, b) => b.lastSeen - a.lastSeen)
+ .slice(0, MAX_RECENT_ERROR_GROUPS)
+}
+
+export const getRelatedExecutionIds = (recentErrorGroupsBase: RecentErrorGroupBase[]) =>
+ Array.from(new Set(recentErrorGroupsBase.flatMap((group) => group.executionIds).filter(Boolean)))
+
+export const getRecentErrorGroups = ({
+ recentErrorGroupsBase,
+ functionRuntimeLogs,
+}: {
+ recentErrorGroupsBase: RecentErrorGroupBase[]
+ functionRuntimeLogs: LogData[]
+}): RecentErrorGroup[] => {
+ const runtimeLogsByExecutionId = functionRuntimeLogs.reduce>(
+ (acc, log) => {
+ const executionId = String(log.execution_id ?? '')
+ if (!executionId) return acc
+
+ acc[executionId] = [...(acc[executionId] ?? []), log]
+ return acc
+ },
+ {}
+ )
+
+ return recentErrorGroupsBase.map((group) => ({
+ ...group,
+ logs: Array.from(new Set(group.executionIds))
+ .flatMap((executionId) => runtimeLogsByExecutionId[executionId] ?? [])
+ .reduce((acc, log) => {
+ const level = String(log.level ?? log.event_type ?? 'log')
+ const message = String(log.event_message ?? '')
+ const key = `${level}:${message}`
+ const timestamp = Number(log.timestamp ?? 0)
+ const existing = acc.find((entry) => entry.key === key)
+
+ if (existing) {
+ existing.count += 1
+ existing.lastSeen = Math.max(existing.lastSeen, timestamp)
+ return acc
+ }
+
+ acc.push({ key, message, level, count: 1, lastSeen: timestamp })
+ return acc
+ }, [])
+ .sort((a, b) => b.count - a.count || b.lastSeen - a.lastSeen)
+ .slice(0, MAX_RECENT_ERROR_GROUPS),
+ }))
+}
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionTimeSeriesChartCard.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionTimeSeriesChartCard.tsx
new file mode 100644
index 00000000000..7eb53d345e7
--- /dev/null
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionTimeSeriesChartCard.tsx
@@ -0,0 +1,78 @@
+import type { ReactNode } from 'react'
+import type { ChartConfig } from 'ui'
+import {
+ Chart,
+ ChartCard,
+ ChartContent,
+ ChartHeader,
+ ChartLine,
+ ChartLoadingState,
+ type ChartLineProps,
+} from 'ui-patterns/Chart'
+
+import { EdgeFunctionChartEmptyState } from './EdgeFunctionChartEmptyState'
+import type { EdgeFunctionChartDatum } from './EdgeFunctionOverview.utils'
+
+interface EdgeFunctionTimeSeriesChartCardProps {
+ data: EdgeFunctionChartDatum[]
+ dateTimeFormat: string
+ isLoading: boolean
+ isError: boolean
+ emptyTitle: string
+ emptyDescription?: string
+ metrics: ReactNode
+ dataKey: string
+ dataKeys?: string[]
+ config: ChartConfig
+ tooltipDetails?: ChartLineProps['tooltipDetails']
+ referenceLines?: ChartLineProps['referenceLines']
+ yAxisProps?: ChartLineProps['YAxisProps']
+ className?: string
+}
+
+export const EdgeFunctionTimeSeriesChartCard = ({
+ data,
+ dateTimeFormat,
+ isLoading,
+ isError,
+ emptyTitle,
+ emptyDescription,
+ metrics,
+ dataKey,
+ dataKeys,
+ config,
+ tooltipDetails,
+ referenceLines,
+ yAxisProps,
+ className,
+}: EdgeFunctionTimeSeriesChartCardProps) => {
+ return (
+
+
+ {metrics}
+
+ }
+ loadingState={}
+ >
+
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionUsageSection.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionUsageSection.tsx
new file mode 100644
index 00000000000..a434abe7781
--- /dev/null
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionUsageSection.tsx
@@ -0,0 +1,158 @@
+import { useMemo } from 'react'
+import { ChartMetric } from 'ui-patterns/Chart'
+import { PageContainer } from 'ui-patterns/PageContainer'
+import {
+ PageSection,
+ PageSectionContent,
+ PageSectionMeta,
+ PageSectionSummary,
+ PageSectionTitle,
+} from 'ui-patterns/PageSection'
+
+import { getCpuTooltipDetails, getMemoryTooltipDetails } from './EdgeFunctionMetricTooltipDetails'
+import {
+ CPU_TIME_CHART_CONFIG,
+ formatMetric,
+ formatRate,
+ getChartEmptyStateCopy,
+ MEMORY_CHART_CONFIG,
+} from './EdgeFunctionOverview.utils'
+import { EdgeFunctionTimeSeriesChartCard } from './EdgeFunctionTimeSeriesChartCard'
+import type { EdgeFunctionChartDatum } from './EdgeFunctionOverview.utils'
+
+interface EdgeFunctionUsageSectionProps {
+ data: EdgeFunctionChartDatum[]
+ dateTimeFormat: string
+ isLoading: boolean
+ isError: boolean
+ errorMessage?: string
+ averageCpuTime: number
+ maxCpuTime: number
+ averageMemoryUsage: number
+ totalHeapMemory: number
+ totalExternalMemory: number
+ totalMemoryByType: number
+}
+
+export const EdgeFunctionUsageSection = ({
+ data,
+ dateTimeFormat,
+ isLoading,
+ isError,
+ errorMessage,
+ averageCpuTime,
+ maxCpuTime,
+ averageMemoryUsage,
+ totalHeapMemory,
+ totalExternalMemory,
+ totalMemoryByType,
+}: EdgeFunctionUsageSectionProps) => {
+ const cpuEmptyStateCopy = getChartEmptyStateCopy('CPU time', isError, errorMessage)
+ const memoryEmptyStateCopy = getChartEmptyStateCopy('memory usage', isError, errorMessage)
+ const cpuTooltipDetails = useMemo(() => getCpuTooltipDetails(averageCpuTime), [averageCpuTime])
+ const memoryTooltipDetails = useMemo(
+ () => getMemoryTooltipDetails(averageMemoryUsage),
+ [averageMemoryUsage]
+ )
+ const cpuMetrics = (
+
+
+
+
+ )
+ const memoryMetrics = (
+
+
+
+
+
+ )
+
+ return (
+
+
+
+
+
+
+ Usage
+
+
+
+
+ `${Math.round(value)}ms`,
+ }}
+ />
+
+ `${Number(value).toFixed(1)}MB`,
+ }}
+ />
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts b/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts
index 130abc22088..383d1e7abb8 100644
--- a/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts
+++ b/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts
@@ -1,7 +1,6 @@
import { useMonaco } from '@monaco-editor/react'
import { IS_PLATFORM } from 'common'
import BackwardIterator from 'components/ui/CodeEditor/Providers/BackwardIterator'
-import type { PlanId } from 'data/subscriptions/types'
import dayjs, { Dayjs } from 'dayjs'
import { get } from 'lodash'
import uniqBy from 'lodash/uniqBy'
@@ -9,7 +8,7 @@ import { useEffect } from 'react'
import logConstants from 'shared-data/log-constants'
import { LogsTableName, SQL_FILTER_TEMPLATES } from './Logs.constants'
-import type { Filters, LogData, LogsEndpointParams } from './Logs.types'
+import type { Filters, LogData, LogsEndpointParams, QueryType } from './Logs.types'
/**
* Convert a micro timestamp from number/string to iso timestamp
@@ -181,7 +180,7 @@ limit ${limit}
`
case 'function_logs':
- return `select id, ${table}.timestamp, event_message, metadata.event_type, metadata.function_id, metadata.level from ${table}
+ return `select id, ${table}.timestamp, event_message, metadata.event_type, metadata.function_id, metadata.execution_id, metadata.level from ${table}
${joins}
${where}
${orderBy}
@@ -205,7 +204,7 @@ ${orderBy}
limit ${limit}
`
}
- return `select id, ${table}.timestamp, event_message, response.status_code, request.method, request.pathname, m.function_id, m.execution_time_ms, m.deployment_id, m.version from ${table}
+ return `select id, ${table}.timestamp, event_message, response.status_code, request.method, request.pathname, m.function_id, m.execution_id, m.execution_time_ms, m.deployment_id, m.version from ${table}
${joins}
${where}
${orderBy}
@@ -303,12 +302,14 @@ export const genCountQuery = (table: LogsTableName, filters: Filters): string =>
}
/** calculates how much the chart start datetime should be offset given the current datetime filter params */
-const calcChartStart = (params: Partial): [Dayjs, string] => {
+const calcChartStart = (
+ params: Partial
+): [Dayjs, 'minute' | 'hour' | 'day'] => {
const ite = params.iso_timestamp_end ? dayjs(params.iso_timestamp_end) : dayjs()
// todo @TzeYiing needs typing
const its: any = params.iso_timestamp_start ? dayjs(params.iso_timestamp_start) : dayjs()
- let trunc = 'minute'
+ let trunc: 'minute' | 'hour' | 'day' = 'minute'
let extendValue = 60 * 6
const minuteDiff = ite.diff(its, 'minute')
const hourDiff = ite.diff(its, 'hour')
@@ -319,8 +320,6 @@ const calcChartStart = (params: Partial): [Dayjs, string] =>
trunc = 'day'
extendValue = 7
}
- //
- // @ts-ignore
return [its.add(-extendValue, trunc), trunc]
}
@@ -775,7 +774,7 @@ export function formatLogsAsMarkdown(rows: LogData[]): string {
.join('\n\n---\n\n')
}
-const QUERY_TYPE_LABELS: Record = {
+const QUERY_TYPE_LABELS: Record = {
api: 'API Gateway (Edge Network)',
database: 'Postgres Database',
functions: 'Edge Functions',
@@ -791,7 +790,7 @@ const QUERY_TYPE_LABELS: Record = {
etl: 'ETL',
}
-const LOG_TABLE_TO_SERVICE_LABEL: Record = {
+const LOG_TABLE_TO_SERVICE_LABEL: Record = {
edge_logs: 'API Gateway (Edge Network)',
postgres_logs: 'Postgres Database',
function_logs: 'Edge Functions',
@@ -808,6 +807,10 @@ const LOG_TABLE_TO_SERVICE_LABEL: Record = {
etl_replication_logs: 'ETL',
}
+const isLogsTableName = (value: string): value is LogsTableName =>
+ value in LOG_TABLE_TO_SERVICE_LABEL
+const isQueryType = (value: string): value is QueryType => value in QUERY_TYPE_LABELS
+
export function extractEdgeFunctionName(pathname: unknown): string {
if (typeof pathname !== 'string' || !pathname) return ''
const parts = pathname.split('/').filter(Boolean)
@@ -817,12 +820,12 @@ export function extractEdgeFunctionName(pathname: unknown): string {
function extractServiceLabelFromSql(sql: string): string | null {
const match = sql.match(/\bfrom\s+(\w+)/i)
const tableName = match?.[1]
- return tableName ? (LOG_TABLE_TO_SERVICE_LABEL[tableName] ?? null) : null
+ return tableName && isLogsTableName(tableName) ? LOG_TABLE_TO_SERVICE_LABEL[tableName] : null
}
export function buildLogsPrompt(rows: LogData[], queryType?: string, sqlQuery?: string): string {
const serviceLabel =
- (queryType ? QUERY_TYPE_LABELS[queryType] : null) ??
+ (queryType && isQueryType(queryType) ? QUERY_TYPE_LABELS[queryType] : null) ??
(sqlQuery ? extractServiceLabelFromSql(sqlQuery) : null)
const serviceContext = serviceLabel ? ` from the **${serviceLabel}** service` : ''
const sqlContext = sqlQuery ? `\n\n**Query used:**\n\`\`\`sql\n${sqlQuery.trim()}\n\`\`\`` : ''
diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx
index dc2d55a6d0f..29dad4538ea 100644
--- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx
+++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx
@@ -1,5 +1,6 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { IS_PLATFORM, useParams } from 'common'
+import { IS_PLATFORM, useFeatureFlags, useFlag, useParams } from 'common'
+import { EdgeFunctionOverview } from 'components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview'
import { EdgeFunctionRecentInvocations } from 'components/interfaces/Functions/EdgeFunctionRecentInvocations'
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
import DefaultLayout from 'components/layouts/DefaultLayout'
@@ -26,6 +27,7 @@ import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Button,
+ LogoLoader,
WarningIcon,
} from 'ui'
import { PageContainer } from 'ui-patterns/PageContainer'
@@ -62,7 +64,7 @@ const CHART_INTERVALS: ChartIntervals[] = [
},
]
-const PageLayout: NextPageWithLayout = () => {
+const LegacyEdgeFunctionOverview = () => {
const router = useRouter()
const { ref: projectRef, functionSlug } = useParams()
@@ -429,6 +431,21 @@ const PageLayout: NextPageWithLayout = () => {
)
}
+const PageLayout: NextPageWithLayout = () => {
+ const { hasLoaded: flagsLoaded } = useFeatureFlags()
+ const showNewOverview = useFlag('edgeFunctionsOverview') === true
+
+ if (IS_PLATFORM && !flagsLoaded) {
+ return
+ }
+
+ if (showNewOverview) {
+ return
+ }
+
+ return
+}
+
PageLayout.getLayout = (page) => (
{page}
diff --git a/packages/ui-patterns/src/Chart/charts/chart-line.tsx b/packages/ui-patterns/src/Chart/charts/chart-line.tsx
index 2fc044ab7fe..a7a111f5779 100644
--- a/packages/ui-patterns/src/Chart/charts/chart-line.tsx
+++ b/packages/ui-patterns/src/Chart/charts/chart-line.tsx
@@ -8,6 +8,7 @@ import {
CartesianGrid,
AreaChart as RechartAreaChart,
ReferenceArea,
+ ReferenceLine,
XAxis,
YAxis,
} from 'recharts'
@@ -42,11 +43,20 @@ export type ChartHighlightAction = {
onSelect: (ctx: { start: string; end: string; clear: () => void }) => void
}
+export type ChartReferenceLine = {
+ y: number
+ label?: string
+ stroke?: string
+ strokeWidth?: number
+ strokeDasharray?: string
+}
+
export interface ChartLineProps {
data: ChartLineTick[]
dataKey: string
- dataKeys?: string[] // Add this line
+ dataKeys?: string[]
config?: ChartConfig
+ tooltipDetails?: (datum: ChartLineTick, key: string, value: unknown) => ReactNode
onLineClick?: (datum: ChartLineTick, tooltipData?: CategoricalChartState) => void
DateTimeFormat?: string
isFullHeight?: boolean
@@ -68,13 +78,15 @@ export interface ChartLineProps {
[key: string]: any
}
strokeWidth?: number
+ referenceLines?: ChartReferenceLine[]
}
export const ChartLine = ({
data,
dataKey,
- dataKeys, // Add this line
+ dataKeys,
config,
+ tooltipDetails,
onLineClick,
DateTimeFormat = 'MMM D, YYYY, hh:mma',
isFullHeight = false,
@@ -91,6 +103,7 @@ export const ChartLine = ({
showYAxis = false,
YAxisProps,
strokeWidth = 1.5,
+ referenceLines,
}: ChartLineProps) => {
const [focusDataIndex, setFocusDataIndex] = useState(null)
const { resolvedTheme } = useTheme()
@@ -134,6 +147,14 @@ export const ChartLine = ({
bottom: 0,
}
+ const formatTooltipValue = (value: unknown) => {
+ if (typeof value === 'number') {
+ return YAxisProps?.tickFormatter ? YAxisProps.tickFormatter(value) : value.toLocaleString()
+ }
+
+ return String(value)
+ }
+
return (
dayjs(v).format(DateTimeFormat)}
+ formatter={(value, name, item) => {
+ const key = String(item.dataKey || name || dataKey)
+ const itemConfig = chartConfig[key]
+ const indicatorColor = item.payload.fill || item.color || color
+ const detail = tooltipDetails?.(item.payload as ChartLineTick, key, value)
+
+ return (
+ <>
+
+
+
+
+ {itemConfig?.label || name || key}
+
+ {detail}
+
+
+ {formatTooltipValue(value)}
+
+
+ >
+ )
+ }}
/>
}
/>
@@ -214,6 +261,15 @@ export const ChartLine = ({
fillOpacity={0.2}
/>
)}
+ {referenceLines?.map((line, index) => (
+
+ ))}
{keysToRender.map((key, index) => {
const keyConfig = chartConfig[key]
const lineColor =
diff --git a/packages/ui/src/components/shadcn/ui/chart.tsx b/packages/ui/src/components/shadcn/ui/chart.tsx
index e8ab237e744..335cb47e16c 100644
--- a/packages/ui/src/components/shadcn/ui/chart.tsx
+++ b/packages/ui/src/components/shadcn/ui/chart.tsx
@@ -50,7 +50,7 @@ const ChartContainer = React.forwardRef<
data-chart={chartId}
ref={ref}
className={cn(
- "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-foreground-muted [&_.recharts-cartesian-grid_line]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line-line]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
+ "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-foreground-muted [&_.recharts-cartesian-grid_line]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line-line]:stroke-foreground [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}