EdgeFunctionRecentErrors.tsx371 lines · main
1import { BookOpen, Check, ExternalLink, Eye } from 'lucide-react'
2import { useRouter } from 'next/router'
3import { Fragment, useMemo } from 'react'
4import {
5 Badge,
6 Button,
7 Card,
8 cn,
9 Table,
10 TableBody,
11 TableCell,
12 TableHead,
13 TableHeader,
14 TableRow,
15} from 'ui'
16import { PageContainer } from 'ui-patterns/PageContainer'
17import {
18 PageSection,
19 PageSectionAside,
20 PageSectionContent,
21 PageSectionMeta,
22 PageSectionSummary,
23 PageSectionTitle,
24} from 'ui-patterns/PageSection'
25import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
26
27import {
28 buildGroupAssistantPrompt,
29 buildTroubleshootingDocsUrl,
30 formatLogTimestamp,
31 formatSingleLineMessage,
32 getDisplayErrorMessage,
33 getFunctionRuntimeLogsSql,
34 getRecentErrorGroups,
35 getRecentErrorGroupsBase,
36 getRecentErrorInvocationsSql,
37 getRelatedExecutionIds,
38 getSinceLastDeployInvocationCount,
39 getSinceLastDeployInvocationCountSql,
40 getSinceLastDeployInvocationPhrase,
41 getSinceLastDeployLogRange,
42 getStatusBadgeVariant,
43 toAlertError,
44 type RecentErrorGroup,
45} from './EdgeFunctionRecentErrors.utils'
46import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
47import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown'
48import AlertError from '@/components/ui/AlertError'
49import useLogsQuery from '@/hooks/analytics/useLogsQuery'
50import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
51import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
52
53interface EdgeFunctionRecentErrorsProps {
54 functionId?: string
55 functionSlug?: string
56 projectRef?: string
57 updatedAt?: string | number
58}
59
60export const EdgeFunctionRecentErrors = ({
61 functionId,
62 functionSlug,
63 projectRef,
64 updatedAt,
65}: EdgeFunctionRecentErrorsProps) => {
66 const router = useRouter()
67 const { openSidebar } = useSidebarManagerSnapshot()
68 const aiAssistant = useAiAssistantStateSnapshot()
69 const { isoTimestampStart, isoTimestampEnd } = useMemo(
70 () => getSinceLastDeployLogRange(updatedAt),
71 [updatedAt]
72 )
73 const emptyStateFallback =
74 'Runtime errors since the last deploy will appear here when this function returns a 5xx response.'
75
76 const isQueryEnabled = Boolean(projectRef && functionId && isoTimestampStart)
77 const recentErrorInvocationsSql = useMemo(
78 () => getRecentErrorInvocationsSql(functionId),
79 [functionId]
80 )
81 const sinceLastDeployInvocationCountSql = useMemo(
82 () => getSinceLastDeployInvocationCountSql(functionId),
83 [functionId]
84 )
85
86 const {
87 logData: recentErrorInvocations,
88 isLoading: isLoadingRecentErrorInvocations,
89 error: recentErrorInvocationsError,
90 } = useLogsQuery(
91 projectRef as string,
92 {
93 sql: recentErrorInvocationsSql,
94 iso_timestamp_start: isoTimestampStart,
95 iso_timestamp_end: isoTimestampEnd,
96 },
97 isQueryEnabled
98 )
99
100 const recentErrorGroupsBase = useMemo(
101 () => getRecentErrorGroupsBase(recentErrorInvocations),
102 [recentErrorInvocations]
103 )
104 const {
105 logData: sinceLastDeployInvocationCountRows,
106 isLoading: isLoadingSinceLastDeployInvocationCount,
107 error: sinceLastDeployInvocationCountError,
108 } = useLogsQuery(
109 projectRef as string,
110 {
111 sql: sinceLastDeployInvocationCountSql,
112 iso_timestamp_start: isoTimestampStart,
113 iso_timestamp_end: isoTimestampEnd,
114 },
115 Boolean(projectRef && sinceLastDeployInvocationCountSql && isoTimestampStart)
116 )
117
118 const relatedExecutionIds = useMemo(
119 () => getRelatedExecutionIds(recentErrorGroupsBase),
120 [recentErrorGroupsBase]
121 )
122
123 const functionRuntimeLogsSql = useMemo(
124 () => getFunctionRuntimeLogsSql({ functionId, executionIds: relatedExecutionIds }),
125 [functionId, relatedExecutionIds]
126 )
127
128 const {
129 logData: functionRuntimeLogs,
130 isLoading: isLoadingFunctionRuntimeLogs,
131 error: functionRuntimeLogsError,
132 } = useLogsQuery(
133 projectRef as string,
134 {
135 sql: functionRuntimeLogsSql,
136 iso_timestamp_start: isoTimestampStart,
137 iso_timestamp_end: isoTimestampEnd,
138 },
139 Boolean(projectRef && functionRuntimeLogsSql && isoTimestampStart)
140 )
141 const queryError =
142 toAlertError(recentErrorInvocationsError) ?? toAlertError(functionRuntimeLogsError)
143
144 const recentErrorGroups = useMemo(
145 () => getRecentErrorGroups({ recentErrorGroupsBase, functionRuntimeLogs }),
146 [functionRuntimeLogs, recentErrorGroupsBase]
147 )
148 const sinceLastDeployInvocationCount = useMemo(
149 () => getSinceLastDeployInvocationCount(sinceLastDeployInvocationCountRows),
150 [sinceLastDeployInvocationCountRows]
151 )
152 const emptyStateMessage = useMemo(() => {
153 if (!isoTimestampStart || sinceLastDeployInvocationCountError) return emptyStateFallback
154
155 const verb = sinceLastDeployInvocationCount === 1 ? 'has' : 'have'
156 const invocationPhrase = getSinceLastDeployInvocationPhrase(sinceLastDeployInvocationCount)
157
158 return (
159 <>
160 There {verb} been <span className="text-foreground">{invocationPhrase}</span> since last
161 deploy and no errors.
162 </>
163 )
164 }, [
165 emptyStateFallback,
166 isoTimestampStart,
167 sinceLastDeployInvocationCount,
168 sinceLastDeployInvocationCountError,
169 ])
170 const emptyStateIcon =
171 isoTimestampStart && !sinceLastDeployInvocationCountError ? (
172 sinceLastDeployInvocationCount > 0 ? (
173 <Check
174 size={16}
175 strokeWidth={1.5}
176 className="mt-0.5 shrink-0 text-brand"
177 aria-hidden="true"
178 />
179 ) : (
180 <Eye
181 size={16}
182 strokeWidth={1.5}
183 className="mt-0.5 shrink-0 text-foreground-muted"
184 aria-hidden="true"
185 />
186 )
187 ) : null
188
189 const handleOpenAssistant = (group: RecentErrorGroup) => {
190 openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
191 aiAssistant.newChat({
192 name: `Investigate ${functionSlug ?? 'error'}`,
193 initialMessage: buildGroupAssistantPrompt(group, functionSlug),
194 })
195 }
196
197 return (
198 <PageSection>
199 <PageSectionContent>
200 <PageContainer size="full">
201 <div className="flex flex-col gap-6">
202 <PageSectionMeta>
203 <PageSectionSummary>
204 <PageSectionTitle>Errors since last deploy</PageSectionTitle>
205 </PageSectionSummary>
206 <PageSectionAside>
207 <Button
208 type="default"
209 size="tiny"
210 icon={<ExternalLink size={14} />}
211 onClick={() =>
212 router.push(`/project/${projectRef}/functions/${functionSlug}/logs`)
213 }
214 >
215 View logs
216 </Button>
217 </PageSectionAside>
218 </PageSectionMeta>
219
220 {recentErrorInvocationsError || functionRuntimeLogsError ? (
221 <AlertError
222 error={queryError}
223 subject="Failed to retrieve edge function errors since the last deploy"
224 />
225 ) : isLoadingRecentErrorInvocations ||
226 isLoadingFunctionRuntimeLogs ||
227 isLoadingSinceLastDeployInvocationCount ? (
228 <GenericSkeletonLoader />
229 ) : recentErrorGroups.length === 0 ? (
230 <div className="rounded-md border border-dashed px-5 py-6 text-sm text-foreground-light">
231 <div className="flex items-start gap-3">
232 {emptyStateIcon}
233 <div>{emptyStateMessage}</div>
234 </div>
235 </div>
236 ) : (
237 <Card className="p-0 overflow-hidden">
238 <Table>
239 <TableHeader>
240 <TableRow>
241 <TableHead>Error</TableHead>
242 <TableHead>Count</TableHead>
243 <TableHead>Last Seen</TableHead>
244 <TableHead>Method</TableHead>
245 <TableHead>Status</TableHead>
246 <TableHead>Duration</TableHead>
247 <TableHead className="text-right">Troubleshoot</TableHead>
248 </TableRow>
249 </TableHeader>
250 <TableBody>
251 {recentErrorGroups.map((group) => {
252 const displayMessage = getDisplayErrorMessage(group)
253 const docsUrl = buildTroubleshootingDocsUrl({
254 statusCode: group.lastStatusCode,
255 })
256
257 return (
258 <Fragment key={group.message}>
259 <TableRow key={`${group.message}-summary`}>
260 <TableCell className="max-w-[420px]">
261 <span
262 className="block truncate whitespace-nowrap text-foreground"
263 title={displayMessage}
264 >
265 {displayMessage}
266 </span>
267 </TableCell>
268 <TableCell className="text-foreground-light">{group.count}</TableCell>
269 <TableCell className="text-foreground-light">
270 {formatLogTimestamp(group.lastSeen, 'relative')}
271 </TableCell>
272 <TableCell className="text-foreground-light">
273 {group.lastMethod ?? '-'}
274 </TableCell>
275 <TableCell>
276 {group.lastStatusCode ? (
277 <Badge
278 variant={getStatusBadgeVariant(group.lastStatusCode)}
279 className="font-mono"
280 >
281 {group.lastStatusCode}
282 </Badge>
283 ) : (
284 <Badge variant="destructive" className="font-mono">
285 Error
286 </Badge>
287 )}
288 </TableCell>
289 <TableCell className="text-foreground-light">
290 {group.executionTime ?? '-'}
291 </TableCell>
292 <TableCell className="text-right">
293 <div className="flex justify-end">
294 <AiAssistantDropdown
295 label="Ask Assistant"
296 size="tiny"
297 buildPrompt={() => buildGroupAssistantPrompt(group, functionSlug)}
298 onOpenAssistant={() => handleOpenAssistant(group)}
299 additionalDropdownItems={[
300 {
301 label: 'View troubleshooting guide',
302 icon: <BookOpen size={14} />,
303 onClick: () =>
304 window.open(docsUrl, '_blank', 'noopener,noreferrer'),
305 },
306 ]}
307 />
308 </div>
309 </TableCell>
310 </TableRow>
311 <TableRow key={`${group.message}-logs`} className="hover:bg-transparent">
312 <TableCell colSpan={7} className="p-0">
313 <div className="max-h-64 overflow-auto bg-surface-75 font-mono text-xs">
314 {group.logs.length === 0 ? (
315 <div className="px-4 py-3 text-foreground-lighter">
316 No related runtime logs found for this error group.
317 </div>
318 ) : (
319 group.logs.map((log, index) => {
320 const isError = log.level === 'error'
321 return (
322 <div
323 key={log.key}
324 className={cn(
325 'flex items-start gap-3 px-4 py-2',
326 index !== 0 && 'border-t border-default',
327 isError && 'bg-destructive-200/40'
328 )}
329 >
330 <span className="shrink-0 tabular-nums text-foreground-muted">
331 {formatLogTimestamp(log.lastSeen, 'time')}
332 </span>
333 <Badge
334 variant={isError ? 'destructive' : 'default'}
335 className="shrink-0"
336 >
337 {log.level}
338 </Badge>
339 {log.count > 1 && (
340 <span className="shrink-0 text-foreground-muted tabular-nums">
341 ×{log.count}
342 </span>
343 )}
344 <span
345 className={cn(
346 'flex-1 wrap-break-word whitespace-pre-wrap',
347 isError ? 'text-destructive' : 'text-foreground-light'
348 )}
349 >
350 {formatSingleLineMessage(log.message)}
351 </span>
352 </div>
353 )
354 })
355 )}
356 </div>
357 </TableCell>
358 </TableRow>
359 </Fragment>
360 )
361 })}
362 </TableBody>
363 </Table>
364 </Card>
365 )}
366 </div>
367 </PageContainer>
368 </PageSectionContent>
369 </PageSection>
370 )
371}