Reports.constants.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817
  1. import { literal, safeSql, type SafeSqlFragment } from '@supabase/pg-meta'
  2. import dayjs from 'dayjs'
  3. import type { DatetimeHelper } from '../Settings/Logs/Logs.types'
  4. import { PresetConfig, Presets, ReportFilterItem } from './Reports.types'
  5. import { PlanId } from '@/data/subscriptions/types'
  6. export const LAYOUT_COLUMN_COUNT = 2
  7. export interface ReportsDatetimeHelper extends DatetimeHelper {
  8. availableIn: PlanId[]
  9. }
  10. export enum REPORT_DATERANGE_HELPER_LABELS {
  11. LAST_10_MINUTES = 'Last 10 minutes',
  12. LAST_30_MINUTES = 'Last 30 minutes',
  13. LAST_60_MINUTES = 'Last 60 minutes',
  14. LAST_3_HOURS = 'Last 3 hours',
  15. LAST_24_HOURS = 'Last 24 hours',
  16. LAST_7_DAYS = 'Last 7 days',
  17. LAST_14_DAYS = 'Last 14 days',
  18. LAST_28_DAYS = 'Last 28 days',
  19. }
  20. export const REPORTS_DATEPICKER_HELPERS: ReportsDatetimeHelper[] = [
  21. {
  22. text: REPORT_DATERANGE_HELPER_LABELS.LAST_10_MINUTES,
  23. calcFrom: () => dayjs().subtract(10, 'minute').toISOString(),
  24. calcTo: () => dayjs().toISOString(),
  25. availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
  26. },
  27. {
  28. text: REPORT_DATERANGE_HELPER_LABELS.LAST_30_MINUTES,
  29. calcFrom: () => dayjs().subtract(30, 'minute').toISOString(),
  30. calcTo: () => dayjs().toISOString(),
  31. availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
  32. },
  33. {
  34. text: REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES,
  35. calcFrom: () => dayjs().subtract(1, 'hour').toISOString(),
  36. calcTo: () => dayjs().toISOString(),
  37. default: true,
  38. availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
  39. },
  40. {
  41. text: REPORT_DATERANGE_HELPER_LABELS.LAST_3_HOURS,
  42. calcFrom: () => dayjs().subtract(3, 'hour').toISOString(),
  43. calcTo: () => dayjs().toISOString(),
  44. availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
  45. },
  46. {
  47. text: REPORT_DATERANGE_HELPER_LABELS.LAST_24_HOURS,
  48. calcFrom: () => dayjs().subtract(1, 'day').toISOString(),
  49. calcTo: () => dayjs().toISOString(),
  50. availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
  51. },
  52. {
  53. text: REPORT_DATERANGE_HELPER_LABELS.LAST_7_DAYS,
  54. calcFrom: () => dayjs().subtract(7, 'day').toISOString(),
  55. calcTo: () => dayjs().toISOString(),
  56. availableIn: ['pro', 'team', 'enterprise'],
  57. },
  58. {
  59. text: REPORT_DATERANGE_HELPER_LABELS.LAST_14_DAYS,
  60. calcFrom: () => dayjs().subtract(14, 'day').toISOString(),
  61. calcTo: () => dayjs().toISOString(),
  62. availableIn: ['team', 'enterprise'],
  63. },
  64. {
  65. text: REPORT_DATERANGE_HELPER_LABELS.LAST_28_DAYS,
  66. calcFrom: () => dayjs().subtract(28, 'day').toISOString(),
  67. calcTo: () => dayjs().toISOString(),
  68. availableIn: ['team', 'enterprise'],
  69. },
  70. ]
  71. export const DEFAULT_QUERY_PARAMS = {
  72. iso_timestamp_start: REPORTS_DATEPICKER_HELPERS[0].calcFrom(),
  73. iso_timestamp_end: REPORTS_DATEPICKER_HELPERS[0].calcTo(),
  74. }
  75. function rewriteWhereToAnd(sql: SafeSqlFragment): SafeSqlFragment {
  76. return sql.replace(/^WHERE/, 'AND') as SafeSqlFragment
  77. }
  78. export const generateRegexpWhere = (filters: ReportFilterItem[], prepend = true) => {
  79. if (filters.length === 0) return ''
  80. const conditions = filters
  81. .map((filter) => {
  82. const splitKey = filter.key.split('.')
  83. const normalizedKey = [splitKey[splitKey.length - 2], splitKey[splitKey.length - 1]].join('.')
  84. const filterKey = filter.key.includes('.') ? normalizedKey : filter.key
  85. const hasQuotes =
  86. filter.value.toString().includes('"') || filter.value.toString().includes("'")
  87. const valueIsNumber = !isNaN(Number(filter.value))
  88. const valueWithQuotes = !valueIsNumber && hasQuotes ? filter.value : `'${filter.value}'`
  89. const lowercaseValue = !valueIsNumber && String(valueWithQuotes).toLowerCase()
  90. const finalValue = valueIsNumber ? filter.value : lowercaseValue
  91. // Handle different comparison operators
  92. switch (filter.compare) {
  93. case 'matches':
  94. return `REGEXP_CONTAINS(${filterKey}, ${finalValue})`
  95. case 'is':
  96. return `${filterKey} = ${finalValue}`
  97. case '!=':
  98. return `${filterKey} != ${finalValue}`
  99. case '>=':
  100. return `${filterKey} >= ${finalValue}`
  101. case '<=':
  102. return `${filterKey} <= ${finalValue}`
  103. case '>':
  104. return `${filterKey} > ${finalValue}`
  105. case '<':
  106. return `${filterKey} < ${finalValue}`
  107. default:
  108. // Fallback to exact match for unknown operators
  109. return `${filterKey} = ${finalValue}`
  110. }
  111. })
  112. .filter(Boolean) // Remove any null/undefined conditions
  113. .join(' AND ')
  114. if (conditions === '') return ''
  115. if (prepend) {
  116. return 'WHERE ' + conditions
  117. } else {
  118. return 'AND ' + conditions
  119. }
  120. }
  121. export const PRESET_CONFIG: Record<Presets, PresetConfig> = {
  122. [Presets.API]: {
  123. title: 'API',
  124. queries: {
  125. totalRequests: {
  126. queryType: 'logs',
  127. sql: (filters) => `
  128. -- reports-api-total-requests
  129. select
  130. cast(timestamp_trunc(t.timestamp, hour) as datetime) as timestamp,
  131. count(t.id) as count
  132. FROM edge_logs t
  133. cross join unnest(metadata) as m
  134. cross join unnest(m.response) as response
  135. cross join unnest(m.request) as request
  136. cross join unnest(request.headers) as headers
  137. ${generateRegexpWhere(filters)}
  138. GROUP BY
  139. timestamp
  140. ORDER BY
  141. timestamp ASC`,
  142. },
  143. topRoutes: {
  144. queryType: 'logs',
  145. sql: (filters) => `
  146. -- reports-api-top-routes
  147. select
  148. request.path as path,
  149. request.method as method,
  150. request.search as search,
  151. response.status_code as status_code,
  152. count(t.id) as count
  153. from edge_logs t
  154. cross join unnest(metadata) as m
  155. cross join unnest(m.response) as response
  156. cross join unnest(m.request) as request
  157. cross join unnest(request.headers) as headers
  158. ${generateRegexpWhere(filters)}
  159. group by
  160. request.path, request.method, request.search, response.status_code
  161. order by
  162. count desc
  163. limit 10
  164. `,
  165. },
  166. errorCounts: {
  167. queryType: 'logs',
  168. sql: (filters) => `
  169. -- reports-api-error-counts
  170. select
  171. cast(timestamp_trunc(t.timestamp, hour) as datetime) as timestamp,
  172. count(t.id) as count
  173. FROM edge_logs t
  174. cross join unnest(metadata) as m
  175. cross join unnest(m.response) as response
  176. cross join unnest(m.request) as request
  177. cross join unnest(request.headers) as headers
  178. WHERE
  179. response.status_code >= 400
  180. ${generateRegexpWhere(filters, false)}
  181. GROUP BY
  182. timestamp
  183. ORDER BY
  184. timestamp ASC
  185. `,
  186. },
  187. topErrorRoutes: {
  188. queryType: 'logs',
  189. sql: (filters) => `
  190. -- reports-api-top-error-routes
  191. select
  192. request.path as path,
  193. request.method as method,
  194. request.search as search,
  195. response.status_code as status_code,
  196. count(t.id) as count
  197. from edge_logs t
  198. cross join unnest(metadata) as m
  199. cross join unnest(m.response) as response
  200. cross join unnest(m.request) as request
  201. cross join unnest(request.headers) as headers
  202. where
  203. response.status_code >= 400
  204. ${generateRegexpWhere(filters, false)}
  205. group by
  206. request.path, request.method, request.search, response.status_code
  207. order by
  208. count desc
  209. limit 10
  210. `,
  211. },
  212. responseSpeed: {
  213. queryType: 'logs',
  214. sql: (filters) => `
  215. -- reports-api-response-speed
  216. select
  217. cast(timestamp_trunc(t.timestamp, hour) as datetime) as timestamp,
  218. avg(response.origin_time) as avg
  219. FROM
  220. edge_logs t
  221. cross join unnest(metadata) as m
  222. cross join unnest(m.response) as response
  223. cross join unnest(m.request) as request
  224. cross join unnest(request.headers) as headers
  225. ${generateRegexpWhere(filters)}
  226. GROUP BY
  227. timestamp
  228. ORDER BY
  229. timestamp ASC
  230. `,
  231. },
  232. topSlowRoutes: {
  233. queryType: 'logs',
  234. sql: (filters) => `
  235. -- reports-api-top-slow-routes
  236. select
  237. request.path as path,
  238. request.method as method,
  239. request.search as search,
  240. response.status_code as status_code,
  241. count(t.id) as count,
  242. avg(response.origin_time) as avg
  243. from edge_logs t
  244. cross join unnest(metadata) as m
  245. cross join unnest(m.response) as response
  246. cross join unnest(m.request) as request
  247. cross join unnest(request.headers) as headers
  248. ${generateRegexpWhere(filters)}
  249. group by
  250. request.path, request.method, request.search, response.status_code
  251. order by
  252. avg desc
  253. limit 10
  254. `,
  255. },
  256. networkTraffic: {
  257. queryType: 'logs',
  258. sql: (filters) => `
  259. -- reports-api-network-traffic
  260. select
  261. cast(timestamp_trunc(t.timestamp, hour) as datetime) as timestamp,
  262. coalesce(
  263. safe_divide(
  264. sum(
  265. cast(coalesce(headers.content_length, "0") as int64)
  266. ),
  267. 1000000
  268. ),
  269. 0
  270. ) as ingress_mb,
  271. coalesce(
  272. safe_divide(
  273. sum(
  274. cast(coalesce(resp_headers.content_length, "0") as int64)
  275. ),
  276. 1000000
  277. ),
  278. 0
  279. ) as egress_mb,
  280. FROM
  281. edge_logs t
  282. cross join unnest(metadata) as m
  283. cross join unnest(m.response) as response
  284. cross join unnest(m.request) as request
  285. cross join unnest(request.headers) as headers
  286. cross join unnest(response.headers) as resp_headers
  287. ${generateRegexpWhere(filters)}
  288. GROUP BY
  289. timestamp
  290. ORDER BY
  291. timestamp ASC
  292. `,
  293. },
  294. requestsByCountry: {
  295. queryType: 'logs',
  296. sql: (filters) => `
  297. -- reports-api-requests-by-country
  298. select
  299. cf.country as country,
  300. count(t.id) as count
  301. from edge_logs t
  302. cross join unnest(metadata) as m
  303. cross join unnest(m.response) as response
  304. cross join unnest(m.request) as request
  305. cross join unnest(request.headers) as headers
  306. cross join unnest(request.cf) as cf
  307. where
  308. cf.country is not null
  309. ${generateRegexpWhere(filters, false)}
  310. group by
  311. cf.country
  312. `,
  313. },
  314. },
  315. },
  316. [Presets.AUTH]: {
  317. title: '',
  318. queries: {},
  319. },
  320. [Presets.STORAGE]: {
  321. title: 'Storage',
  322. queries: {
  323. cacheHitRate: {
  324. queryType: 'logs',
  325. // storage report does not perform any filtering
  326. sql: (filters) => `
  327. -- reports-storage-cache-hit-rate
  328. SELECT
  329. timestamp_trunc(timestamp, hour) as timestamp,
  330. countif( h.cf_cache_status in ('HIT', 'STALE', 'REVALIDATED', 'UPDATING') ) as hit_count,
  331. countif( h.cf_cache_status in ('MISS', 'NONE/UNKNOWN', 'EXPIRED', 'BYPASS', 'DYNAMIC') ) as miss_count
  332. from edge_logs f
  333. cross join unnest(f.metadata) as m
  334. cross join unnest(m.request) as r
  335. cross join unnest(m.response) as res
  336. cross join unnest(res.headers) as h
  337. where starts_with(r.path, '/storage/v1/object') and r.method = 'GET'
  338. ${generateRegexpWhere(filters, false)}
  339. group by timestamp
  340. order by timestamp desc
  341. `,
  342. },
  343. topCacheMisses: {
  344. queryType: 'logs',
  345. // storage report does not perform any filtering
  346. sql: (filters) => `
  347. -- reports-storage-top-cache-misses
  348. SELECT
  349. r.path as path,
  350. r.search as search,
  351. count(id) as count
  352. from edge_logs f
  353. cross join unnest(f.metadata) as m
  354. cross join unnest(m.request) as r
  355. cross join unnest(m.response) as res
  356. cross join unnest(res.headers) as h
  357. where starts_with(r.path, '/storage/v1/object')
  358. and r.method = 'GET'
  359. and h.cf_cache_status in ('MISS', 'NONE/UNKNOWN', 'EXPIRED', 'BYPASS', 'DYNAMIC')
  360. ${generateRegexpWhere(filters, false)}
  361. group by path, search
  362. order by count desc
  363. limit 12
  364. `,
  365. },
  366. },
  367. },
  368. [Presets.QUERY_PERFORMANCE]: {
  369. title: 'Query performance',
  370. queries: {
  371. mostFrequentlyInvoked: {
  372. queryType: 'db',
  373. safeSql: (
  374. _params,
  375. where,
  376. orderBy,
  377. runIndexAdvisor = false,
  378. _filterIndexAdvisor = false
  379. ) => safeSql`
  380. -- reports-query-performance-most-frequently-invoked
  381. set search_path to public, extensions;
  382. select
  383. auth.rolname,
  384. statements.query,
  385. statements.calls,
  386. -- -- Postgres 13, 14, 15
  387. statements.total_exec_time + statements.total_plan_time as total_time,
  388. statements.min_exec_time + statements.min_plan_time as min_time,
  389. statements.max_exec_time + statements.max_plan_time as max_time,
  390. statements.mean_exec_time + statements.mean_plan_time as mean_time,
  391. -- -- Postgres <= 12
  392. -- total_time,
  393. -- min_time,
  394. -- max_time,
  395. -- mean_time,
  396. coalesce(statements.rows::numeric / nullif(statements.calls, 0), 0) as avg_rows,
  397. statements.rows as rows_read,
  398. case
  399. when (statements.shared_blks_hit + statements.shared_blks_read) > 0
  400. then round(
  401. (statements.shared_blks_hit * 100.0) /
  402. (statements.shared_blks_hit + statements.shared_blks_read),
  403. 2
  404. )
  405. else 0
  406. end as cache_hit_rate${
  407. runIndexAdvisor
  408. ? safeSql`,
  409. case
  410. when (lower(statements.query) like 'select%' or lower(statements.query) like 'with pgrst%')
  411. then (
  412. select json_build_object(
  413. 'has_suggestion', array_length(index_statements, 1) > 0,
  414. 'startup_cost_before', startup_cost_before,
  415. 'startup_cost_after', startup_cost_after,
  416. 'total_cost_before', total_cost_before,
  417. 'total_cost_after', total_cost_after,
  418. 'index_statements', index_statements
  419. )
  420. from index_advisor(statements.query)
  421. )
  422. else null
  423. end as index_advisor_result`
  424. : safeSql``
  425. }
  426. from pg_stat_statements as statements
  427. inner join pg_authid as auth on statements.userid = auth.oid
  428. -- skip queries that were never actually executed
  429. WHERE statements.calls > 0 ${where ? rewriteWhereToAnd(where) : safeSql``}
  430. ${orderBy || safeSql`order by statements.calls desc`}
  431. limit 20`,
  432. },
  433. mostTimeConsuming: {
  434. queryType: 'db',
  435. safeSql: (
  436. _,
  437. where,
  438. orderBy,
  439. runIndexAdvisor = false,
  440. _filterIndexAdvisor = false
  441. ) => safeSql`
  442. -- reports-query-performance-most-time-consuming
  443. set search_path to public, extensions;
  444. -- compute total time once up front so we don't need a window function over all rows
  445. with grand_total as (
  446. select coalesce(nullif(sum(total_exec_time + total_plan_time), 0), 1) as v
  447. from pg_stat_statements where calls > 0
  448. )
  449. select
  450. auth.rolname,
  451. statements.query,
  452. statements.calls,
  453. statements.total_exec_time + statements.total_plan_time as total_time,
  454. statements.mean_exec_time + statements.mean_plan_time as mean_time,
  455. coalesce(
  456. ((statements.total_exec_time + statements.total_plan_time) /
  457. (select v from grand_total)) *
  458. 100,
  459. 0
  460. ) as prop_total_time${
  461. runIndexAdvisor
  462. ? safeSql`,
  463. case
  464. when (lower(statements.query) like 'select%' or lower(statements.query) like 'with pgrst%')
  465. then (
  466. select json_build_object(
  467. 'has_suggestion', array_length(index_statements, 1) > 0,
  468. 'startup_cost_before', startup_cost_before,
  469. 'startup_cost_after', startup_cost_after,
  470. 'total_cost_before', total_cost_before,
  471. 'total_cost_after', total_cost_after,
  472. 'index_statements', index_statements
  473. )
  474. from index_advisor(statements.query)
  475. )
  476. else null
  477. end as index_advisor_result`
  478. : safeSql``
  479. }
  480. from pg_stat_statements as statements
  481. inner join pg_authid as auth on statements.userid = auth.oid
  482. -- skip queries that were never actually executed
  483. WHERE statements.calls > 0 ${where ? rewriteWhereToAnd(where) : safeSql``}
  484. ${orderBy || safeSql`order by total_time desc`}
  485. limit 20`,
  486. },
  487. slowestExecutionTime: {
  488. queryType: 'db',
  489. safeSql: (
  490. _params,
  491. where,
  492. orderBy,
  493. runIndexAdvisor = false,
  494. _filterIndexAdvisor = false
  495. ) => safeSql`
  496. -- reports-query-performance-slowest-execution-time
  497. set search_path to public, extensions;
  498. select
  499. auth.rolname,
  500. statements.query,
  501. statements.calls,
  502. -- -- Postgres 13, 14, 15
  503. statements.total_exec_time + statements.total_plan_time as total_time,
  504. statements.min_exec_time + statements.min_plan_time as min_time,
  505. statements.max_exec_time + statements.max_plan_time as max_time,
  506. statements.mean_exec_time + statements.mean_plan_time as mean_time,
  507. -- -- Postgres <= 12
  508. -- total_time,
  509. -- min_time,
  510. -- max_time,
  511. -- mean_time,
  512. coalesce(statements.rows::numeric / nullif(statements.calls, 0), 0) as avg_rows${
  513. runIndexAdvisor
  514. ? safeSql`,
  515. case
  516. when (lower(statements.query) like 'select%' or lower(statements.query) like 'with pgrst%')
  517. then (
  518. select json_build_object(
  519. 'has_suggestion', array_length(index_statements, 1) > 0,
  520. 'startup_cost_before', startup_cost_before,
  521. 'startup_cost_after', startup_cost_after,
  522. 'total_cost_before', total_cost_before,
  523. 'total_cost_after', total_cost_after,
  524. 'index_statements', index_statements
  525. )
  526. from index_advisor(statements.query)
  527. )
  528. else null
  529. end as index_advisor_result`
  530. : safeSql``
  531. }
  532. from pg_stat_statements as statements
  533. inner join pg_authid as auth on statements.userid = auth.oid
  534. -- skip queries that were never actually executed
  535. WHERE statements.calls > 0 ${where ? rewriteWhereToAnd(where) : safeSql``}
  536. ${orderBy || safeSql`order by max_time desc`}
  537. limit 20`,
  538. },
  539. queryHitRate: {
  540. queryType: 'db',
  541. safeSql: (_params) => safeSql`-- reports-query-performance-cache-and-index-hit-rate
  542. select
  543. 'index hit rate' as name,
  544. (sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read),0) as ratio
  545. from pg_statio_user_indexes
  546. union all
  547. select
  548. 'table hit rate' as name,
  549. sum(heap_blks_hit) / nullif(sum(heap_blks_hit) + sum(heap_blks_read),0) as ratio
  550. from pg_statio_user_tables;`,
  551. },
  552. unified: {
  553. queryType: 'db',
  554. safeSql: (
  555. _params,
  556. where,
  557. orderBy,
  558. runIndexAdvisor = false,
  559. filterIndexAdvisor = false,
  560. page = 1,
  561. pageSize = 20
  562. ) => {
  563. const offset = (page - 1) * pageSize
  564. // When filtering by index suggestions we need a larger scan window since we don't
  565. // know how many rows will match. Cap at a reasonable upper bound to avoid running
  566. // index_advisor() across the entire dataset on any code path where it's active.
  567. const INDEX_ADVISOR_SCAN_CAP = 500
  568. const baseScanTarget =
  569. filterIndexAdvisor && runIndexAdvisor ? offset + pageSize * 10 : offset + pageSize
  570. const baseCteLimit = runIndexAdvisor
  571. ? Math.min(baseScanTarget, INDEX_ADVISOR_SCAN_CAP)
  572. : baseScanTarget
  573. const baseQuery = safeSql`
  574. -- reports-query-performance-unified
  575. set search_path to public, extensions;
  576. -- compute total time once up front so we don't need a window function over all rows
  577. with grand_total as (
  578. select coalesce(nullif(sum(total_exec_time + total_plan_time), 0), 1) as v
  579. from pg_stat_statements where calls > 0
  580. ),
  581. base as (
  582. select
  583. auth.rolname,
  584. statements.query,
  585. statements.calls,
  586. statements.total_exec_time + statements.total_plan_time as total_time,
  587. statements.min_exec_time + statements.min_plan_time as min_time,
  588. statements.max_exec_time + statements.max_plan_time as max_time,
  589. statements.mean_exec_time + statements.mean_plan_time as mean_time,
  590. coalesce(statements.rows::numeric / nullif(statements.calls, 0), 0) as avg_rows,
  591. statements.rows as rows_read,
  592. statements.shared_blks_hit as debug_hit,
  593. statements.shared_blks_read as debug_read,
  594. case
  595. when (statements.shared_blks_hit + statements.shared_blks_read) > 0
  596. then (statements.shared_blks_hit::numeric * 100.0) /
  597. (statements.shared_blks_hit + statements.shared_blks_read)
  598. else 0
  599. end as cache_hit_rate,
  600. coalesce(
  601. ((statements.total_exec_time + statements.total_plan_time) /
  602. (select v from grand_total)) *
  603. 100,
  604. 0
  605. ) as prop_total_time
  606. from pg_stat_statements as statements
  607. inner join pg_authid as auth on statements.userid = auth.oid
  608. -- skip queries that were never actually executed
  609. WHERE statements.calls > 0 ${where ? rewriteWhereToAnd(where) : safeSql``}
  610. ${orderBy || safeSql`order by total_time desc`}
  611. ${baseCteLimit !== null ? safeSql`limit ${literal(baseCteLimit)}` : safeSql``}
  612. ),
  613. query_results as (
  614. select
  615. base.*${
  616. runIndexAdvisor
  617. ? safeSql`,
  618. case
  619. when (lower(base.query) like 'select%' or lower(base.query) like 'with pgrst%')
  620. then (
  621. select json_build_object(
  622. 'has_suggestion', array_length(index_statements, 1) > 0,
  623. 'startup_cost_before', startup_cost_before,
  624. 'startup_cost_after', startup_cost_after,
  625. 'total_cost_before', total_cost_before,
  626. 'total_cost_after', total_cost_after,
  627. 'index_statements', index_statements
  628. )
  629. from index_advisor(base.query)
  630. )
  631. else null
  632. end as index_advisor_result`
  633. : safeSql``
  634. }
  635. from base
  636. )
  637. select *
  638. from query_results
  639. ${filterIndexAdvisor && runIndexAdvisor ? safeSql`where (index_advisor_result->>'has_suggestion')::boolean = true` : safeSql``}
  640. ${orderBy || safeSql`order by total_time desc`}
  641. limit ${literal(pageSize)} offset ${literal(offset)}`
  642. return baseQuery
  643. },
  644. },
  645. slowQueriesCount: {
  646. queryType: 'db',
  647. safeSql: () => safeSql`
  648. -- reports-query-performance-slow-queries-count
  649. set search_path to public, extensions;
  650. -- Count of slow queries (> 1 second average)
  651. SELECT count(*) as slow_queries_count
  652. -- alias needed to reference columns in WHERE
  653. FROM pg_stat_statements as statements
  654. -- skip never-executed queries; mean_exec_time > 1000ms = avg over 1 second
  655. WHERE statements.calls > 0 AND statements.mean_exec_time > 1000;`,
  656. },
  657. queryMetrics: {
  658. queryType: 'db',
  659. safeSql: (
  660. _params,
  661. where,
  662. orderBy,
  663. _runIndexAdvisor = false,
  664. _filterIndexAdvisor = false
  665. ) => safeSql`
  666. -- reports-query-performance-metrics
  667. set search_path to public, extensions;
  668. SELECT
  669. COALESCE(ROUND(AVG(statements.rows::numeric / NULLIF(statements.calls, 0)), 1), 0) as avg_rows_per_call,
  670. COUNT(*) FILTER (WHERE statements.total_exec_time + statements.total_plan_time > 1000) as slow_queries,
  671. COALESCE(
  672. ROUND(
  673. SUM(statements.shared_blks_hit) * 100.0 /
  674. NULLIF(SUM(statements.shared_blks_hit + statements.shared_blks_read), 0),
  675. 2
  676. ), 0
  677. ) || '%' as cache_hit_rate
  678. FROM pg_stat_statements as statements
  679. -- skip queries that were never actually executed
  680. WHERE statements.calls > 0 ${where ? rewriteWhereToAnd(where) : safeSql``}
  681. ${orderBy || safeSql``}`,
  682. },
  683. },
  684. },
  685. [Presets.DATABASE]: {
  686. title: 'database',
  687. queries: {
  688. largeObjects: {
  689. queryType: 'db',
  690. safeSql: (_) => safeSql`-- reports-database-large-objects
  691. SELECT
  692. SCHEMA_NAME,
  693. relname,
  694. table_size
  695. FROM
  696. (SELECT
  697. pg_catalog.pg_namespace.nspname AS SCHEMA_NAME,
  698. relname,
  699. pg_total_relation_size(pg_catalog.pg_class.oid) AS table_size
  700. FROM pg_catalog.pg_class
  701. JOIN pg_catalog.pg_namespace ON relnamespace = pg_catalog.pg_namespace.oid
  702. ) t
  703. WHERE SCHEMA_NAME NOT LIKE 'pg_%'
  704. ORDER BY table_size DESC
  705. LIMIT 5;`,
  706. },
  707. },
  708. },
  709. }
  710. export const DEPRECATED_REPORTS = [
  711. 'total_realtime_ingress',
  712. 'total_rest_options_requests',
  713. 'total_auth_ingress',
  714. 'total_auth_get_requests',
  715. 'total_auth_post_requests',
  716. 'total_auth_patch_requests',
  717. 'total_auth_options_requests',
  718. 'total_storage_options_requests',
  719. 'total_storage_patch_requests',
  720. 'total_options_requests',
  721. 'total_rest_ingress',
  722. 'total_rest_get_requests',
  723. 'total_rest_post_requests',
  724. 'total_rest_patch_requests',
  725. 'total_rest_delete_requests',
  726. 'total_storage_get_requests',
  727. 'total_storage_post_requests',
  728. 'total_storage_delete_requests',
  729. 'total_auth_delete_requests',
  730. 'total_get_requests',
  731. 'total_patch_requests',
  732. 'total_post_requests',
  733. 'total_ingress',
  734. 'total_delete_requests',
  735. ]
  736. export const EDGE_FUNCTION_REGIONS = [
  737. {
  738. key: 'ap-northeast-1',
  739. label: 'Tokyo',
  740. },
  741. {
  742. key: 'ap-northeast-2',
  743. label: 'Seoul',
  744. },
  745. {
  746. key: 'ap-south-1',
  747. label: 'Mumbai',
  748. },
  749. {
  750. key: 'ap-southeast-1',
  751. label: 'Singapore',
  752. },
  753. {
  754. key: 'ap-southeast-2',
  755. label: 'Sydney',
  756. },
  757. {
  758. key: 'ca-central-1',
  759. label: 'Canada Central',
  760. },
  761. {
  762. key: 'us-east-1',
  763. label: 'N. Virginia',
  764. },
  765. {
  766. key: 'us-west-1',
  767. label: 'N. California',
  768. },
  769. {
  770. key: 'us-west-2',
  771. label: 'Oregon',
  772. },
  773. {
  774. key: 'eu-central-1',
  775. label: 'Frankfurt',
  776. },
  777. {
  778. key: 'eu-west-1',
  779. label: 'Ireland',
  780. },
  781. {
  782. key: 'eu-west-2',
  783. label: 'London',
  784. },
  785. {
  786. key: 'eu-west-3',
  787. label: 'Paris',
  788. },
  789. {
  790. key: 'sa-east-1',
  791. label: 'São Paulo',
  792. },
  793. ] as const