mirror of
https://github.com/supabase/supabase.git
synced 2026-09-28 13:21:25 +08:00
## 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 -->
[](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 -->
115 lines
3.7 KiB
TypeScript
115 lines
3.7 KiB
TypeScript
import { type ComparisonOperator } from '@/components/interfaces/Reports/v2/ReportsNumericFilter'
|
|
import { AnalyticsInterval } from '@/data/analytics/constants'
|
|
import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-query'
|
|
import { executeAnalyticsSql } from '@/data/logs/execute-analytics-sql'
|
|
import { safeSql, type SafeLogSqlFragment } from '@/data/logs/safe-analytics-sql'
|
|
|
|
export type Granularity = 'minute' | 'hour' | 'day'
|
|
|
|
/**
|
|
* Pre-branded SQL fragments for the closed set of granularity tokens that
|
|
* `analyticsIntervalToGranularity` may return. Use to splice a granularity into
|
|
* a `safeSql` template without re-validating at the call site.
|
|
*/
|
|
export const SAFE_GRANULARITY_SQL: Record<Granularity, SafeLogSqlFragment> = {
|
|
minute: safeSql`minute`,
|
|
hour: safeSql`hour`,
|
|
day: safeSql`day`,
|
|
}
|
|
|
|
/**
|
|
* Pre-branded SQL fragments for the closed set of numeric comparison operators
|
|
* accepted by `ReportsNumericFilter`. Use to splice an operator into a
|
|
* `safeSql` template without re-validating at the call site.
|
|
*/
|
|
export const SAFE_COMPARISON_OPERATOR_SQL: Record<ComparisonOperator, SafeLogSqlFragment> = {
|
|
'=': safeSql`=`,
|
|
'>=': safeSql`>=`,
|
|
'<=': safeSql`<=`,
|
|
'>': safeSql`>`,
|
|
'<': safeSql`<`,
|
|
'!=': safeSql`!=`,
|
|
}
|
|
|
|
export function analyticsIntervalToGranularity(interval: AnalyticsInterval): Granularity {
|
|
switch (interval) {
|
|
case '1m':
|
|
return 'minute'
|
|
case '5m':
|
|
return 'minute'
|
|
case '10m':
|
|
return 'minute'
|
|
case '30m':
|
|
return 'minute'
|
|
case '1h':
|
|
return 'hour'
|
|
case '1d':
|
|
return 'day'
|
|
default:
|
|
return 'hour'
|
|
}
|
|
}
|
|
|
|
export const REPORT_STATUS_CODE_COLORS: { [key: string]: { light: string; dark: string } } = {
|
|
'400': { light: '#FFD54F', dark: '#FFF176' },
|
|
'401': { light: '#FF8A65', dark: '#FFAB91' },
|
|
'403': { light: '#FFB74D', dark: '#FFCC80' },
|
|
'404': { light: '#90A4AE', dark: '#B0BEC5' },
|
|
'409': { light: '#BA68C8', dark: '#CE93D8' },
|
|
'410': { light: '#A1887F', dark: '#BCAAA4' },
|
|
'422': { light: '#FF9800', dark: '#FFB74D' },
|
|
'429': { light: '#E65100', dark: '#F57C00' },
|
|
'500': { light: '#B71C1C', dark: '#D32F2F' },
|
|
'502': { light: '#9575CD', dark: '#B39DDB' },
|
|
'503': { light: '#0097A7', dark: '#4DD0E1' },
|
|
'504': { light: '#C0CA33', dark: '#D4E157' },
|
|
default: { light: '#757575', dark: '#9E9E9E' },
|
|
}
|
|
|
|
export const useEdgeFnIdToName = ({ projectRef }: { projectRef: string }) => {
|
|
const { data: edgeFunctions, isPending: isLoading } = useEdgeFunctionsQuery({
|
|
projectRef,
|
|
})
|
|
|
|
function edgeFnIdToName(id: string) {
|
|
return edgeFunctions?.find((fn) => fn.id === id)?.name
|
|
}
|
|
|
|
return {
|
|
edgeFnIdToName,
|
|
isLoading,
|
|
}
|
|
}
|
|
|
|
export async function fetchLogs(
|
|
projectRef: string,
|
|
sql: SafeLogSqlFragment,
|
|
startDate: string,
|
|
endDate: string
|
|
) {
|
|
return await executeAnalyticsSql({
|
|
projectRef,
|
|
endpoint: '/platform/projects/{ref}/analytics/endpoints/logs.all',
|
|
sql,
|
|
iso_timestamp_start: startDate,
|
|
iso_timestamp_end: endDate,
|
|
method: 'get',
|
|
})
|
|
}
|
|
|
|
export const STATUS_CODE_COLORS: { [key: string]: { light: string; dark: string } } = {
|
|
'400': { light: '#FFD54F', dark: '#FFF176' },
|
|
'401': { light: '#FF8A65', dark: '#FFAB91' },
|
|
'403': { light: '#FFB74D', dark: '#FFCC80' },
|
|
'404': { light: '#90A4AE', dark: '#B0BEC5' },
|
|
'409': { light: '#BA68C8', dark: '#CE93D8' },
|
|
'410': { light: '#A1887F', dark: '#BCAAA4' },
|
|
'422': { light: '#FF9800', dark: '#FFB74D' },
|
|
'429': { light: '#E65100', dark: '#F57C00' },
|
|
'500': { light: '#B71C1C', dark: '#D32F2F' },
|
|
'502': { light: '#9575CD', dark: '#B39DDB' },
|
|
'503': { light: '#0097A7', dark: '#4DD0E1' },
|
|
'504': { light: '#C0CA33', dark: '#D4E157' },
|
|
default: { light: '#757575', dark: '#9E9E9E' },
|
|
}
|