mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
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 -->
[](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:
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
|
||||
|
||||
@@ -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 } } = {
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user