Files
supabase/apps/studio/data/reports/v2/auth.config.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

978 lines
33 KiB
TypeScript

import { AUTH_ERROR_CODES } from 'common/constants/auth-error-codes'
import z from 'zod'
import { ReportConfig, ReportDataProviderAttribute } from './reports.types'
import {
extractStatusCodesFromData,
generateStatusCodeAttributes,
transformCategoricalCountData,
transformStatusCodeData,
} from '@/components/interfaces/Reports/Reports.utils'
import { NumericFilter } from '@/components/interfaces/Reports/v2/ReportsNumericFilter'
import type { AnalyticsInterval } from '@/data/analytics/constants'
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,
description: value.description,
}))
const METRIC_KEYS = [
'ActiveUsers',
'SignInAttempts',
'PasswordResetRequests',
'TotalSignUps',
'SignInProcessingTimeBasic',
'SignInProcessingTimePercentiles',
'SignUpProcessingTimeBasic',
'SignUpProcessingTimePercentiles',
'ErrorsByStatus',
'ErrorsByAuthCode',
]
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) => SafeLogSqlFragment
> = {
ActiveUsers: (interval, filters) => {
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = authFiltersToAndPredicates(filters)
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
return safeSql`
--active-users
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
${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'
)
${andPredicates}
group by timestamp${providerGroupBy(groupByProvider)}
order by timestamp desc${providerGroupBy(groupByProvider)}
`
},
SignInAttempts: (interval, filters) => {
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = authFiltersToAndPredicates(filters)
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
return safeSql`
--sign-in-attempts
SELECT
timestamp_trunc(timestamp, ${granularity}) as timestamp,
${providerSelectFragment(groupByProvider, false)}
CASE
WHEN JSON_VALUE(event_message, "$.provider") IS NOT NULL
AND JSON_VALUE(event_message, "$.provider") != ''
THEN CONCAT(
JSON_VALUE(event_message, "$.login_method"),
' (',
JSON_VALUE(event_message, "$.provider"),
')'
)
ELSE JSON_VALUE(event_message, "$.login_method")
END as login_type_provider,
COUNT(*) as count
FROM
auth_logs
WHERE
JSON_VALUE(event_message, "$.action") = 'login'
AND JSON_VALUE(event_message, "$.metering") = "true"
${andPredicates}
GROUP BY
timestamp, login_type_provider${providerGroupBy(groupByProvider)}
ORDER BY
timestamp desc, login_type_provider${providerGroupBy(groupByProvider)}
`
},
PasswordResetRequests: (interval, filters) => {
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = authFiltersToAndPredicates(filters)
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
return safeSql`
--password-reset-requests
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
${providerSelectFragment(groupByProvider, true)}
count(*) as count
from auth_logs f
where json_value(f.event_message, "$.auth_event.action") = 'user_recovery_requested'
${andPredicates}
group by timestamp${providerGroupBy(groupByProvider)}
order by timestamp desc${providerGroupBy(groupByProvider)}
`
},
TotalSignUps: (interval, filters) => {
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = authFiltersToAndPredicates(filters)
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
return safeSql`
--total-signups
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
${providerSelectFragment(groupByProvider, false)}
count(*) as count
from auth_logs
where json_value(event_message, "$.auth_event.action") = 'user_signedup'
${andPredicates}
group by timestamp${providerGroupBy(groupByProvider)}
order by timestamp desc${providerGroupBy(groupByProvider)}
`
},
SignInProcessingTimeBasic: (interval, filters) => {
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = authFiltersToAndPredicates(filters)
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
return safeSql`
--signin-processing-time-basic
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
${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'
${andPredicates}
group by timestamp${providerGroupBy(groupByProvider)}
order by timestamp desc${providerGroupBy(groupByProvider)}
`
},
SignInProcessingTimePercentiles: (interval, filters) => {
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = authFiltersToAndPredicates(filters)
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
return safeSql`
--signin-processing-time-percentiles
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
${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'
${andPredicates}
group by timestamp${providerGroupBy(groupByProvider)}
order by timestamp desc${providerGroupBy(groupByProvider)}
`
},
SignUpProcessingTimeBasic: (interval, filters) => {
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = authFiltersToAndPredicates(filters)
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
return safeSql`
--signup-processing-time-basic
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
${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'
${andPredicates}
group by timestamp${providerGroupBy(groupByProvider)}
order by timestamp desc${providerGroupBy(groupByProvider)}
`
},
SignUpProcessingTimePercentiles: (interval, filters) => {
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = authFiltersToAndPredicates(filters)
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
return safeSql`
--signup-processing-time-percentiles
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
${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'
${andPredicates}
group by timestamp${providerGroupBy(groupByProvider)}
order by timestamp desc${providerGroupBy(groupByProvider)}
`
},
ErrorsByStatus: (interval, filters) => {
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = edgeLogsFiltersToAndPredicates(filters)
return safeSql`
--auth-errors-by-status
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
count(*) as count,
response.status_code
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 >= 400 and response.status_code <= 599
${andPredicates}
group by timestamp, status_code
order by timestamp desc
`
},
ErrorsByAuthCode: (interval, filters) => {
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
const andPredicates = edgeLogsFiltersToAndPredicates(filters)
return safeSql`
--auth-errors-by-code
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
count(*) as count,
h.x_sb_error_code as error_code
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 >= 400 and response.status_code <= 599
${andPredicates}
group by timestamp, error_code
order by timestamp desc
`
},
}
export const AUTH_ERROR_CODE_VALUES: string[] = [
'anonymous_provider_disabled',
'bad_code_verifier',
'bad_json',
'bad_jwt',
'bad_oauth_callback',
'bad_oauth_state',
'captcha_failed',
'conflict',
'email_address_invalid',
'email_address_not_authorized',
'email_conflict_identity_not_deletable',
'email_exists',
'email_not_confirmed',
'email_provider_disabled',
'flow_state_expired',
'flow_state_not_found',
'hook_payload_invalid_content_type',
'hook_payload_over_size_limit',
'hook_timeout',
'hook_timeout_after_retry',
'identity_already_exists',
'identity_not_found',
'insufficient_aal',
'invalid_credentials',
'invite_not_found',
'manual_linking_disabled',
'mfa_challenge_expired',
'mfa_factor_name_conflict',
'mfa_factor_not_found',
'mfa_ip_address_mismatch',
'mfa_phone_enroll_not_enabled',
'mfa_phone_verify_not_enabled',
'mfa_totp_enroll_not_enabled',
'mfa_totp_verify_not_enabled',
'mfa_verification_failed',
'mfa_verification_rejected',
'mfa_verified_factor_exists',
'mfa_web_authn_enroll_not_enabled',
'mfa_web_authn_verify_not_enabled',
'no_authorization',
'not_admin',
'oauth_provider_not_supported',
'otp_disabled',
'otp_expired',
'over_email_send_rate_limit',
'over_request_rate_limit',
'over_sms_send_rate_limit',
'phone_exists',
'phone_not_confirmed',
'phone_provider_disabled',
'provider_disabled',
'provider_email_needs_verification',
'reauthentication_needed',
'reauthentication_not_valid',
'refresh_token_already_used',
'refresh_token_not_found',
'request_timeout',
'same_password',
'saml_assertion_no_email',
'saml_assertion_no_user_id',
'saml_entity_id_mismatch',
'saml_idp_already_exists',
'saml_idp_not_found',
'saml_metadata_fetch_failed',
'saml_provider_disabled',
'saml_relay_state_expired',
'saml_relay_state_not_found',
'session_expired',
'session_not_found',
'signup_disabled',
'single_identity_not_deletable',
'sms_send_failed',
'sso_domain_already_exists',
'sso_provider_not_found',
'too_many_enrolled_mfa_factors',
'unexpected_audience',
'unexpected_failure',
'user_already_exists',
'user_banned',
'user_not_found',
'user_sso_managed',
'validation_failed',
'weak_password',
]
/**
* Transforms raw analytics data into a chart-ready format by ensuring data consistency and completeness.
*
* This function addresses several key requirements for chart rendering:
* 1. Fills missing timestamps with zero values to prevent gaps in charts
* 2. Creates a consistent data structure with `period_start` as the time axis
* 3. Initializes all chart attributes to 0, then populates actual values
* 4. Sorts timestamps chronologically for proper chart ordering
*
* @param rawData - Raw analytics data from backend queries containing timestamp and count fields
* @param attributes - Chart attribute configuration defining what metrics to display
* @returns Formatted data object with consistent time series data and chart attributes
*/
export function defaultAuthReportFormatter(
rawData: unknown,
attributes: ReportDataProviderAttribute[],
groupByProvider = false
) {
const chartAttributes = attributes
const rawDataSchema = z.object({
result: z.array(
z
.object({
timestamp: z.coerce.number(),
})
.catchall(z.any())
),
})
const parsedRawData = rawDataSchema.parse(rawData)
const result = parsedRawData.result
if (!result) return { data: undefined, chartAttributes }
if (groupByProvider) {
// Group by provider - create separate attributes for each provider
const providers = new Set<string>()
result.forEach((p: any) => {
if (p.provider) {
providers.add(p.provider)
}
})
const providerAttributes: ReportDataProviderAttribute[] = []
providers.forEach((provider) => {
chartAttributes.forEach((attr) => {
providerAttributes.push({
...attr,
attribute: `${attr.attribute}_${provider}`,
label: `${attr.label} (${provider})`,
})
})
})
const timestamps = new Set<string>(result.map((p: any) => String(p.timestamp)))
const data = Array.from(timestamps)
.sort()
.map((timestamp) => {
const point: any = { timestamp }
providerAttributes.forEach((attr) => {
point[attr.attribute] = 0
})
const matchingPoints = result.filter((p: any) => String(p.timestamp) === timestamp)
matchingPoints.forEach((p: any) => {
providerAttributes.forEach((attr) => {
const baseAttribute = attr.attribute.split('_').slice(0, -1).join('_')
const provider = attr.attribute.split('_').slice(-1)[0]
if (p.provider !== provider) return
const valueFromField =
typeof p[baseAttribute] === 'number'
? p[baseAttribute]
: typeof p.count === 'number'
? p.count
: undefined
if (typeof valueFromField === 'number') {
point[attr.attribute] = (point[attr.attribute] ?? 0) + valueFromField
}
})
})
return point
})
return { data, chartAttributes: providerAttributes }
} else {
// Original logic for non-provider grouping
const timestamps = new Set<string>(result.map((p: any) => String(p.timestamp)))
const data = Array.from(timestamps)
.sort()
.map((timestamp) => {
const point: any = { timestamp }
chartAttributes.forEach((attr) => {
point[attr.attribute] = 0
})
const matchingPoints = result.filter((p: any) => String(p.timestamp) === timestamp)
matchingPoints.forEach((p: any) => {
chartAttributes.forEach((attr) => {
// Optional dimension filters used by some reports
if ('login_type_provider' in (attr as any)) {
if (p.login_type_provider !== (attr as any).login_type_provider) return
}
if ('providerType' in (attr as any)) {
if (p.provider !== (attr as any).providerType) return
}
const valueFromField =
typeof p[attr.attribute] === 'number'
? p[attr.attribute]
: typeof p.count === 'number'
? p.count
: undefined
if (typeof valueFromField === 'number') {
point[attr.attribute] = (point[attr.attribute] ?? 0) + valueFromField
}
})
})
return point
})
return { data, chartAttributes }
}
}
export const createUsageReportConfig = ({
projectRef,
startDate,
endDate,
interval,
filters,
}: {
projectRef: string
startDate: string
endDate: string
interval: AnalyticsInterval
filters: AuthReportFilters
}): ReportConfig<AuthReportFilters>[] => {
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
return [
{
id: 'active-user',
label: 'Auth Activity', // https://supabase.slack.com/archives/C08N7894QTG/p1761210058358439?thread_ts=1761147906.491599&cid=C08N7894QTG
valuePrecision: 0,
hide: false,
showTooltip: true,
showLegend: false,
showMaxValue: false,
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip:
"Users who generated any Auth event in this period. This metric tracks authentication activity, not total product usage. Some active users won't appear here if their session stayed valid.",
dataProvider: async () => {
const attributes = [
{ attribute: 'ActiveUsers', provider: 'logs', label: 'Auth Activity', enabled: true },
]
const sql = AUTH_REPORT_SQL.ActiveUsers(interval, filters)
const rawData = await fetchLogs(projectRef, sql, startDate, endDate)
const transformedData = defaultAuthReportFormatter(rawData, attributes, groupByProvider)
return {
data: transformedData.data,
attributes: transformedData.chartAttributes,
query: sql,
}
},
},
{
id: 'sign-in-attempts',
label: 'Sign In Attempts by Type',
valuePrecision: 0,
hide: false,
showTooltip: true,
showLegend: true,
showMaxValue: false,
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: 'The total number of sign in attempts by type.',
dataProvider: async () => {
const attributes = [
{
attribute: 'SignInAttempts',
provider: 'logs',
label: 'Password',
login_type_provider: 'password',
enabled: true,
},
{
attribute: 'SignInAttempts',
provider: 'logs',
label: 'PKCE',
login_type_provider: 'pkce',
enabled: true,
},
{
attribute: 'SignInAttempts',
provider: 'logs',
label: 'Refresh Token',
login_type_provider: 'token',
enabled: true,
},
{
attribute: 'SignInAttempts',
provider: 'logs',
label: 'ID Token',
login_type_provider: 'id_token',
enabled: true,
},
]
const sql = AUTH_REPORT_SQL.SignInAttempts(interval, filters)
const rawData = await fetchLogs(projectRef, sql, startDate, endDate)
const transformedData = defaultAuthReportFormatter(rawData, attributes, groupByProvider)
return {
data: transformedData.data,
attributes: transformedData.chartAttributes,
query: sql,
}
},
},
{
id: 'signups',
label: 'Sign Ups',
valuePrecision: 0,
hide: false,
showTooltip: true,
showLegend: true,
showMaxValue: false,
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: 'The total number of sign ups.',
dataProvider: async () => {
const attributes = [
{
attribute: 'TotalSignUps',
provider: 'logs',
label: 'Sign Ups',
enabled: true,
},
]
const sql = AUTH_REPORT_SQL.TotalSignUps(interval, filters)
const rawData = await fetchLogs(projectRef, sql, startDate, endDate)
const transformedData = defaultAuthReportFormatter(rawData, attributes, groupByProvider)
return {
data: transformedData.data,
attributes: transformedData.chartAttributes,
query: sql,
}
},
},
{
id: 'password-reset-requests',
label: 'Password Reset Requests',
valuePrecision: 0,
hide: false,
showTooltip: true,
showLegend: true,
showMaxValue: false,
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: 'The total number of password reset requests.',
dataProvider: async () => {
const attributes = [
{
attribute: 'PasswordResetRequests',
provider: 'logs',
label: 'Password Reset Requests',
enabled: true,
},
]
const sql = AUTH_REPORT_SQL.PasswordResetRequests(interval, filters)
const rawData = await fetchLogs(projectRef, sql, startDate, endDate)
const transformedData = defaultAuthReportFormatter(rawData, attributes, groupByProvider)
return {
data: transformedData.data,
attributes: transformedData.chartAttributes,
query: sql,
}
},
},
]
}
export const createErrorsReportConfig = ({
projectRef,
startDate,
endDate,
interval,
filters,
}: {
projectRef: string
startDate: string
endDate: string
interval: AnalyticsInterval
filters: AuthReportFilters
}): ReportConfig<AuthReportFilters>[] => [
{
id: 'auth-errors',
label: 'API Gateway Auth Errors',
valuePrecision: 0,
hide: false,
showTooltip: true,
showLegend: true,
showMaxValue: false,
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: 'The total number of auth errors by status code from the API Gateway.',
dataProvider: async () => {
const sql = AUTH_REPORT_SQL.ErrorsByStatus(interval, filters)
const rawData = await fetchLogs(projectRef, sql, startDate, endDate)
if (!rawData?.result) return { data: [] }
const statusCodes = extractStatusCodesFromData(rawData.result)
const attributes = generateStatusCodeAttributes(statusCodes)
const data = transformStatusCodeData(rawData.result, statusCodes)
return { data, attributes, query: sql }
},
},
{
id: 'auth-errors-by-code',
label: 'Auth Errors by Code',
valuePrecision: 0,
hide: false,
showTooltip: true,
showLegend: true,
showMaxValue: false,
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip:
'The total number of auth errors by Supabase Auth error code from the API Gateway.',
dataProvider: async () => {
const sql = AUTH_REPORT_SQL.ErrorsByAuthCode(interval, filters)
const rawData = await fetchLogs(projectRef, sql, startDate, endDate)
if (!rawData?.result) return { data: [] }
const categories = rawData.result
.map((r: any) => r.error_code)
.filter((v: any) => v !== null && v !== undefined)
const distinct = Array.from(new Set(categories)).sort()
const attributes = distinct.map((c: string) => ({
attribute: c,
label: c,
tooltip: AUTH_ERROR_CODE_LIST.find((e) => e.key === c)?.description,
}))
const pivoted = transformCategoricalCountData(rawData.result, 'error_code', distinct)
return { data: pivoted, attributes, query: sql }
},
},
]
export const createLatencyReportConfig = ({
projectRef,
startDate,
endDate,
interval,
filters,
}: {
projectRef: string
startDate: string
endDate: string
interval: AnalyticsInterval
filters: AuthReportFilters
}): ReportConfig<AuthReportFilters>[] => {
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
return [
{
id: 'sign-in-processing-time-basic',
label: 'Sign In Processing Time',
valuePrecision: 2,
hide: false,
hideHighlightedValue: true,
showTooltip: true,
showLegend: true,
showMaxValue: false,
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip:
'Basic processing time metrics for sign in operations within the auth server (excludes network latency).',
dataProvider: async () => {
const attributes = [
{
attribute: 'avg_processing_time_ms',
label: 'Avg. Processing Time (ms)',
},
{
attribute: 'min_processing_time_ms',
label: 'Min. Processing Time (ms)',
},
{
attribute: 'max_processing_time_ms',
label: 'Max. Processing Time (ms)',
},
]
const sql = AUTH_REPORT_SQL.SignInProcessingTimeBasic(interval, filters)
const rawData = await fetchLogs(projectRef, sql, startDate, endDate)
const transformedData = defaultAuthReportFormatter(rawData, attributes, groupByProvider)
return {
data: transformedData.data,
attributes: transformedData.chartAttributes,
query: sql,
}
},
},
{
id: 'sign-in-processing-time-percentiles',
label: 'Sign In Processing Time Percentiles',
valuePrecision: 2,
hide: false,
hideHighlightedValue: true,
showTooltip: true,
showLegend: true,
showMaxValue: false,
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip:
'Percentile processing time metrics for sign in operations within the auth server (excludes network latency).',
entitlement: 'auth',
requiredPlan: 'Pro',
dataProvider: async () => {
const attributes = [
{
attribute: 'p50_processing_time_ms',
label: 'P50 Processing Time (ms)',
},
{
attribute: 'p95_processing_time_ms',
label: 'P95 Processing Time (ms)',
},
{
attribute: 'p99_processing_time_ms',
label: 'P99 Processing Time (ms)',
},
]
const sql = AUTH_REPORT_SQL.SignInProcessingTimePercentiles(interval, filters)
const rawData = await fetchLogs(projectRef, sql, startDate, endDate)
const transformedData = defaultAuthReportFormatter(rawData, attributes, groupByProvider)
return {
data: transformedData.data,
attributes: transformedData.chartAttributes,
query: sql,
}
},
},
{
id: 'sign-up-processing-time-basic',
label: 'Sign Up Processing Time',
valuePrecision: 2,
hide: false,
hideHighlightedValue: true,
showTooltip: true,
showLegend: true,
showMaxValue: false,
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip:
'Basic processing time metrics for sign up operations within the auth server (excludes network latency).',
dataProvider: async () => {
const attributes = [
{
attribute: 'avg_processing_time_ms',
label: 'Avg. Processing Time (ms)',
},
{
attribute: 'min_processing_time_ms',
label: 'Min. Processing Time (ms)',
},
{
attribute: 'max_processing_time_ms',
label: 'Max. Processing Time (ms)',
},
]
const sql = AUTH_REPORT_SQL.SignUpProcessingTimeBasic(interval, filters)
const rawData = await fetchLogs(projectRef, sql, startDate, endDate)
const transformedData = defaultAuthReportFormatter(rawData, attributes, groupByProvider)
return {
data: transformedData.data,
attributes: transformedData.chartAttributes,
query: sql,
}
},
},
{
id: 'sign-up-processing-time-percentiles',
label: 'Sign Up Processing Time Percentiles',
valuePrecision: 2,
hide: false,
hideHighlightedValue: true,
showTooltip: true,
showLegend: true,
showMaxValue: false,
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip:
'Percentile processing time metrics for sign up operations within the auth server (excludes network latency).',
entitlement: 'auth',
requiredPlan: 'Pro',
dataProvider: async () => {
const attributes = [
{
attribute: 'p50_processing_time_ms',
label: 'P50 Processing Time (ms)',
},
{
attribute: 'p95_processing_time_ms',
label: 'P95 Processing Time (ms)',
},
{
attribute: 'p99_processing_time_ms',
label: 'P99 Processing Time (ms)',
},
]
const sql = AUTH_REPORT_SQL.SignUpProcessingTimePercentiles(interval, filters)
const rawData = await fetchLogs(projectRef, sql, startDate, endDate)
const transformedData = defaultAuthReportFormatter(rawData, attributes, groupByProvider)
return {
data: transformedData.data,
attributes: transformedData.chartAttributes,
query: sql,
}
},
},
]
}
export const createAuthReportConfig = ({
projectRef,
startDate,
endDate,
interval,
filters,
}: {
projectRef: string
startDate: string
endDate: string
interval: AnalyticsInterval
filters: AuthReportFilters
}): ReportConfig<AuthReportFilters>[] => [
...createUsageReportConfig({ projectRef, startDate, endDate, interval, filters }),
...createErrorsReportConfig({ projectRef, startDate, endDate, interval, filters }),
...createLatencyReportConfig({ projectRef, startDate, endDate, interval, filters }),
]