index.tsx449 lines · main
1import { PermissionAction } from '@supabase/shared-types/out/constants'
2import { IS_PLATFORM, useFeatureFlags, useFlag, useParams } from 'common'
3import dayjs, { Dayjs } from 'dayjs'
4import maxBy from 'lodash/maxBy'
5import meanBy from 'lodash/meanBy'
6import sumBy from 'lodash/sumBy'
7import { useRouter } from 'next/router'
8import { useMemo, useState } from 'react'
9import { Alert, AlertDescription, AlertTitle, Button, LogoLoader, WarningIcon } from 'ui'
10import { PageContainer } from 'ui-patterns/PageContainer'
11import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
12
13import { EdgeFunctionOverview } from '@/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview'
14import { EdgeFunctionRecentInvocations } from '@/components/interfaces/Functions/EdgeFunctionRecentInvocations'
15import ReportWidget from '@/components/interfaces/Reports/ReportWidget'
16import DefaultLayout from '@/components/layouts/DefaultLayout'
17import EdgeFunctionDetailsLayout from '@/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout'
18import AreaChart from '@/components/ui/Charts/AreaChart'
19import StackedBarChart from '@/components/ui/Charts/StackedBarChart'
20import NoPermission from '@/components/ui/NoPermission'
21import {
22 FunctionsCombinedStatsVariables,
23 useFunctionsCombinedStatsQuery,
24} from '@/data/analytics/functions-combined-stats-query'
25import { useEdgeFunctionQuery } from '@/data/edge-functions/edge-function-query'
26import { useFillTimeseriesSorted } from '@/hooks/analytics/useFillTimeseriesSorted'
27import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
28import type { ChartIntervals, NextPageWithLayout } from '@/types'
29
30const CHART_INTERVALS: ChartIntervals[] = [
31 {
32 key: '15min',
33 label: '15 min',
34 startValue: 15,
35 startUnit: 'minute',
36 format: 'MMM D, h:mm:ssa',
37 },
38 {
39 key: '1hr',
40 label: '1 hour',
41 startValue: 1,
42 startUnit: 'hour',
43 format: 'MMM D, h:mma',
44 },
45 {
46 key: '3hr',
47 label: '3 hours',
48 startValue: 3,
49 startUnit: 'hour',
50 format: 'MMM D, h:mma',
51 },
52 {
53 key: '1day',
54 label: '1 day',
55 startValue: 1,
56 startUnit: 'hour',
57 format: 'MMM D, h:mma',
58 },
59]
60
61const LegacyEdgeFunctionOverview = () => {
62 const router = useRouter()
63 const { ref: projectRef, functionSlug } = useParams()
64
65 const [interval, setInterval] = useState<string>('15min')
66 const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
67 const { data: selectedFunction } = useEdgeFunctionQuery({
68 projectRef,
69 slug: functionSlug,
70 })
71 const id = selectedFunction?.id
72 const combinedStatsResults = useFunctionsCombinedStatsQuery({
73 projectRef,
74 functionId: id,
75 interval: selectedInterval.key as FunctionsCombinedStatsVariables['interval'],
76 })
77
78 const combinedStatsData = useMemo(() => {
79 const result = combinedStatsResults.data?.result as
80 | Record<string, string | number>[]
81 | undefined
82 return result || []
83 }, [combinedStatsResults.data])
84
85 const [startDate, endDate]: [Dayjs, Dayjs] = useMemo(() => {
86 const start = dayjs()
87 .subtract(selectedInterval.startValue, selectedInterval.startUnit as dayjs.ManipulateType)
88 .startOf(selectedInterval.startUnit as dayjs.ManipulateType)
89
90 const end = dayjs().startOf(selectedInterval.startUnit as dayjs.ManipulateType)
91 return [start, end]
92 }, [selectedInterval])
93
94 const {
95 data: combinedStatsChartData,
96 error: combinedStatsError,
97 isError: isErrorCombinedStats,
98 } = useFillTimeseriesSorted({
99 data: combinedStatsData,
100 timestampKey: 'timestamp',
101 valueKey: [
102 'requests_count',
103 'log_count',
104 'log_info_count',
105 'log_warn_count',
106 'log_error_count',
107 'success_count',
108 'redirect_count',
109 'client_err_count',
110 'server_err_count',
111 'avg_cpu_time_used',
112 'avg_memory_used',
113 'avg_execution_time',
114 'max_execution_time',
115 'avg_heap_memory_used',
116 'avg_external_memory_used',
117 'max_cpu_time_used',
118 ],
119 defaultValue: 0,
120 startDate: startDate.toISOString(),
121 endDate: endDate.toISOString(),
122 })
123
124 const { isLoading: permissionsLoading, can: canReadFunction } = useAsyncCheckPermissions(
125 PermissionAction.FUNCTIONS_READ,
126 functionSlug as string
127 )
128 if (!canReadFunction && !permissionsLoading) {
129 return <NoPermission isFullPage resourceText="access this edge function" />
130 }
131
132 return (
133 <PageContainer size="full">
134 <PageSection>
135 <PageSectionContent>
136 {IS_PLATFORM && id && (
137 <div className="mb-8">
138 <EdgeFunctionRecentInvocations
139 functionId={id}
140 functionSlug={functionSlug as string}
141 />
142 </div>
143 )}
144 <div className="flex flex-row items-center gap-2 mb-4">
145 <div className="flex items-center">
146 {CHART_INTERVALS.map((item, i) => {
147 const classes = []
148
149 if (i === 0) {
150 classes.push('rounded-tr-none rounded-br-none')
151 } else if (i === CHART_INTERVALS.length - 1) {
152 classes.push('rounded-tl-none rounded-bl-none')
153 } else {
154 classes.push('rounded-none')
155 }
156
157 return (
158 <Button
159 key={`function-filter-${i}`}
160 type={interval === item.key ? 'secondary' : 'default'}
161 onClick={() => setInterval(item.key)}
162 className={classes.join(' ')}
163 >
164 {item.label}
165 </Button>
166 )
167 })}
168 </div>
169
170 <span className="text-xs text-foreground-light">
171 Statistics for past {selectedInterval.label}
172 </span>
173 </div>
174 <div>
175 <div className="grid grid-cols-1 md:grid-cols-2 md:gap-4 lg:grid-cols-2 lg:gap-8">
176 <ReportWidget
177 title="Execution time"
178 tooltip="Average execution time of function invocations"
179 data={combinedStatsChartData}
180 isLoading={combinedStatsResults.isLoading}
181 renderer={(props) => {
182 return isErrorCombinedStats ? (
183 <Alert variant="warning">
184 <WarningIcon />
185 <AlertTitle>Failed to reterieve execution time</AlertTitle>
186 <AlertDescription>
187 {combinedStatsError?.message ?? 'Unknown error'}
188 </AlertDescription>
189 </Alert>
190 ) : (
191 <div className="space-y-8">
192 <AreaChart
193 title="Average execution time"
194 className="w-full"
195 xAxisKey="timestamp"
196 customDateFormat={selectedInterval.format}
197 yAxisKey="avg_execution_time"
198 data={props.data}
199 format="ms"
200 highlightedValue={meanBy(props.data, 'avg_execution_time')}
201 />
202 <AreaChart
203 title="Max execution time"
204 className="w-full"
205 xAxisKey="timestamp"
206 customDateFormat={selectedInterval.format}
207 yAxisKey="max_execution_time"
208 data={props.data}
209 format="ms"
210 highlightedValue={
211 maxBy(props.data, 'max_execution_time')?.max_execution_time
212 }
213 />
214 </div>
215 )
216 }}
217 />
218 <ReportWidget
219 title="Invocations"
220 tooltip="Requests made to a function are considered invocations, and each invocation may have worker logs."
221 data={combinedStatsChartData}
222 isLoading={combinedStatsResults.isLoading}
223 renderer={(props) => {
224 if (isErrorCombinedStats) {
225 return (
226 <Alert variant="warning">
227 <WarningIcon />
228 <AlertTitle>Failed to reterieve invocations</AlertTitle>
229 <AlertDescription>
230 {combinedStatsError?.message ?? 'Unknown error'}
231 </AlertDescription>
232 </Alert>
233 )
234 } else {
235 const requestData = props.data
236 .map((d: any) => [
237 {
238 status: '2xx',
239 count: d.success_count,
240 timestamp: d.timestamp,
241 },
242 {
243 status: '3xx',
244 count: d.redirect_count,
245 timestamp: d.timestamp,
246 },
247 {
248 status: '4xx',
249 count: d.client_err_count,
250 timestamp: d.timestamp,
251 },
252 {
253 status: '5xx',
254 count: d.server_err_count,
255 timestamp: d.timestamp,
256 },
257 ])
258 .flat()
259
260 const logsData = props.data
261 .map((d: any) => [
262 {
263 status: 'error',
264 count: d.log_error_count,
265 timestamp: d.timestamp,
266 },
267 {
268 status: 'info',
269 count: d.log_info_count,
270 timestamp: d.timestamp,
271 },
272 {
273 status: 'warn',
274 count: d.log_warn_count,
275 timestamp: d.timestamp,
276 },
277 ])
278 .flat()
279
280 return (
281 <div className="space-y-8">
282 <StackedBarChart
283 title="Invocation Requests"
284 className="w-full"
285 xAxisKey="timestamp"
286 yAxisKey="count"
287 stackKey="status"
288 data={requestData}
289 highlightedValue={sumBy(requestData, 'count')}
290 customDateFormat={selectedInterval.format}
291 stackColors={['brand', 'slate', 'yellow', 'red']}
292 onBarClick={() => {
293 router.push(
294 `/project/${projectRef}/functions/${functionSlug}/invocations?its=${startDate.toISOString()}`
295 )
296 }}
297 />
298 <StackedBarChart
299 title="Worker Logs"
300 className="w-full"
301 xAxisKey="timestamp"
302 yAxisKey="count"
303 stackKey="status"
304 data={logsData}
305 highlightedValue={sumBy(logsData, 'count')}
306 customDateFormat={selectedInterval.format}
307 stackColors={['red', 'brand', 'yellow']}
308 onBarClick={() => {
309 router.push(
310 `/project/${projectRef}/functions/${functionSlug}/logs?its=${startDate.toISOString()}`
311 )
312 }}
313 />
314 </div>
315 )
316 }
317 }}
318 />
319 <ReportWidget
320 title="CPU time"
321 tooltip="Average CPU time usage for the function"
322 data={combinedStatsChartData}
323 isLoading={combinedStatsResults.isLoading}
324 renderer={(props) => {
325 return isErrorCombinedStats ? (
326 <Alert variant="warning">
327 <WarningIcon />
328 <AlertTitle>Failed to retrieve CPU time</AlertTitle>
329 <AlertDescription>
330 {combinedStatsError?.message ?? 'Unknown error'}
331 </AlertDescription>
332 </Alert>
333 ) : (
334 <div className="space-y-8">
335 <AreaChart
336 title="Average CPU Time"
337 className="w-full"
338 xAxisKey="timestamp"
339 customDateFormat={selectedInterval.format}
340 yAxisKey="avg_cpu_time_used"
341 data={props.data}
342 format="ms"
343 highlightedValue={meanBy(props.data, 'avg_cpu_time_used')}
344 />
345 <AreaChart
346 title="Max CPU Time"
347 className="w-full"
348 xAxisKey="timestamp"
349 customDateFormat={selectedInterval.format}
350 yAxisKey="max_cpu_time_used"
351 data={props.data}
352 format="ms"
353 highlightedValue={maxBy(props.data, 'max_cpu_time_used')?.max_cpu_time_used}
354 />
355 </div>
356 )
357 }}
358 />
359 <ReportWidget
360 title="Memory"
361 tooltip="Average memory usage for the function"
362 data={combinedStatsChartData}
363 isLoading={combinedStatsResults.isLoading}
364 renderer={(props) => {
365 if (isErrorCombinedStats) {
366 return (
367 <Alert variant="warning">
368 <WarningIcon />
369 <AlertTitle>Failed to retrieve memory usage</AlertTitle>
370 <AlertDescription>
371 {combinedStatsError?.message ?? 'Unknown error'}
372 </AlertDescription>
373 </Alert>
374 )
375 }
376
377 const memoryData = props.data
378 .map((d: any) => [
379 {
380 type: 'heap',
381 count: d.avg_heap_memory_used,
382 timestamp: d.timestamp,
383 },
384 {
385 type: 'external',
386 count: d.avg_external_memory_used,
387 timestamp: d.timestamp,
388 },
389 ])
390 .flat()
391
392 return (
393 <div className="space-y-8">
394 <AreaChart
395 title="Average Memory Usage"
396 className="w-full"
397 xAxisKey="timestamp"
398 customDateFormat={selectedInterval.format}
399 yAxisKey="avg_memory_used"
400 data={props.data}
401 format="MB"
402 highlightedValue={meanBy(props.data, 'avg_memory_used')}
403 />
404 <StackedBarChart
405 title="Average Memory Usage by Type"
406 className="w-full"
407 xAxisKey="timestamp"
408 yAxisKey="count"
409 stackKey="type"
410 format="MB"
411 data={memoryData}
412 highlightedValue={sumBy(memoryData, 'count')}
413 customDateFormat={selectedInterval.format}
414 stackColors={['blue', 'brand']}
415 />
416 </div>
417 )
418 }}
419 />
420 </div>
421 </div>
422 </PageSectionContent>
423 </PageSection>
424 </PageContainer>
425 )
426}
427
428const PageLayout: NextPageWithLayout = () => {
429 const { hasLoaded: flagsLoaded } = useFeatureFlags()
430 const showNewOverview = useFlag('edgeFunctionsOverview') === true
431
432 if (IS_PLATFORM && !flagsLoaded) {
433 return <LogoLoader />
434 }
435
436 if (showNewOverview) {
437 return <EdgeFunctionOverview />
438 }
439
440 return <LegacyEdgeFunctionOverview />
441}
442
443PageLayout.getLayout = (page) => (
444 <DefaultLayout>
445 <EdgeFunctionDetailsLayout title="Overview">{page}</EdgeFunctionDetailsLayout>
446 </DefaultLayout>
447)
448
449export default PageLayout