SharedAPIReport.constants.ts 10 KB

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