| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371 |
- import { BookOpen, Check, ExternalLink, Eye } from 'lucide-react'
- import { useRouter } from 'next/router'
- import { Fragment, useMemo } from 'react'
- import {
- Badge,
- Button,
- Card,
- cn,
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
- } from 'ui'
- import { PageContainer } from 'ui-patterns/PageContainer'
- import {
- PageSection,
- PageSectionAside,
- PageSectionContent,
- PageSectionMeta,
- PageSectionSummary,
- PageSectionTitle,
- } from 'ui-patterns/PageSection'
- import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
- import {
- buildGroupAssistantPrompt,
- buildTroubleshootingDocsUrl,
- formatLogTimestamp,
- formatSingleLineMessage,
- getDisplayErrorMessage,
- getFunctionRuntimeLogsSql,
- getRecentErrorGroups,
- getRecentErrorGroupsBase,
- getRecentErrorInvocationsSql,
- getRelatedExecutionIds,
- getSinceLastDeployInvocationCount,
- getSinceLastDeployInvocationCountSql,
- getSinceLastDeployInvocationPhrase,
- getSinceLastDeployLogRange,
- getStatusBadgeVariant,
- toAlertError,
- type RecentErrorGroup,
- } from './EdgeFunctionRecentErrors.utils'
- import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
- import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown'
- import AlertError from '@/components/ui/AlertError'
- import useLogsQuery from '@/hooks/analytics/useLogsQuery'
- import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
- import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
- interface EdgeFunctionRecentErrorsProps {
- functionId?: string
- functionSlug?: string
- projectRef?: string
- updatedAt?: string | number
- }
- export const EdgeFunctionRecentErrors = ({
- functionId,
- functionSlug,
- projectRef,
- 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 && isoTimestampStart)
- const recentErrorInvocationsSql = useMemo(
- () => getRecentErrorInvocationsSql(functionId),
- [functionId]
- )
- const sinceLastDeployInvocationCountSql = useMemo(
- () => getSinceLastDeployInvocationCountSql(functionId),
- [functionId]
- )
- const {
- logData: recentErrorInvocations,
- isLoading: isLoadingRecentErrorInvocations,
- error: recentErrorInvocationsError,
- } = useLogsQuery(
- projectRef as string,
- {
- sql: recentErrorInvocationsSql,
- iso_timestamp_start: isoTimestampStart,
- iso_timestamp_end: isoTimestampEnd,
- },
- isQueryEnabled
- )
- const recentErrorGroupsBase = useMemo(
- () => 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),
- [recentErrorGroupsBase]
- )
- const functionRuntimeLogsSql = useMemo(
- () => getFunctionRuntimeLogsSql({ functionId, executionIds: relatedExecutionIds }),
- [functionId, relatedExecutionIds]
- )
- const {
- logData: functionRuntimeLogs,
- isLoading: isLoadingFunctionRuntimeLogs,
- error: functionRuntimeLogsError,
- } = useLogsQuery(
- projectRef as string,
- {
- sql: functionRuntimeLogsSql,
- iso_timestamp_start: isoTimestampStart,
- iso_timestamp_end: isoTimestampEnd,
- },
- Boolean(projectRef && functionRuntimeLogsSql && isoTimestampStart)
- )
- const queryError =
- toAlertError(recentErrorInvocationsError) ?? toAlertError(functionRuntimeLogsError)
- const recentErrorGroups = useMemo(
- () => 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)
- aiAssistant.newChat({
- name: `Investigate ${functionSlug ?? 'error'}`,
- initialMessage: buildGroupAssistantPrompt(group, functionSlug),
- })
- }
- return (
- <PageSection>
- <PageSectionContent>
- <PageContainer size="full">
- <div className="flex flex-col gap-6">
- <PageSectionMeta>
- <PageSectionSummary>
- <PageSectionTitle>Errors since last deploy</PageSectionTitle>
- </PageSectionSummary>
- <PageSectionAside>
- <Button
- type="default"
- size="tiny"
- icon={<ExternalLink size={14} />}
- onClick={() =>
- router.push(`/project/${projectRef}/functions/${functionSlug}/logs`)
- }
- >
- View logs
- </Button>
- </PageSectionAside>
- </PageSectionMeta>
- {recentErrorInvocationsError || functionRuntimeLogsError ? (
- <AlertError
- error={queryError}
- subject="Failed to retrieve edge function errors since the last deploy"
- />
- ) : isLoadingRecentErrorInvocations ||
- isLoadingFunctionRuntimeLogs ||
- isLoadingSinceLastDeployInvocationCount ? (
- <GenericSkeletonLoader />
- ) : recentErrorGroups.length === 0 ? (
- <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">
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead>Error</TableHead>
- <TableHead>Count</TableHead>
- <TableHead>Last Seen</TableHead>
- <TableHead>Method</TableHead>
- <TableHead>Status</TableHead>
- <TableHead>Duration</TableHead>
- <TableHead className="text-right">Troubleshoot</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {recentErrorGroups.map((group) => {
- const displayMessage = getDisplayErrorMessage(group)
- const docsUrl = buildTroubleshootingDocsUrl({
- statusCode: group.lastStatusCode,
- })
- return (
- <Fragment key={group.message}>
- <TableRow key={`${group.message}-summary`}>
- <TableCell className="max-w-[420px]">
- <span
- className="block truncate whitespace-nowrap text-foreground"
- title={displayMessage}
- >
- {displayMessage}
- </span>
- </TableCell>
- <TableCell className="text-foreground-light">{group.count}</TableCell>
- <TableCell className="text-foreground-light">
- {formatLogTimestamp(group.lastSeen, 'relative')}
- </TableCell>
- <TableCell className="text-foreground-light">
- {group.lastMethod ?? '-'}
- </TableCell>
- <TableCell>
- {group.lastStatusCode ? (
- <Badge
- variant={getStatusBadgeVariant(group.lastStatusCode)}
- className="font-mono"
- >
- {group.lastStatusCode}
- </Badge>
- ) : (
- <Badge variant="destructive" className="font-mono">
- Error
- </Badge>
- )}
- </TableCell>
- <TableCell className="text-foreground-light">
- {group.executionTime ?? '-'}
- </TableCell>
- <TableCell className="text-right">
- <div className="flex justify-end">
- <AiAssistantDropdown
- label="Ask Assistant"
- size="tiny"
- buildPrompt={() => buildGroupAssistantPrompt(group, functionSlug)}
- onOpenAssistant={() => handleOpenAssistant(group)}
- additionalDropdownItems={[
- {
- label: 'View troubleshooting guide',
- icon: <BookOpen size={14} />,
- onClick: () =>
- window.open(docsUrl, '_blank', 'noopener,noreferrer'),
- },
- ]}
- />
- </div>
- </TableCell>
- </TableRow>
- <TableRow key={`${group.message}-logs`} className="hover:bg-transparent">
- <TableCell colSpan={7} className="p-0">
- <div className="max-h-64 overflow-auto bg-surface-75 font-mono text-xs">
- {group.logs.length === 0 ? (
- <div className="px-4 py-3 text-foreground-lighter">
- No related runtime logs found for this error group.
- </div>
- ) : (
- group.logs.map((log, index) => {
- const isError = log.level === 'error'
- return (
- <div
- key={log.key}
- className={cn(
- 'flex items-start gap-3 px-4 py-2',
- index !== 0 && 'border-t border-default',
- isError && 'bg-destructive-200/40'
- )}
- >
- <span className="shrink-0 tabular-nums text-foreground-muted">
- {formatLogTimestamp(log.lastSeen, 'time')}
- </span>
- <Badge
- variant={isError ? 'destructive' : 'default'}
- className="shrink-0"
- >
- {log.level}
- </Badge>
- {log.count > 1 && (
- <span className="shrink-0 text-foreground-muted tabular-nums">
- ×{log.count}
- </span>
- )}
- <span
- className={cn(
- 'flex-1 wrap-break-word whitespace-pre-wrap',
- isError ? 'text-destructive' : 'text-foreground-light'
- )}
- >
- {formatSingleLineMessage(log.message)}
- </span>
- </div>
- )
- })
- )}
- </div>
- </TableCell>
- </TableRow>
- </Fragment>
- )
- })}
- </TableBody>
- </Table>
- </Card>
- )}
- </div>
- </PageContainer>
- </PageSectionContent>
- </PageSection>
- )
- }
|