Files
supabase/apps/studio/components/interfaces/Auth/Overview/OverviewErrors.constants.ts
Charis fd1f437eca feat(logs): brand remaining analytics SQL callers with SafeLogSqlFragment (#46476)
## Summary

PR 10 of the analytics SQL safety series. Migrates the last surface of
analytics queries that flowed through plain
`get(.../analytics/endpoints/logs.all, { query: { sql } })` or the
`fetchLogs(projectRef, sql: string, ...)` helper over to
`executeAnalyticsSql` with branded `SafeLogSqlFragment` inputs.

After this PR, every analytics SQL call site builds its query through
the safe-analytics-sql helpers and hits the wire through the single
`executeAnalyticsSql` boundary. User-controlled values (filter
operators, numeric thresholds, function IDs, regions, provider names)
all flow through `analyticsLiteral` / branded operator maps; static
fragments are wrapped in `safeSql`. PR 11 (ESLint / vitest rule
forbidding direct analytics-endpoint POST/GET outside
`executeAnalyticsSql`) is the next and final step.

## Changes

- **`hooks/analytics/useProjectUsageStats.tsx`** — route the
already-branded `genChartQuery` output through `executeAnalyticsSql`
(parallels `useLogsPreview`).
- **`data/reports/report.utils.ts`** — tighten `fetchLogs(sql)` from
`string` to `SafeLogSqlFragment`; the wire boundary is now the same
single `executeAnalyticsSql` wrapper used by the rest of the analytics
path. Adds two pre-branded fragment maps reused by the report configs:
- `SAFE_GRANULARITY_SQL` — closed set returned by
`analyticsIntervalToGranularity`.
- `SAFE_COMPARISON_OPERATOR_SQL` — closed set on
`NumericFilter.operator`.
- **`components/interfaces/Auth/Overview/OverviewErrors.constants.ts`**
— wrap the two static `AUTH_TOP_*_SQL` fragments in `safeSql` (no
interpolation, but the type now flows).
- **`data/reports/v2/edge-functions.config.ts`** — `filterToWhereClause`
and every entry in `METRIC_SQL` now return `SafeLogSqlFragment`.
User-controlled values (`status_code.value`, `execution_time.value`,
function IDs, regions) pass through `analyticsLiteral`; operators look
up the branded map; the granularity uses the branded map. The
wire-format strings are unchanged, so the existing
`edge-functions.test.tsx` exact-string expectations still hold.
- **`data/reports/v2/auth.config.ts`** — same shape applied to all ten
`AUTH_REPORT_SQL` entries. The legacy `whereClause.replace(/^WHERE\s+/,
'')` pattern is replaced by two helpers that emit `AND`-prefixed
predicate fragments directly (`authFiltersToAndPredicates`,
`edgeLogsFiltersToAndPredicates`). Static provider SELECT / GROUP BY
fragments are pre-branded.

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

## Summary by CodeRabbit

* **Refactor**
* Enhanced security for analytics and reporting queries by updating
query construction methods across auth, edge functions, and project
usage reports.

<!-- 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/46476?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-29 09:26:06 -04:00

70 lines
1.9 KiB
TypeScript

import dayjs from 'dayjs'
import { safeSql } from '@/data/logs/safe-analytics-sql'
import { fetchLogs } from '@/data/reports/report.utils'
export type ResponseErrorRow = {
method: string
path: string
status_code: number
count: number
}
export type AuthErrorCodeRow = {
error_code: string
count: number
}
export const getDateRange = () => {
return {
start: dayjs().subtract(24, 'hour').toISOString(),
end: dayjs().toISOString(),
}
}
// Top API response errors for /auth/v1 endpoints (path/method/status)
export const AUTH_TOP_RESPONSE_ERRORS_SQL = safeSql`
select
request.method as method,
request.path as path,
response.status_code as status_code,
count(*) as count
from edge_logs
cross join unnest(metadata) as m
cross join unnest(m.request) as request
cross join unnest(m.response) as response
where path like '%auth/v1%'
and response.status_code between 400 and 599
group by method, path, status_code
order by count desc
limit 10
`
// Top Auth service error codes from x_sb_error_code header for /auth/v1 endpoints
export const AUTH_TOP_ERROR_CODES_SQL = safeSql`
select
h.x_sb_error_code as error_code,
count(*) as count
from edge_logs
cross join unnest(metadata) as m
cross join unnest(m.request) as request
cross join unnest(m.response) as response
cross join unnest(response.headers) as h
where path like '%auth/v1%'
and response.status_code between 400 and 599
and h.x_sb_error_code is not null
group by error_code
order by count desc
limit 10
`
export const fetchTopResponseErrors = async (projectRef: string) => {
const { start, end } = getDateRange()
return await fetchLogs(projectRef, AUTH_TOP_RESPONSE_ERRORS_SQL, start, end)
}
export const fetchTopAuthErrorCodes = async (projectRef: string) => {
const { start, end } = getDateRange()
return await fetchLogs(projectRef, AUTH_TOP_ERROR_CODES_SQL, start, end)
}