SharedAPIReport.constants.ts375 lines · main
1import * as Sentry from '@sentry/nextjs'
2import { useQueries, useQueryClient } from '@tanstack/react-query'
3import { useParams } from 'common'
4import { isEqual } from 'lodash'
5import { useState } from 'react'
6
7import { generateRegexpWhere } from '../Reports.constants'
8import { ReportFilterItem } from '../Reports.types'
9import { get } from '@/data/fetchers'
10
11export const SHARED_API_REPORT_SQL = {
12 totalRequests: {
13 queryType: 'logs',
14 sql: (filters: ReportFilterItem[], src = 'edge_logs') => `
15 --reports-api-total-requests
16 select
17 cast(timestamp_trunc(t.timestamp, hour) as datetime) as timestamp,
18 count(t.id) as count
19 FROM ${src} t
20 cross join unnest(metadata) as m
21 cross join unnest(m.response) as response
22 cross join unnest(m.request) as request
23 cross join unnest(request.headers) as headers
24 ${generateRegexpWhere(filters)}
25 GROUP BY
26 timestamp
27 ORDER BY
28 timestamp ASC`,
29 },
30 topRoutes: {
31 queryType: 'logs',
32 sql: (filters: ReportFilterItem[], src = 'edge_logs') => `
33 -- reports-api-top-routes
34 select
35 request.path as path,
36 request.method as method,
37 request.search as search,
38 response.status_code as status_code,
39 count(t.id) as count
40 from ${src} t
41 cross join unnest(metadata) as m
42 cross join unnest(m.response) as response
43 cross join unnest(m.request) as request
44 cross join unnest(request.headers) as headers
45 ${generateRegexpWhere(filters)}
46 group by
47 request.path, request.method, request.search, response.status_code
48 order by
49 count desc
50 limit 10
51 `,
52 },
53 errorCounts: {
54 queryType: 'logs',
55 sql: (filters: ReportFilterItem[], src = 'edge_logs') => `
56 -- reports-api-error-counts
57 select
58 cast(timestamp_trunc(t.timestamp, hour) as datetime) as timestamp,
59 count(t.id) as count
60 FROM ${src} t
61 cross join unnest(metadata) as m
62 cross join unnest(m.response) as response
63 cross join unnest(m.request) as request
64 cross join unnest(request.headers) as headers
65 WHERE
66 response.status_code >= 400
67 ${generateRegexpWhere(filters, false)}
68 GROUP BY
69 timestamp
70 ORDER BY
71 timestamp ASC
72 `,
73 },
74 topErrorRoutes: {
75 queryType: 'logs',
76 sql: (filters: ReportFilterItem[], src = 'edge_logs') => `
77 -- reports-api-top-error-routes
78 select
79 request.path as path,
80 request.method as method,
81 request.search as search,
82 response.status_code as status_code,
83 count(t.id) as count
84 from ${src} t
85 cross join unnest(metadata) as m
86 cross join unnest(m.response) as response
87 cross join unnest(m.request) as request
88 cross join unnest(request.headers) as headers
89 where
90 response.status_code >= 400
91 ${generateRegexpWhere(filters, false)}
92 group by
93 request.path, request.method, request.search, response.status_code
94 order by
95 count desc
96 limit 10
97 `,
98 },
99 responseSpeed: {
100 queryType: 'logs',
101 sql: (filters: ReportFilterItem[], src = 'edge_logs') => `
102 -- reports-api-response-speed
103 select
104 cast(timestamp_trunc(t.timestamp, hour) as datetime) as timestamp,
105 avg(response.origin_time) as avg
106 FROM
107 ${src} t
108 cross join unnest(metadata) as m
109 cross join unnest(m.response) as response
110 cross join unnest(m.request) as request
111 cross join unnest(request.headers) as headers
112 ${generateRegexpWhere(filters)}
113 GROUP BY
114 timestamp
115 ORDER BY
116 timestamp ASC
117 `,
118 },
119 topSlowRoutes: {
120 queryType: 'logs',
121 sql: (filters: ReportFilterItem[], src = 'edge_logs') => `
122 -- reports-api-top-slow-routes
123 select
124 request.path as path,
125 request.method as method,
126 request.search as search,
127 response.status_code as status_code,
128 count(t.id) as count,
129 avg(response.origin_time) as avg
130 from ${src} t
131 cross join unnest(metadata) as m
132 cross join unnest(m.response) as response
133 cross join unnest(m.request) as request
134 cross join unnest(request.headers) as headers
135 ${generateRegexpWhere(filters)}
136 group by
137 request.path, request.method, request.search, response.status_code
138 order by
139 avg desc
140 limit 10
141 `,
142 },
143 networkTraffic: {
144 queryType: 'logs',
145 sql: (filters: ReportFilterItem[], src = 'edge_logs') => `
146 -- reports-api-network-traffic
147 select
148 cast(timestamp_trunc(t.timestamp, hour) as datetime) as timestamp,
149 coalesce(
150 safe_divide(
151 sum(
152 cast(coalesce(headers.content_length, "0") as int64)
153 ),
154 1000000
155 ),
156 0
157 ) as ingress_mb,
158 coalesce(
159 safe_divide(
160 sum(
161 cast(coalesce(resp_headers.content_length, "0") as int64)
162 ),
163 1000000
164 ),
165 0
166 ) as egress_mb,
167 FROM
168 ${src} t
169 cross join unnest(metadata) as m
170 cross join unnest(m.response) as response
171 cross join unnest(m.request) as request
172 cross join unnest(request.headers) as headers
173 cross join unnest(response.headers) as resp_headers
174 ${generateRegexpWhere(filters)}
175 GROUP BY
176 timestamp
177 ORDER BY
178 timestamp ASC
179 `,
180 },
181}
182
183export type SharedAPIReportKey = keyof typeof SHARED_API_REPORT_SQL
184
185const fetchLogs = async ({
186 projectRef,
187 sql,
188 start,
189 end,
190}: {
191 projectRef: string
192 sql: string
193 start: string
194 end: string
195}) => {
196 const { data, error } = await get(`/platform/projects/{ref}/analytics/endpoints/logs.all`, {
197 params: {
198 path: { ref: projectRef },
199 query: {
200 sql,
201 iso_timestamp_start: start,
202 iso_timestamp_end: end,
203 },
204 },
205 })
206
207 if (error || data?.error) {
208 Sentry.captureException({
209 message: 'Shared API Report Error',
210 data: {
211 error,
212 data,
213 },
214 })
215 throw error || data?.error
216 }
217
218 return data
219}
220
221const DEFAULT_KEYS = ['shared-api-report']
222
223export type SharedAPIReportFilterBy =
224 | 'auth'
225 | 'realtime'
226 | 'storage'
227 | 'graphql'
228 | 'functions'
229 | 'postgrest'
230type SharedAPIReportParams = {
231 filterBy: SharedAPIReportFilterBy
232 start: string
233 end: string
234 projectRef: string
235 enabled?: boolean
236}
237export const useSharedAPIReport = ({
238 filterBy,
239 start,
240 end,
241 enabled = true,
242}: Omit<SharedAPIReportParams, 'projectRef'>) => {
243 const { ref } = useParams() as { ref: string }
244 const [filters, setFilters] = useState<ReportFilterItem[]>([])
245 const queryClient = useQueryClient()
246 const filterByMapSource = {
247 functions: 'function_edge_logs',
248 realtime: 'edge_logs',
249 storage: 'edge_logs',
250 graphql: 'edge_logs',
251 postgrest: 'edge_logs',
252 auth: 'edge_logs',
253 }
254
255 const filterByMapValue = {
256 functions: '/functions',
257 realtime: '/realtime',
258 storage: '/storage',
259 graphql: '/graphql',
260 postgrest: '/rest',
261 auth: '/auth',
262 }
263
264 const baseFilter = {
265 key: 'request.path',
266 value: filterByMapValue[filterBy],
267 compare: 'matches' as const,
268 }
269
270 const allFilters = [baseFilter, ...filters]
271
272 const queries = useQueries({
273 queries: Object.entries(SHARED_API_REPORT_SQL).map(([key, value]) => ({
274 queryKey: [
275 ...DEFAULT_KEYS,
276 filterBy,
277 key,
278 filterByMapSource[filterBy],
279 filters,
280 start,
281 end,
282 ref,
283 ],
284 enabled: enabled && !!ref && !!filterBy,
285 queryFn: () =>
286 fetchLogs({
287 projectRef: ref,
288 sql: value.sql(allFilters, filterByMapSource[filterBy]),
289 start,
290 end,
291 }),
292 })),
293 })
294
295 const keys = Object.keys(SHARED_API_REPORT_SQL) as Array<keyof typeof SHARED_API_REPORT_SQL>
296
297 const data = keys.reduce(
298 (acc, key, i) => {
299 acc[key] = queries[i].data?.result || []
300 return acc
301 },
302 {} as { [K in keyof typeof SHARED_API_REPORT_SQL]: unknown[] }
303 )
304
305 const error = keys.reduce(
306 (acc, key, i) => {
307 acc[key] = queries[i].error as unknown as string
308 return acc
309 },
310 {} as { [K in keyof typeof SHARED_API_REPORT_SQL]: string }
311 )
312
313 const isLoading = keys.reduce(
314 (acc, key, i) => {
315 acc[key] = queries[i].isLoading
316 return acc
317 },
318 {} as { [K in keyof typeof SHARED_API_REPORT_SQL]: boolean }
319 )
320 const addFilter = (filter: ReportFilterItem) => {
321 if (isEqual(filter, baseFilter)) return
322 if (filters.some((f) => isEqual(f, filter))) return
323 setFilters((prev) =>
324 [...prev, filter].sort((a, b) => {
325 const keyA = a.key.toLowerCase()
326 const keyB = b.key.toLowerCase()
327 if (keyA < keyB) {
328 return -1
329 }
330 if (keyA > keyB) {
331 return 1
332 }
333 return 0
334 })
335 )
336 }
337
338 const removeFilters = (toRemove: ReportFilterItem[]) => {
339 setFilters((prev) => prev.filter((f) => !toRemove.find((r) => isEqual(f, r))))
340 }
341
342 const isLoadingData = Object.values(isLoading).some(Boolean)
343
344 const SQLMap: Record<SharedAPIReportKey, string> = {
345 totalRequests: SHARED_API_REPORT_SQL.totalRequests.sql(allFilters, filterByMapSource[filterBy]),
346 topRoutes: SHARED_API_REPORT_SQL.topRoutes.sql(allFilters, filterByMapSource[filterBy]),
347 errorCounts: SHARED_API_REPORT_SQL.errorCounts.sql(allFilters, filterByMapSource[filterBy]),
348 topErrorRoutes: SHARED_API_REPORT_SQL.topErrorRoutes.sql(
349 allFilters,
350 filterByMapSource[filterBy]
351 ),
352 responseSpeed: SHARED_API_REPORT_SQL.responseSpeed.sql(allFilters, filterByMapSource[filterBy]),
353 topSlowRoutes: SHARED_API_REPORT_SQL.topSlowRoutes.sql(allFilters, filterByMapSource[filterBy]),
354 networkTraffic: SHARED_API_REPORT_SQL.networkTraffic.sql(
355 allFilters,
356 filterByMapSource[filterBy]
357 ),
358 }
359
360 return {
361 data,
362 error,
363 isLoading,
364 isLoadingData,
365 isRefetching: queryClient.isFetching({ queryKey: DEFAULT_KEYS }) > 0 || false,
366 refetch: () => queryClient.invalidateQueries({ queryKey: DEFAULT_KEYS }),
367 filters,
368 addFilter,
369 removeFilters,
370 /**
371 * The SQL queries used to fetch each metric
372 */
373 sql: SQLMap,
374 }
375}