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 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 = {} 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), })) }