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

206 lines
5.7 KiB
TypeScript

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')
})
})