mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Errors since last deploy (#44733)
This commit is contained in:
1 parent
ac439d2366
commit
b8a246f2d7
4 files changed
+214
-18
No files matched your search
+1
-2
@@ -203,8 +203,7 @@ export const EdgeFunctionOverview = () => {
|
||||
functionId={id}
|
||||
functionSlug={functionSlug as string}
|
||||
projectRef={projectRef as string}
|
||||
isoTimestampStart={selectedWindowStart.toISOString()}
|
||||
isoTimestampEnd={selectedWindowEnd.toISOString()}
|
||||
updatedAt={selectedFunction?.updated_at}
|
||||
/>
|
||||
|
||||
<EdgeFunctionPerformanceSection
|
||||
|
||||
+82
-12
@@ -1,4 +1,4 @@
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import { Check, ExternalLink, Eye } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Fragment, useMemo } from 'react'
|
||||
import {
|
||||
@@ -33,6 +33,10 @@ import {
|
||||
getRecentErrorGroupsBase,
|
||||
getRecentErrorInvocationsSql,
|
||||
getRelatedExecutionIds,
|
||||
getSinceLastDeployInvocationCount,
|
||||
getSinceLastDeployInvocationCountSql,
|
||||
getSinceLastDeployInvocationPhrase,
|
||||
getSinceLastDeployLogRange,
|
||||
getStatusBadgeVariant,
|
||||
toAlertError,
|
||||
type RecentErrorGroup,
|
||||
@@ -48,26 +52,34 @@ interface EdgeFunctionRecentErrorsProps {
|
||||
functionId?: string
|
||||
functionSlug?: string
|
||||
projectRef?: string
|
||||
isoTimestampStart?: string
|
||||
isoTimestampEnd?: string
|
||||
updatedAt?: string | number
|
||||
}
|
||||
|
||||
export const EdgeFunctionRecentErrors = ({
|
||||
functionId,
|
||||
functionSlug,
|
||||
projectRef,
|
||||
isoTimestampStart,
|
||||
isoTimestampEnd,
|
||||
updatedAt,
|
||||
}: EdgeFunctionRecentErrorsProps) => {
|
||||
const router = useRouter()
|
||||
const { openSidebar } = useSidebarManagerSnapshot()
|
||||
const aiAssistant = useAiAssistantStateSnapshot()
|
||||
const { isoTimestampStart, isoTimestampEnd } = useMemo(
|
||||
() => getSinceLastDeployLogRange(updatedAt),
|
||||
[updatedAt]
|
||||
)
|
||||
const emptyStateFallback =
|
||||
'Runtime errors since the last deploy will appear here when this function returns a 5xx response.'
|
||||
|
||||
const isQueryEnabled = Boolean(projectRef && functionId)
|
||||
const isQueryEnabled = Boolean(projectRef && functionId && isoTimestampStart)
|
||||
const recentErrorInvocationsSql = useMemo(
|
||||
() => getRecentErrorInvocationsSql(functionId),
|
||||
[functionId]
|
||||
)
|
||||
const sinceLastDeployInvocationCountSql = useMemo(
|
||||
() => getSinceLastDeployInvocationCountSql(functionId),
|
||||
[functionId]
|
||||
)
|
||||
|
||||
const {
|
||||
logData: recentErrorInvocations,
|
||||
@@ -87,6 +99,19 @@ export const EdgeFunctionRecentErrors = ({
|
||||
() => getRecentErrorGroupsBase(recentErrorInvocations),
|
||||
[recentErrorInvocations]
|
||||
)
|
||||
const {
|
||||
logData: sinceLastDeployInvocationCountRows,
|
||||
isLoading: isLoadingSinceLastDeployInvocationCount,
|
||||
error: sinceLastDeployInvocationCountError,
|
||||
} = useLogsQuery(
|
||||
projectRef as string,
|
||||
{
|
||||
sql: sinceLastDeployInvocationCountSql,
|
||||
iso_timestamp_start: isoTimestampStart,
|
||||
iso_timestamp_end: isoTimestampEnd,
|
||||
},
|
||||
Boolean(projectRef && sinceLastDeployInvocationCountSql && isoTimestampStart)
|
||||
)
|
||||
|
||||
const relatedExecutionIds = useMemo(
|
||||
() => getRelatedExecutionIds(recentErrorGroupsBase),
|
||||
@@ -109,7 +134,7 @@ export const EdgeFunctionRecentErrors = ({
|
||||
iso_timestamp_start: isoTimestampStart,
|
||||
iso_timestamp_end: isoTimestampEnd,
|
||||
},
|
||||
Boolean(projectRef && functionRuntimeLogsSql)
|
||||
Boolean(projectRef && functionRuntimeLogsSql && isoTimestampStart)
|
||||
)
|
||||
const queryError =
|
||||
toAlertError(recentErrorInvocationsError) ?? toAlertError(functionRuntimeLogsError)
|
||||
@@ -118,6 +143,46 @@ export const EdgeFunctionRecentErrors = ({
|
||||
() => getRecentErrorGroups({ recentErrorGroupsBase, functionRuntimeLogs }),
|
||||
[functionRuntimeLogs, recentErrorGroupsBase]
|
||||
)
|
||||
const sinceLastDeployInvocationCount = useMemo(
|
||||
() => getSinceLastDeployInvocationCount(sinceLastDeployInvocationCountRows),
|
||||
[sinceLastDeployInvocationCountRows]
|
||||
)
|
||||
const emptyStateMessage = useMemo(() => {
|
||||
if (!isoTimestampStart || sinceLastDeployInvocationCountError) return emptyStateFallback
|
||||
|
||||
const verb = sinceLastDeployInvocationCount === 1 ? 'has' : 'have'
|
||||
const invocationPhrase = getSinceLastDeployInvocationPhrase(sinceLastDeployInvocationCount)
|
||||
|
||||
return (
|
||||
<>
|
||||
There {verb} been <span className="text-foreground">{invocationPhrase}</span> since last
|
||||
deploy and no errors.
|
||||
</>
|
||||
)
|
||||
}, [
|
||||
emptyStateFallback,
|
||||
isoTimestampStart,
|
||||
sinceLastDeployInvocationCount,
|
||||
sinceLastDeployInvocationCountError,
|
||||
])
|
||||
const emptyStateIcon =
|
||||
isoTimestampStart && !sinceLastDeployInvocationCountError ? (
|
||||
sinceLastDeployInvocationCount > 0 ? (
|
||||
<Check
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
className="mt-0.5 shrink-0 text-brand"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<Eye
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
className="mt-0.5 shrink-0 text-foreground-muted"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
) : null
|
||||
|
||||
const handleOpenAssistant = (group: RecentErrorGroup) => {
|
||||
openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
|
||||
@@ -134,7 +199,7 @@ export const EdgeFunctionRecentErrors = ({
|
||||
<div className="flex flex-col gap-6">
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Recent Failed Invocations</PageSectionTitle>
|
||||
<PageSectionTitle>Errors since last deploy</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
<PageSectionAside>
|
||||
<Button
|
||||
@@ -153,13 +218,18 @@ export const EdgeFunctionRecentErrors = ({
|
||||
{recentErrorInvocationsError || functionRuntimeLogsError ? (
|
||||
<AlertError
|
||||
error={queryError}
|
||||
subject="Failed to retrieve recent edge function errors"
|
||||
subject="Failed to retrieve edge function errors since the last deploy"
|
||||
/>
|
||||
) : isLoadingRecentErrorInvocations || isLoadingFunctionRuntimeLogs ? (
|
||||
) : isLoadingRecentErrorInvocations ||
|
||||
isLoadingFunctionRuntimeLogs ||
|
||||
isLoadingSinceLastDeployInvocationCount ? (
|
||||
<GenericSkeletonLoader />
|
||||
) : recentErrorGroups.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed px-4 py-6 text-sm text-foreground-light">
|
||||
Recent runtime errors will appear here when this function returns a 5xx response.
|
||||
<div className="rounded-md border border-dashed px-5 py-6 text-sm text-foreground-light">
|
||||
<div className="flex items-start gap-3">
|
||||
{emptyStateIcon}
|
||||
<div>{emptyStateMessage}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Card className="p-0 overflow-hidden">
|
||||
|
||||
+67
-2
@@ -1,18 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
buildGroupAssistantPrompt,
|
||||
formatLogTimestamp,
|
||||
formatSingleLineMessage,
|
||||
getFunctionRuntimeLogsSql,
|
||||
getNoErrorsSinceLastDeployMessage,
|
||||
getRecentErrorGroups,
|
||||
getRecentErrorGroupsBase,
|
||||
getRelatedExecutionIds,
|
||||
getSinceLastDeployInvocationCount,
|
||||
getSinceLastDeployInvocationCountSql,
|
||||
getSinceLastDeployInvocationPhrase,
|
||||
getSinceLastDeployLogRange,
|
||||
getStatusBadgeVariant,
|
||||
toAlertError,
|
||||
toIsoTimestamp,
|
||||
} from './EdgeFunctionRecentErrors.utils'
|
||||
|
||||
describe('EdgeFunctionRecentErrors.utils', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('normalizes alert errors and single-line messages', () => {
|
||||
expect(toAlertError('boom')).toEqual({ message: 'boom' })
|
||||
expect(toAlertError({ message: 'broken' })).toEqual({ message: 'broken' })
|
||||
@@ -42,6 +52,61 @@ order by timestamp desc
|
||||
limit 25`)
|
||||
})
|
||||
|
||||
it('normalizes deploy timestamps and derives the logs query range', () => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date('2026-03-20T12:00:00.000Z'))
|
||||
|
||||
const deployedAt = '2026-03-20T10:15:00.000Z'
|
||||
const deployedAtMilliseconds = Date.parse(deployedAt)
|
||||
|
||||
expect(toIsoTimestamp(deployedAt)).toBe(deployedAt)
|
||||
expect(toIsoTimestamp(String(deployedAtMilliseconds))).toBe(deployedAt)
|
||||
expect(toIsoTimestamp(String(deployedAtMilliseconds * 1000))).toBe(deployedAt)
|
||||
expect(toIsoTimestamp('')).toBeUndefined()
|
||||
expect(toIsoTimestamp('not-a-date')).toBeUndefined()
|
||||
|
||||
expect(getSinceLastDeployLogRange(deployedAt)).toEqual({
|
||||
isoTimestampStart: deployedAt,
|
||||
isoTimestampEnd: '2026-03-20T12:00:00.000Z',
|
||||
})
|
||||
|
||||
expect(getSinceLastDeployLogRange('2026-03-20T13:00:00.000Z')).toEqual({
|
||||
isoTimestampStart: '2026-03-20T13:00:00.000Z',
|
||||
isoTimestampEnd: '2026-03-20T13:00:00.000Z',
|
||||
})
|
||||
|
||||
expect(getSinceLastDeployLogRange()).toEqual({})
|
||||
})
|
||||
|
||||
it('builds the since-deploy invocation count query and empty-state message', () => {
|
||||
expect(getSinceLastDeployInvocationCountSql()).toContain(
|
||||
'SELECT count(*) as count FROM function_edge_logs'
|
||||
)
|
||||
expect(getSinceLastDeployInvocationCountSql()).toContain("(function_id = '__pending__')")
|
||||
|
||||
expect(
|
||||
getSinceLastDeployInvocationCount([
|
||||
{
|
||||
count: '12',
|
||||
},
|
||||
] as unknown as Parameters<typeof getSinceLastDeployInvocationCount>[0])
|
||||
).toBe(12)
|
||||
expect(getSinceLastDeployInvocationCount([])).toBe(0)
|
||||
|
||||
expect(getSinceLastDeployInvocationPhrase(1)).toBe('1 invocation')
|
||||
expect(getSinceLastDeployInvocationPhrase(1200)).toBe('1,200 invocations')
|
||||
|
||||
expect(getNoErrorsSinceLastDeployMessage(0)).toBe(
|
||||
'There have been 0 invocations since last deploy and no errors.'
|
||||
)
|
||||
expect(getNoErrorsSinceLastDeployMessage(1)).toBe(
|
||||
'There has been 1 invocation since last deploy and no errors.'
|
||||
)
|
||||
expect(getNoErrorsSinceLastDeployMessage(1200)).toBe(
|
||||
'There have been 1,200 invocations since last deploy and no errors.'
|
||||
)
|
||||
})
|
||||
|
||||
it('groups recent failed invocations by parsed error message', () => {
|
||||
const groups = getRecentErrorGroupsBase([
|
||||
{
|
||||
@@ -196,7 +261,7 @@ limit 25`)
|
||||
},
|
||||
'my-function'
|
||||
)
|
||||
).toContain('Analyze this recurring edge function error for `my-function`.')
|
||||
).toContain('Analyze this edge function error since the last deploy for `my-function`.')
|
||||
|
||||
expect(getStatusBadgeVariant()).toBe('destructive')
|
||||
expect(getStatusBadgeVariant('500')).toBe('destructive')
|
||||
|
||||
+64
-2
@@ -5,6 +5,7 @@ import { parseEdgeFunctionEventMessage } from '../EdgeFunctionRecentInvocations.
|
||||
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,
|
||||
@@ -16,6 +17,7 @@ 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
|
||||
@@ -66,9 +68,45 @@ export const formatLogTimestamp = (
|
||||
: 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 = [
|
||||
`## Recent error for \`${functionSlug ?? 'edge function'}\``,
|
||||
`## Error since last deploy for \`${functionSlug ?? 'edge function'}\``,
|
||||
'',
|
||||
`### ${group.message}`,
|
||||
`- Occurrences: ${group.count}`,
|
||||
@@ -98,7 +136,7 @@ export const buildGroupMarkdown = (group: RecentErrorGroup, functionSlug?: strin
|
||||
|
||||
export const buildGroupAssistantPrompt = (group: RecentErrorGroup, functionSlug?: string) => {
|
||||
return [
|
||||
`Analyze this recurring edge function error for \`${functionSlug ?? 'edge function'}\`.`,
|
||||
`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),
|
||||
@@ -128,6 +166,30 @@ export const getRecentErrorInvocationsSql = (
|
||||
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,
|
||||
|
||||
Reference in new issue
Block a user