Files
supabase/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.utils.ts
Saxon Fletcher 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

252 lines
7.7 KiB
TypeScript

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<RecentErrorGroup, 'logs'>
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<string, RecentErrorGroupBase> = {}
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<Record<string, LogData[]>>(
(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<GroupedRuntimeLog[]>((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),
}))
}