| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- import dayjs from 'dayjs'
- import relativeTime from 'dayjs/plugin/relativeTime'
- import { parseEdgeFunctionEventMessage } from '../EdgeFunctionRecentInvocations.utils'
- import { LOGS_TABLES } from '@/components/interfaces/Settings/Logs/Logs.constants'
- import type { LogData } from '@/components/interfaces/Settings/Logs/Logs.types'
- import {
- genCountQuery,
- genDefaultQuery,
- isUnixMicro,
- unixMicroToIsoTimestamp,
- } from '@/components/interfaces/Settings/Logs/Logs.utils'
- import type { AlertErrorProps } from '@/components/ui/AlertError'
- dayjs.extend(relativeTime)
- export const MAX_RECENT_ERROR_GROUPS = 5
- export const RECENT_ERROR_INVOCATIONS_LIMIT = 50
- export const RELATED_RUNTIME_LOGS_LIMIT = 100
- const NUMERIC_TIMESTAMP_PATTERN = /^\d+(?:\.\d+)?$/
- 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()
- /**
- * Trims a runtime error message down to the meaningful summary, dropping the
- * stack trace that follows the first ` at ` frame so we can show it inline in
- * a table cell.
- */
- export const summarizeErrorMessage = (message: string): string => {
- const collapsed = formatSingleLineMessage(message)
- if (!collapsed) return collapsed
- const stackFrameMatch = collapsed.match(/\s+at\s+\S+\s+\(/)
- if (stackFrameMatch && stackFrameMatch.index !== undefined) {
- return collapsed.slice(0, stackFrameMatch.index).trim()
- }
- return collapsed
- }
- /**
- * Picks the most useful error description for a group. The invocation
- * `event_message` only contains the request URL, so when we have a related
- * runtime error log we surface its summary instead.
- */
- export const getDisplayErrorMessage = (group: RecentErrorGroup): string => {
- const errorLog = group.logs.find((log) => log.level === 'error')
- if (errorLog) {
- const summary = summarizeErrorMessage(errorLog.message)
- if (summary) return summary
- }
- return summarizeErrorMessage(group.message)
- }
- const TROUBLESHOOTING_DOCS_BASE = 'https://supabase.com/docs/guides/troubleshooting'
- export const buildTroubleshootingDocsUrl = ({ statusCode }: { statusCode?: string }): string => {
- const numericStatusCode = Number(statusCode)
- if (Number.isFinite(numericStatusCode) && numericStatusCode >= 100) {
- return `${TROUBLESHOOTING_DOCS_BASE}/edge-function-${numericStatusCode}-response`
- }
- return `${TROUBLESHOOTING_DOCS_BASE}?search=${encodeURIComponent('edge function')}`
- }
- 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 toIsoTimestamp = (value?: string | number) => {
- if (value === undefined) return undefined
- const normalizedValue = typeof value === 'string' ? value.trim() : value
- if (normalizedValue === '') return undefined
- const stringValue = String(normalizedValue)
- const isNumericTimestamp = NUMERIC_TIMESTAMP_PATTERN.test(stringValue)
- const date = (() => {
- if (!isNumericTimestamp) return new Date(stringValue)
- const numericValue = Number(stringValue)
- if (!Number.isFinite(numericValue)) return new Date(NaN)
- if (stringValue.length >= 16) return new Date(numericValue / 1000)
- if (stringValue.length <= 10) return new Date(numericValue * 1000)
- return new Date(numericValue)
- })()
- return Number.isNaN(date.valueOf()) ? undefined : date.toISOString()
- }
- export const getSinceLastDeployLogRange = (updatedAt?: string | number, now: Date = new Date()) => {
- const isoTimestampStart = toIsoTimestamp(updatedAt)
- if (!isoTimestampStart) return {}
- const startDate = new Date(isoTimestampStart)
- const normalizedNow = new Date(now)
- const endDate = Number.isNaN(normalizedNow.valueOf()) ? new Date() : normalizedNow
- return {
- isoTimestampStart,
- isoTimestampEnd: new Date(Math.max(startDate.valueOf(), endDate.valueOf())).toISOString(),
- }
- }
- export const buildGroupMarkdown = (group: RecentErrorGroup, functionSlug?: string) => {
- const lines = [
- `## Error since last deploy 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 edge function error since the last deploy 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 getSinceLastDeployInvocationCountSql = (functionId?: string) =>
- genCountQuery(LOGS_TABLES.fn_edge, {
- function_id: functionId ?? '__pending__',
- })
- export const getSinceLastDeployInvocationCount = (invocationCountRows: LogData[]) => {
- const count = Number(invocationCountRows[0]?.count ?? 0)
- return Number.isFinite(count) ? count : 0
- }
- export const getSinceLastDeployInvocationPhrase = (invocationCount: number) => {
- const formattedCount = invocationCount.toLocaleString('en-US')
- const invocationLabel = invocationCount === 1 ? 'invocation' : 'invocations'
- return `${formattedCount} ${invocationLabel}`
- }
- export const getNoErrorsSinceLastDeployMessage = (invocationCount: number) => {
- const verb = invocationCount === 1 ? 'has' : 'have'
- const invocationPhrase = getSinceLastDeployInvocationPhrase(invocationCount)
- return `There ${verb} been ${invocationPhrase} since last deploy and no errors.`
- }
- 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),
- }))
- }
|