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