Files
supabase/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.utils.ts
T
Saxon FletcherandClaude Opus 5.5 862c8634d9 feat(studio): show edge function Overview errors in the shared logs table
The Overview's recent errors section now reuses the Logs tab's queries
and the read-only EmbeddedLogsTable instead of its own SQL and table.
It previews the latest five errors since the last deploy (capped at
24h), notes how many more there are and links to them.

- Rows, "View logs" and "+N more" open the function's Logs tab with the
  same range and error filter. With the unified logs preview off, they
  open the Invocations or Logs tab with those tabs' own params
- "Explain with AI" and "Copy summary" hand the previewed errors to the
  Assistant with the same prompt as selected logs on the Logs page
- Removes the custom error grouping and its SQL

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:59:53 +10:00

383 lines
11 KiB
TypeScript

import dayjs from 'dayjs'
import maxBy from 'lodash/maxBy'
import meanBy from 'lodash/meanBy'
import sumBy from 'lodash/sumBy'
import type { ChartConfig } from 'ui'
import { calculateBarClickTimeRange } from '@/components/interfaces/Settings/Logs/LogsBarChart.utils'
import { formatUnifiedLogsDateParam } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.utils'
import type { ChartIntervals } from '@/types'
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: 'var(--primary-bright)',
},
warning_count: {
label: 'Warnings',
color: 'var(--chart-status-warning)',
},
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: 'var(--primary-bright)',
},
} satisfies ChartConfig
export const EXECUTION_TIME_CHART_CONFIG = {
avg_execution_time: {
label: 'Average Execution Time',
color: 'var(--foreground-default)',
},
max_execution_time: {
label: 'Max Execution Time',
color: 'var(--primary-bright)',
},
} satisfies ChartConfig
export const MEMORY_CHART_CONFIG = {
avg_memory_used: {
label: 'Memory Usage',
color: 'var(--primary-bright)',
},
} 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 getInvocationChartNavigationUrl = ({
projectRef,
functionSlug,
isUnifiedLogsEnabled,
rangeStart,
rangeEnd,
clickedTimestamp,
}: {
projectRef: string
functionSlug: string
isUnifiedLogsEnabled: boolean
rangeStart: string
rangeEnd: string
clickedTimestamp: string
}) => {
const { start, end } = calculateBarClickTimeRange(rangeStart, rangeEnd, clickedTimestamp)
if (isUnifiedLogsEnabled) {
return getEdgeFunctionLogsUrl({ projectRef, functionSlug, start, end })
}
return getLegacyEdgeFunctionLogsUrl({ projectRef, functionSlug, tab: 'invocations', start, end })
}
/**
* Links to an edge function's unified Logs tab, optionally focused on a time range, filters
* (e.g. `level:eq:error`) and a selected log. Uses the same URL params as the project-wide Logs page.
*/
export const getEdgeFunctionLogsUrl = ({
projectRef,
functionSlug,
start,
end,
filters,
logId,
}: {
projectRef: string
functionSlug: string
start?: string
end?: string
filters?: readonly string[]
logId?: string
}) => {
const params = new URLSearchParams()
if (start && end) params.set('date', formatUnifiedLogsDateParam(start, end))
if (filters?.length) params.set('filter', filters.join(','))
if (logId) params.set('id', logId)
const query = params.toString()
return `/project/${projectRef}/functions/${functionSlug}/logs${query ? `?${query}` : ''}`
}
/**
* Links to an edge function's Invocations or Logs tab from before unified logs, optionally focused
* on a time range and a selected log. Uses those tabs' own URL params.
*/
export const getLegacyEdgeFunctionLogsUrl = ({
projectRef,
functionSlug,
tab,
start,
end,
logId,
}: {
projectRef: string
functionSlug: string
tab: 'invocations' | 'logs'
start?: string
end?: string
logId?: string
}) => {
const params = new URLSearchParams()
if (start && end) {
params.set('its', start)
params.set('ite', end)
}
if (logId) params.set('log', logId)
const query = params.toString()
return `/project/${projectRef}/functions/${functionSlug}/${tab}${query ? `?${query}` : ''}`
}
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?: number | string
invocationChartData: InvocationChartDatum[]
windowStart: Date
windowEnd: Date
}): InvocationUpdateAnnotation | undefined => {
if (updatedAt === undefined || 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')}`
}