Files
supabase/apps/studio/components/interfaces/Observability/usePostgrestOverviewMetrics.ts
Charis 9bdb757b6a feat(logs): brand Observability/EdgeFunctions SQL with SafeLogSqlFragment (#8) (#46466)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Refactor / security hardening — continues the analytics SQL
provenance-tracking series (PR 8).

## What is the current behavior?

- `generateRegexpWhere` (unsafe: interpolates user-controlled filter
keys/values without escaping) still exists alongside
`generateRegexpWhereSafe` and its tests only cover the old function.
- `usePostgrestOverviewMetrics` builds a SQL query string with plain
string interpolation and calls the analytics endpoint directly via
`get()`.
- `edge-functions-last-hour-stats-query` builds a SQL query with
`functionIds` escaped via Postgres-only `quoteLiteral` and calls the
analytics endpoint directly via `post()`.
- `executeAnalyticsSql` has no way to pass a `key` query-string param
for network-tool identification.
- `rawSql('minute')` / `rawSql('hour')` / `rawSql('day')` and
`rawSql(value ? 'true' : 'false')` are used for static strings that
could be expressed with the `safeSql` template tag.

## What is the new behavior?

- `generateRegexpWhere` is deleted; its tests are replaced with
`generateRegexpWhereSafe` coverage including injection-attempt cases
(`level OR id IS NOT NULL`, `request.method); DROP TABLE edge_logs; --`)
that verify predicates are silently dropped rather than emitted.
- `usePostgrestOverviewMetrics` returns `SafeLogSqlFragment` from its
SQL builder and routes through `executeAnalyticsSql`.
- `edge-functions-last-hour-stats-query` uses `analyticsLiteral`
(BigQuery/ClickHouse-correct escaping) instead of `quoteLiteral`
(Postgres-only) and routes through `executeAnalyticsSql`.
- `executeAnalyticsSql` accepts an optional `key?: string` forwarded as
a query-string param on both GET and POST requests; `key:
'last-hour-stats'` is restored on the edge-functions query.
- Static `rawSql('...')` calls replaced with `safeSql\`...\`` template
literals throughout.

## Additional context

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Bug Fixes
- Removed legacy unsafe SQL-filter utility from Reports

## Chores
- Enhanced analytics SQL execution infrastructure with improved error
handling
- Added optional request identification parameter to analytics query
execution
- Refined SQL filtering mechanisms in reporting features

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46466?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-28 10:30:57 -04:00

99 lines
2.8 KiB
TypeScript

import { useQuery } from '@tanstack/react-query'
import type { LogsBarChartDatum } from '../ProjectHome/ProjectUsage.metrics'
import { executeAnalyticsSql } from '@/data/logs/execute-analytics-sql'
import { safeSql, type SafeLogSqlFragment } from '@/data/logs/safe-analytics-sql'
type PostgrestMetricsVariables = {
projectRef: string
startDate: string
endDate: string
interval: '1hr' | '1day' | '7day'
}
function getIntervalTrunc(interval: '1hr' | '1day' | '7day'): SafeLogSqlFragment {
switch (interval) {
case '1hr':
return safeSql`minute` // 1-minute buckets for 1 hour
case '1day':
return safeSql`hour` // 1-hour buckets for 1 day
case '7day':
return safeSql`day` // 1-day buckets for 7 days
default:
return safeSql`hour`
}
}
const POSTGREST_METRICS_SQL = (interval: '1hr' | '1day' | '7day'): SafeLogSqlFragment => {
const trunc = getIntervalTrunc(interval)
return safeSql`
-- postgrest-overview-metrics
select
cast(timestamp_trunc(t.timestamp, ${trunc}) as datetime) as timestamp,
countif(response.status_code < 300) as ok_count,
countif(response.status_code >= 300 and response.status_code < 400) as warning_count,
countif(response.status_code >= 400) as error_count
FROM edge_logs t
cross join unnest(metadata) as m
cross join unnest(m.response) as response
cross join unnest(m.request) as request
WHERE
request.path like '/rest/%'
GROUP BY
timestamp
ORDER BY
timestamp ASC
`
}
type MetricsRow = {
timestamp: string
ok_count: number
warning_count: number
error_count: number
}
async function fetchPostgrestMetrics(
{ projectRef, startDate, endDate, interval }: PostgrestMetricsVariables,
signal?: AbortSignal
) {
const sql = POSTGREST_METRICS_SQL(interval)
const data = await executeAnalyticsSql({
projectRef,
endpoint: '/platform/projects/{ref}/analytics/endpoints/logs.all',
sql,
iso_timestamp_start: startDate,
iso_timestamp_end: endDate,
method: 'get',
signal,
})
if (data?.error) throw data.error
return (data?.result || []) as MetricsRow[]
}
export const usePostgrestOverviewMetrics = (
{ projectRef, startDate, endDate, interval }: PostgrestMetricsVariables,
options?: { enabled?: boolean }
) => {
return useQuery({
queryKey: ['postgrest-overview-metrics', projectRef, startDate, endDate, interval],
queryFn: ({ signal }) =>
fetchPostgrestMetrics({ projectRef, startDate, endDate, interval }, signal),
enabled: (options?.enabled ?? true) && Boolean(projectRef),
staleTime: 1000 * 60,
})
}
export const transformPostgrestMetrics = (rows: MetricsRow[]): LogsBarChartDatum[] => {
return rows.map((row) => ({
timestamp: row.timestamp,
ok_count: row.ok_count,
warning_count: row.warning_count,
error_count: row.error_count,
}))
}