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 -->
This commit is contained in:
Charis authored and GitHub committed 2026-05-29 09:26:06 -04:00
1 parent 19944c1e4e
commit fd1f437eca
5 files changed
+237 -160

No files matched your search

@@ -1,5 +1,6 @@
import dayjs from 'dayjs'
import { safeSql } from '@/data/logs/safe-analytics-sql'
import { fetchLogs } from '@/data/reports/report.utils'
export type ResponseErrorRow = {
@@ -22,7 +23,7 @@ export const getDateRange = () => {
}
// Top API response errors for /auth/v1 endpoints (path/method/status)
export const AUTH_TOP_RESPONSE_ERRORS_SQL = `
export const AUTH_TOP_RESPONSE_ERRORS_SQL = safeSql`
select
request.method as method,
request.path as path,
@@ -40,7 +41,7 @@ export const AUTH_TOP_RESPONSE_ERRORS_SQL = `
`
// Top Auth service error codes from x_sb_error_code header for /auth/v1 endpoints
export const AUTH_TOP_ERROR_CODES_SQL = `
export const AUTH_TOP_ERROR_CODES_SQL = safeSql`
select
h.x_sb_error_code as error_code,
count(*) as count
+37 -13
View File
@@ -1,8 +1,36 @@
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 { get } from '@/data/fetchers'
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':
@@ -55,22 +83,18 @@ export const useEdgeFnIdToName = ({ projectRef }: { projectRef: string }) => {
export async function fetchLogs(
projectRef: string,
sql: string,
sql: SafeLogSqlFragment,
startDate: string,
endDate: string
) {
const { data, error } = await get(`/platform/projects/{ref}/analytics/endpoints/logs.all`, {
params: {
path: { ref: projectRef },
query: {
sql,
iso_timestamp_start: startDate,
iso_timestamp_end: endDate,
},
},
return await executeAnalyticsSql({
projectRef,
endpoint: '/platform/projects/{ref}/analytics/endpoints/logs.all',
sql,
iso_timestamp_start: startDate,
iso_timestamp_end: endDate,
method: 'get',
})
if (error) throw error
return data
}
export const STATUS_CODE_COLORS: { [key: string]: { light: string; dark: string } } = {
+150 -109
View File
@@ -10,7 +10,18 @@ import {
} from '@/components/interfaces/Reports/Reports.utils'
import { NumericFilter } from '@/components/interfaces/Reports/v2/ReportsNumericFilter'
import type { AnalyticsInterval } from '@/data/analytics/constants'
import { analyticsIntervalToGranularity, fetchLogs } from '@/data/reports/report.utils'
import {
analyticsLiteral,
joinSqlFragments,
safeSql,
type SafeLogSqlFragment,
} from '@/data/logs/safe-analytics-sql'
import {
analyticsIntervalToGranularity,
fetchLogs,
SAFE_COMPARISON_OPERATOR_SQL,
SAFE_GRANULARITY_SQL,
} from '@/data/reports/report.utils'
const AUTH_ERROR_CODE_LIST = Object.entries(AUTH_ERROR_CODES).map(([key, value]) => ({
key,
@@ -32,39 +43,103 @@ const METRIC_KEYS = [
type MetricKey = (typeof METRIC_KEYS)[number]
type AuthReportFilters = {
status_code?: NumericFilter | null
provider?: string[] | null
}
// Static SELECT-clause fragment for the `auth_logs` table.
const PROVIDER_SELECT_FRAGMENT = safeSql`COALESCE(JSON_VALUE(event_message, "$.provider"), 'unknown') as provider,`
// Static SELECT-clause fragment for the aliased `auth_logs f` form.
const PROVIDER_SELECT_FRAGMENT_F_ALIAS = safeSql`COALESCE(JSON_VALUE(f.event_message, "$.provider"), 'unknown') as provider,`
const PROVIDER_GROUP_BY_FRAGMENT = safeSql`, provider`
const EMPTY = safeSql``
function providerSelectFragment(groupByProvider: boolean, aliased: boolean): SafeLogSqlFragment {
if (!groupByProvider) return EMPTY
return aliased ? PROVIDER_SELECT_FRAGMENT_F_ALIAS : PROVIDER_SELECT_FRAGMENT
}
function providerGroupBy(groupByProvider: boolean): SafeLogSqlFragment {
return groupByProvider ? PROVIDER_GROUP_BY_FRAGMENT : EMPTY
}
/**
* Builds an `AND`-prefixed predicate fragment for `auth_logs`-shaped queries.
* Returns the empty fragment when no filters apply. The returned value can be
* spliced directly after a query's existing `WHERE` clause.
*/
function authFiltersToAndPredicates(filters?: AuthReportFilters): SafeLogSqlFragment {
const predicates: SafeLogSqlFragment[] = []
if (filters?.status_code) {
const op = SAFE_COMPARISON_OPERATOR_SQL[filters.status_code.operator]
predicates.push(
safeSql`response.status_code ${op} ${analyticsLiteral(filters.status_code.value)}`
)
}
if (filters?.provider && filters.provider.length > 0) {
const list = joinSqlFragments(filters.provider.map(analyticsLiteral), ', ')
predicates.push(safeSql`JSON_VALUE(event_message, "$.provider") IN (${list})`)
}
if (predicates.length === 0) return EMPTY
return safeSql`AND ${joinSqlFragments(predicates, ' AND ')}`
}
/**
* Builds an `AND`-prefixed predicate fragment for `edge_logs`-shaped queries.
*/
function edgeLogsFiltersToAndPredicates(filters?: AuthReportFilters): SafeLogSqlFragment {
const predicates: SafeLogSqlFragment[] = []
if (filters?.status_code) {
const op = SAFE_COMPARISON_OPERATOR_SQL[filters.status_code.operator]
predicates.push(
safeSql`response.status_code ${op} ${analyticsLiteral(filters.status_code.value)}`
)
}
if (predicates.length === 0) return EMPTY
return safeSql`AND ${joinSqlFragments(predicates, ' AND ')}`
}
const AUTH_REPORT_SQL: Record<
MetricKey,
(interval: AnalyticsInterval, filters?: AuthReportFilters) => string
(interval: AnalyticsInterval, filters?: AuthReportFilters) => SafeLogSqlFragment
> = {
ActiveUsers: (interval, filters) => {
const granularity = analyticsIntervalToGranularity(interval)
const whereClause = filterToWhereClause(filters)
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = authFiltersToAndPredicates(filters)
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
return `
return safeSql`
--active-users
select
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
${groupByProvider ? 'COALESCE(JSON_VALUE(f.event_message, "$.provider"), \'unknown\') as provider,' : ''}
${providerSelectFragment(groupByProvider, true)}
count(distinct json_value(f.event_message, "$.auth_event.actor_id")) as count
from auth_logs f
where json_value(f.event_message, "$.auth_event.action") in (
'login', 'user_signedup', 'token_refreshed', 'user_modified',
'user_recovery_requested', 'user_reauthenticate_requested'
)
${whereClause ? `AND ${whereClause.replace(/^WHERE\s+/, '')}` : ''}
group by timestamp${groupByProvider ? ', provider' : ''}
order by timestamp desc${groupByProvider ? ', provider' : ''}
${andPredicates}
group by timestamp${providerGroupBy(groupByProvider)}
order by timestamp desc${providerGroupBy(groupByProvider)}
`
},
SignInAttempts: (interval, filters) => {
const granularity = analyticsIntervalToGranularity(interval)
const whereClause = filterToWhereClause(filters)
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = authFiltersToAndPredicates(filters)
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
return `
return safeSql`
--sign-in-attempts
SELECT
timestamp_trunc(timestamp, ${granularity}) as timestamp,
${groupByProvider ? 'COALESCE(JSON_VALUE(event_message, "$.provider"), \'unknown\') as provider,' : ''}
${providerSelectFragment(groupByProvider, false)}
CASE
WHEN JSON_VALUE(event_message, "$.provider") IS NOT NULL
AND JSON_VALUE(event_message, "$.provider") != ''
@@ -82,133 +157,133 @@ const AUTH_REPORT_SQL: Record<
WHERE
JSON_VALUE(event_message, "$.action") = 'login'
AND JSON_VALUE(event_message, "$.metering") = "true"
${whereClause ? `AND ${whereClause.replace(/^WHERE\s+/, '')}` : ''}
${andPredicates}
GROUP BY
timestamp, login_type_provider${groupByProvider ? ', provider' : ''}
timestamp, login_type_provider${providerGroupBy(groupByProvider)}
ORDER BY
timestamp desc, login_type_provider${groupByProvider ? ', provider' : ''}
timestamp desc, login_type_provider${providerGroupBy(groupByProvider)}
`
},
PasswordResetRequests: (interval, filters) => {
const granularity = analyticsIntervalToGranularity(interval)
const whereClause = filterToWhereClause(filters)
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = authFiltersToAndPredicates(filters)
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
return `
return safeSql`
--password-reset-requests
select
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
${groupByProvider ? 'COALESCE(JSON_VALUE(f.event_message, "$.provider"), \'unknown\') as provider,' : ''}
${providerSelectFragment(groupByProvider, true)}
count(*) as count
from auth_logs f
where json_value(f.event_message, "$.auth_event.action") = 'user_recovery_requested'
${whereClause ? `AND ${whereClause.replace(/^WHERE\s+/, '')}` : ''}
group by timestamp${groupByProvider ? ', provider' : ''}
order by timestamp desc${groupByProvider ? ', provider' : ''}
${andPredicates}
group by timestamp${providerGroupBy(groupByProvider)}
order by timestamp desc${providerGroupBy(groupByProvider)}
`
},
TotalSignUps: (interval, filters) => {
const granularity = analyticsIntervalToGranularity(interval)
const whereClause = filterToWhereClause(filters)
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = authFiltersToAndPredicates(filters)
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
return `
return safeSql`
--total-signups
select
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
${groupByProvider ? 'COALESCE(JSON_VALUE(event_message, "$.provider"), \'unknown\') as provider,' : ''}
${providerSelectFragment(groupByProvider, false)}
count(*) as count
from auth_logs
where json_value(event_message, "$.auth_event.action") = 'user_signedup'
${whereClause ? `AND ${whereClause.replace(/^WHERE\s+/, '')}` : ''}
group by timestamp${groupByProvider ? ', provider' : ''}
order by timestamp desc${groupByProvider ? ', provider' : ''}
${andPredicates}
group by timestamp${providerGroupBy(groupByProvider)}
order by timestamp desc${providerGroupBy(groupByProvider)}
`
},
SignInProcessingTimeBasic: (interval, filters) => {
const granularity = analyticsIntervalToGranularity(interval)
const whereClause = filterToWhereClause(filters)
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = authFiltersToAndPredicates(filters)
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
return `
return safeSql`
--signin-processing-time-basic
select
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
${groupByProvider ? 'COALESCE(JSON_VALUE(event_message, "$.provider"), \'unknown\') as provider,' : ''}
${providerSelectFragment(groupByProvider, false)}
count(*) as count,
round(avg(cast(json_value(event_message, "$.duration") as int64)) / 1000000, 2) as avg_processing_time_ms,
round(min(cast(json_value(event_message, "$.duration") as int64)) / 1000000, 2) as min_processing_time_ms,
round(max(cast(json_value(event_message, "$.duration") as int64)) / 1000000, 2) as max_processing_time_ms
from auth_logs
where json_value(event_message, "$.auth_event.action") = 'login'
${whereClause ? `AND ${whereClause.replace(/^WHERE\s+/, '')}` : ''}
group by timestamp${groupByProvider ? ', provider' : ''}
order by timestamp desc${groupByProvider ? ', provider' : ''}
${andPredicates}
group by timestamp${providerGroupBy(groupByProvider)}
order by timestamp desc${providerGroupBy(groupByProvider)}
`
},
SignInProcessingTimePercentiles: (interval, filters) => {
const granularity = analyticsIntervalToGranularity(interval)
const whereClause = filterToWhereClause(filters)
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = authFiltersToAndPredicates(filters)
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
return `
return safeSql`
--signin-processing-time-percentiles
select
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
${groupByProvider ? 'COALESCE(JSON_VALUE(event_message, "$.provider"), \'unknown\') as provider,' : ''}
${providerSelectFragment(groupByProvider, false)}
count(*) as count,
round(approx_quantiles(cast(json_value(event_message, "$.duration") as int64), 100)[offset(50)] / 1000000, 2) as p50_processing_time_ms,
round(approx_quantiles(cast(json_value(event_message, "$.duration") as int64), 100)[offset(95)] / 1000000, 2) as p95_processing_time_ms,
round(approx_quantiles(cast(json_value(event_message, "$.duration") as int64), 100)[offset(99)] / 1000000, 2) as p99_processing_time_ms
from auth_logs
where json_value(event_message, "$.auth_event.action") = 'login'
${whereClause ? `AND ${whereClause.replace(/^WHERE\s+/, '')}` : ''}
group by timestamp${groupByProvider ? ', provider' : ''}
order by timestamp desc${groupByProvider ? ', provider' : ''}
${andPredicates}
group by timestamp${providerGroupBy(groupByProvider)}
order by timestamp desc${providerGroupBy(groupByProvider)}
`
},
SignUpProcessingTimeBasic: (interval, filters) => {
const granularity = analyticsIntervalToGranularity(interval)
const whereClause = filterToWhereClause(filters)
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = authFiltersToAndPredicates(filters)
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
return `
return safeSql`
--signup-processing-time-basic
select
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
${groupByProvider ? 'COALESCE(JSON_VALUE(event_message, "$.provider"), \'unknown\') as provider,' : ''}
${providerSelectFragment(groupByProvider, false)}
count(*) as count,
round(avg(cast(json_value(event_message, "$.duration") as int64)) / 1000000, 2) as avg_processing_time_ms,
round(min(cast(json_value(event_message, "$.duration") as int64)) / 1000000, 2) as min_processing_time_ms,
round(max(cast(json_value(event_message, "$.duration") as int64)) / 1000000, 2) as max_processing_time_ms
from auth_logs
where json_value(event_message, "$.auth_event.action") = 'user_signedup'
${whereClause ? `AND ${whereClause.replace(/^WHERE\s+/, '')}` : ''}
group by timestamp${groupByProvider ? ', provider' : ''}
order by timestamp desc${groupByProvider ? ', provider' : ''}
${andPredicates}
group by timestamp${providerGroupBy(groupByProvider)}
order by timestamp desc${providerGroupBy(groupByProvider)}
`
},
SignUpProcessingTimePercentiles: (interval, filters) => {
const granularity = analyticsIntervalToGranularity(interval)
const whereClause = filterToWhereClause(filters)
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = authFiltersToAndPredicates(filters)
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
return `
return safeSql`
--signup-processing-time-percentiles
select
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
${groupByProvider ? 'COALESCE(JSON_VALUE(event_message, "$.provider"), \'unknown\') as provider,' : ''}
${providerSelectFragment(groupByProvider, false)}
count(*) as count,
round(approx_quantiles(cast(json_value(event_message, "$.duration") as int64), 100)[offset(50)] / 1000000, 2) as p50_processing_time_ms,
round(approx_quantiles(cast(json_value(event_message, "$.duration") as int64), 100)[offset(95)] / 1000000, 2) as p95_processing_time_ms,
round(approx_quantiles(cast(json_value(event_message, "$.duration") as int64), 100)[offset(99)] / 1000000, 2) as p99_processing_time_ms
from auth_logs
where json_value(event_message, "$.auth_event.action") = 'user_signedup'
${whereClause ? `AND ${whereClause.replace(/^WHERE\s+/, '')}` : ''}
group by timestamp${groupByProvider ? ', provider' : ''}
order by timestamp desc${groupByProvider ? ', provider' : ''}
${andPredicates}
group by timestamp${providerGroupBy(groupByProvider)}
order by timestamp desc${providerGroupBy(groupByProvider)}
`
},
ErrorsByStatus: (interval, filters) => {
const granularity = analyticsIntervalToGranularity(interval)
const whereClause = edgeLogsFilterToWhereClause(filters)
return `
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = edgeLogsFiltersToAndPredicates(filters)
return safeSql`
--auth-errors-by-status
select
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
count(*) as count,
response.status_code
@@ -219,17 +294,17 @@ const AUTH_REPORT_SQL: Record<
cross join unnest(response.headers) as h
where path like '%auth/v1%'
and response.status_code >= 400 and response.status_code <= 599
${whereClause ? `AND ${whereClause.replace(/^WHERE\s+/, '')}` : ''}
${andPredicates}
group by timestamp, status_code
order by timestamp desc
`
},
ErrorsByAuthCode: (interval, filters) => {
const granularity = analyticsIntervalToGranularity(interval)
const whereClause = edgeLogsFilterToWhereClause(filters)
return `
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = edgeLogsFiltersToAndPredicates(filters)
return safeSql`
--auth-errors-by-code
select
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
count(*) as count,
h.x_sb_error_code as error_code
@@ -240,47 +315,13 @@ const AUTH_REPORT_SQL: Record<
cross join unnest(response.headers) as h
where path like '%auth/v1%'
and response.status_code >= 400 and response.status_code <= 599
${whereClause ? `AND ${whereClause.replace(/^WHERE\s+/, '')}` : ''}
${andPredicates}
group by timestamp, error_code
order by timestamp desc
`
},
}
type AuthReportFilters = {
status_code?: NumericFilter | null
provider?: string[] | null
}
function filterToWhereClause(filters?: AuthReportFilters): string {
const whereClauses: string[] = []
if (filters?.status_code) {
whereClauses.push(
`response.status_code ${filters.status_code.operator} ${filters.status_code.value}`
)
}
if (filters?.provider && filters.provider.length > 0) {
const providerList = filters.provider.map((p) => `'${p}'`).join(', ')
whereClauses.push(`JSON_VALUE(event_message, "$.provider") IN (${providerList})`)
}
return whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : ''
}
function edgeLogsFilterToWhereClause(filters?: AuthReportFilters): string {
const whereClauses: string[] = []
if (filters?.status_code) {
whereClauses.push(
`response.status_code ${filters.status_code.operator} ${filters.status_code.value}`
)
}
return whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : ''
}
export const AUTH_ERROR_CODE_VALUES: string[] = [
'anonymous_provider_disabled',
'bad_code_verifier',
@@ -13,7 +13,18 @@ import {
unixMicroToIsoTimestamp,
} from '@/components/interfaces/Settings/Logs/Logs.utils'
import type { AnalyticsInterval } from '@/data/analytics/constants'
import { analyticsIntervalToGranularity, fetchLogs } from '@/data/reports/report.utils'
import {
analyticsLiteral,
joinSqlFragments,
safeSql,
type SafeLogSqlFragment,
} from '@/data/logs/safe-analytics-sql'
import {
analyticsIntervalToGranularity,
fetchLogs,
SAFE_COMPARISON_OPERATOR_SQL,
SAFE_GRANULARITY_SQL,
} from '@/data/reports/report.utils'
type EdgeFunctionReportFilters = {
status_code: NumericFilter | null
@@ -22,44 +33,48 @@ type EdgeFunctionReportFilters = {
functions: SelectFilters
}
export function filterToWhereClause(filters?: EdgeFunctionReportFilters): string {
const whereClauses: string[] = []
export function filterToWhereClause(filters?: EdgeFunctionReportFilters): SafeLogSqlFragment {
const whereClauses: SafeLogSqlFragment[] = []
if (filters?.functions && filters.functions.length > 0) {
whereClauses.push(`function_id IN (${filters.functions.map((id) => `'${id}'`).join(',')})`)
const ids = joinSqlFragments(filters.functions.map(analyticsLiteral), ',')
whereClauses.push(safeSql`function_id IN (${ids})`)
}
if (filters?.status_code) {
const op = SAFE_COMPARISON_OPERATOR_SQL[filters.status_code.operator]
whereClauses.push(
`response.status_code ${filters.status_code.operator} ${filters.status_code.value}`
safeSql`response.status_code ${op} ${analyticsLiteral(filters.status_code.value)}`
)
}
if (filters?.region && filters.region.length > 0) {
whereClauses.push(
`h.x_sb_edge_region IN (${filters.region.map((region) => `'${region}'`).join(',')})`
)
const regions = joinSqlFragments(filters.region.map(analyticsLiteral), ',')
whereClauses.push(safeSql`h.x_sb_edge_region IN (${regions})`)
}
if (filters?.execution_time) {
const op = SAFE_COMPARISON_OPERATOR_SQL[filters.execution_time.operator]
whereClauses.push(
`m.execution_time_ms ${filters.execution_time.operator} ${filters.execution_time.value}`
safeSql`m.execution_time_ms ${op} ${analyticsLiteral(filters.execution_time.value)}`
)
}
return whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : ''
if (whereClauses.length === 0) return safeSql``
return safeSql`WHERE ${joinSqlFragments(whereClauses, ' AND ')}`
}
const METRIC_SQL: Record<
string,
(interval: AnalyticsInterval, filters?: EdgeFunctionReportFilters) => string
(interval: AnalyticsInterval, filters?: EdgeFunctionReportFilters) => SafeLogSqlFragment
> = {
TotalInvocations: (interval, filters) => {
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const whereClause = filterToWhereClause(filters)
return `
return safeSql`
--edgefn-report-invocations
select
timestamp_trunc(timestamp, ${analyticsIntervalToGranularity(interval)}) as timestamp,
timestamp_trunc(timestamp, ${granularity}) as timestamp,
function_id,
count(*) as count
from
@@ -77,11 +92,12 @@ order by
`
},
ExecutionStatusCodes: (interval, filters) => {
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const whereClause = filterToWhereClause(filters)
return `
return safeSql`
--edgefn-report-execution-status-codes
select
timestamp_trunc(timestamp, ${analyticsIntervalToGranularity(interval)}) as timestamp,
timestamp_trunc(timestamp, ${granularity}) as timestamp,
response.status_code as status_code,
count(response.status_code) as count
from
@@ -98,14 +114,14 @@ order by
`
},
InvocationsByRegion: (interval, filters) => {
const granularity = analyticsIntervalToGranularity(interval)
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const whereClause = filterToWhereClause(filters)
const hasWhere = whereClause.includes('WHERE')
const regionCondition = hasWhere
? 'AND h.x_sb_edge_region is not null'
: 'WHERE h.x_sb_edge_region is not null'
const regionCondition =
whereClause.length > 0
? safeSql`AND h.x_sb_edge_region is not null`
: safeSql`WHERE h.x_sb_edge_region is not null`
return `
return safeSql`
--edgefn-report-invocations-by-region
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
@@ -126,10 +142,10 @@ order by
`
},
ExecutionTime: (interval, filters) => {
const granularity = analyticsIntervalToGranularity(interval)
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const whereClause = filterToWhereClause(filters)
return `
return safeSql`
--edgefn-report-execution-time
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
@@ -11,7 +11,7 @@ import type {
LogsEndpointParams,
} from '@/components/interfaces/Settings/Logs/Logs.types'
import { genChartQuery } from '@/components/interfaces/Settings/Logs/Logs.utils'
import { get } from '@/data/fetchers'
import { executeAnalyticsSql } from '@/data/logs/execute-analytics-sql'
interface ProjectUsageStatsHookResult {
error: string | Object | null
@@ -71,20 +71,15 @@ function useProjectUsageStats({
const { data: eventChartResponse, refetch: refreshEventChart } = useQuery({
queryKey: chartQueryKey,
queryFn: async ({ signal }) => {
const { data, error } = await get(`/platform/projects/{ref}/analytics/endpoints/logs.all`, {
params: {
path: { ref: projectRef },
query: {
iso_timestamp_start: timestampStart,
iso_timestamp_end: timestampEnd,
sql: chartQuery,
},
},
const data = await executeAnalyticsSql({
projectRef,
endpoint: '/platform/projects/{ref}/analytics/endpoints/logs.all',
sql: chartQuery,
iso_timestamp_start: timestampStart,
iso_timestamp_end: timestampEnd,
method: 'get',
signal,
})
if (error) {
throw error
}
return data as unknown as EventChart
},