Files
supabase/apps/studio/data/edge-functions/edge-functions-last-hour-stats-query.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

120 lines
3.7 KiB
TypeScript

import { useQuery } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { edgeFunctionsKeys } from './keys'
import { handleError } from '@/data/fetchers'
import { executeAnalyticsSql } from '@/data/logs/execute-analytics-sql'
import {
analyticsLiteral,
joinSqlFragments,
safeSql,
type SafeLogSqlFragment,
} from '@/data/logs/safe-analytics-sql'
import type { ResponseError, UseCustomQueryOptions } from '@/types'
export type EdgeFunctionsLastHourStatsVariables = { projectRef?: string; functionIds?: string[] }
export type EdgeFunctionLastHourStats = {
functionId: string
requestsCount: number
serverErrorCount: number
errorRate: number
}
export type EdgeFunctionsLastHourStatsResponse = Record<string, EdgeFunctionLastHourStats>
function getEdgeFunctionsLastHourStatsSql(functionIds: string[]): SafeLogSqlFragment {
const functionIdFilter: SafeLogSqlFragment =
functionIds.length > 0
? safeSql` and function_id in (${joinSqlFragments(functionIds.map(analyticsLiteral), ', ')})\n`
: safeSql``
return safeSql`
-- edge-functions-last-hour-stats
select
function_id,
count(distinct id) as requests_count,
count(distinct case when response.status_code >= 500 then id end) as server_err_count
from
function_edge_logs
cross join unnest(metadata) as m
cross join unnest(m.response) as response
where
function_id is not null
${functionIdFilter}group by
function_id
`
}
export async function getEdgeFunctionsLastHourStats(
{ projectRef, functionIds = [] }: EdgeFunctionsLastHourStatsVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('projectRef is required')
if (functionIds.length === 0) return {}
const endDate = dayjs().toISOString()
const startDate = dayjs().subtract(1, 'hour').toISOString()
const data = await executeAnalyticsSql({
projectRef,
endpoint: '/platform/projects/{ref}/analytics/endpoints/logs.all',
sql: getEdgeFunctionsLastHourStatsSql(functionIds),
iso_timestamp_start: startDate,
iso_timestamp_end: endDate,
key: 'last-hour-stats',
signal,
})
if (data?.error) handleError(data.error)
const result = (data?.result ?? []) as {
function_id: string
requests_count: number | string
server_err_count: number | string
}[]
return result.reduce<EdgeFunctionsLastHourStatsResponse>((acc, row) => {
const toSafeNumber = (v: number | string | undefined) => {
const n = Number(v ?? 0)
return Number.isFinite(n) ? n : 0
}
const safeRequestsCount = toSafeNumber(row.requests_count)
const safeServerErrorCount = toSafeNumber(row.server_err_count)
acc[row.function_id] = {
functionId: row.function_id,
requestsCount: safeRequestsCount,
serverErrorCount: safeServerErrorCount,
errorRate: safeRequestsCount > 0 ? (safeServerErrorCount / safeRequestsCount) * 100 : 0,
}
return acc
}, {})
}
export type EdgeFunctionsLastHourStatsData = Awaited<
ReturnType<typeof getEdgeFunctionsLastHourStats>
>
export type EdgeFunctionsLastHourStatsError = ResponseError
export const useEdgeFunctionsLastHourStatsQuery = <TData = EdgeFunctionsLastHourStatsData>(
{ projectRef, functionIds = [] }: EdgeFunctionsLastHourStatsVariables,
{
enabled = true,
...options
}: UseCustomQueryOptions<
EdgeFunctionsLastHourStatsData,
EdgeFunctionsLastHourStatsError,
TData
> = {}
) =>
useQuery<EdgeFunctionsLastHourStatsData, EdgeFunctionsLastHourStatsError, TData>({
queryKey: edgeFunctionsKeys.lastHourStats(projectRef, functionIds),
queryFn: ({ signal }) => getEdgeFunctionsLastHourStats({ projectRef, functionIds }, signal),
enabled: enabled && typeof projectRef !== 'undefined' && functionIds.length > 0,
staleTime: 60 * 1000,
retry: false,
...options,
})