Files
supabase/apps/studio/components/interfaces/Auth/Overview/OverviewErrors.constants.ts
Jordi Enric d2ed60da27 fix(auth): migrate overview errors to clickhouse (#50174)
## Problem

Auth overview error tables call the legacy logs endpoint through
fetchLogs defaults, even with the ClickHouse migration enabled.
Success-rate cards also show zero when there are no requests and
misleading relative changes between small rates.

## Fix

Select matching BigQuery or ClickHouse queries with otelLegacyLogs,
separate caches by engine, and normalize numeric results.

Show No data for success rates without requests and omit comparisons
when either period has no requests. Show success-rate changes in
percentage points: 0% to 0.2% displays +0.2 pp. Omit undefined relative
changes from a zero baseline for activity and sign-up counts. Show
explicit errors for failed log requests, including error payloads
returned with HTTP 200, instead of empty tables.

## Validation

- Auth overview error tables compared against staging with matching
data.
- 84 focused tests passed across four suites, including 25 direct
formatter tests.
- 12 MSW integration tests exercise both endpoint/SQL pairs, HTTP and
embedded API failures, and rendered No data, genuine 0%, and +0.2 pp
states.
- Unit tests cover missing periods, zero requests, percentage-point and
relative changes, SQL structure, and numeric result parsing.
- Formatting and diff checks passed; code review found no actionable
issues.
- Full local lint/typecheck are limited by shared checkout dependencies.
Browser comparison confirmed the deployed rate display uses percentage
points and shows No data without a comparison for absent server
requests; populated error rows match staging. The final formatter
extraction (9886b78682) was also deployed and verified in the browser;
CI completion remains outstanding.

Split from #50173.


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

## Summary by CodeRabbit

- **New Features**
- Added OpenTelemetry support for authentication error and metrics
reporting.
- Added clearer handling of missing metric data, including “No data”
states.
- Improved success-rate change calculations using percentage-point
differences.
  - Added user-visible error states when analytics requests fail.

- **Bug Fixes**
- Improved validation and handling of authentication metrics and error
data.
  - Corrected formatting and rounding for metric values and changes.

- **Tests**
- Expanded coverage for legacy and OpenTelemetry analytics, error
handling, empty data, formatting, and edge cases.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-14 15:35:03 +02:00

136 lines
4.0 KiB
TypeScript

import dayjs from 'dayjs'
import { z } from 'zod'
import { pickLogsQueryBuilder } from '@/data/logs/logs-endpoint'
import { safeSql } from '@/data/logs/safe-analytics-sql'
import { fetchLogs } from '@/data/reports/report.utils'
export type ResponseErrorRow = {
method: string
path: string
status_code: number
count: number
}
export type AuthErrorCodeRow = {
error_code: string
count: number
}
export const getDateRange = () => {
return {
start: dayjs().subtract(24, 'hour').toISOString(),
end: dayjs().toISOString(),
}
}
// Top API response errors for /auth/v1 endpoints (path/method/status)
export const AUTH_TOP_RESPONSE_ERRORS_SQL = safeSql`
select
request.method as method,
request.path as path,
response.status_code as status_code,
count(*) as count
from edge_logs
cross join unnest(metadata) as m
cross join unnest(m.request) as request
cross join unnest(m.response) as response
where path like '%auth/v1%'
and response.status_code between 400 and 599
group by method, path, status_code
order by count desc
limit 10
`
// Top Auth service error codes from x_sb_error_code header for /auth/v1 endpoints
export const AUTH_TOP_ERROR_CODES_SQL = safeSql`
select
h.x_sb_error_code as error_code,
count(*) as count
from edge_logs
cross join unnest(metadata) as m
cross join unnest(m.request) as request
cross join unnest(m.response) as response
cross join unnest(response.headers) as h
where path like '%auth/v1%'
and response.status_code between 400 and 599
and h.x_sb_error_code is not null
group by error_code
order by count desc
limit 10
`
export const AUTH_TOP_RESPONSE_ERRORS_SQL_OTEL = safeSql`
select
log_attributes['request.method'] as method,
log_attributes['request.path'] as path,
toInt32OrZero(log_attributes['response.status_code']) as status_code,
count() as count
from logs
where source = 'edge_logs'
and path like '%auth/v1%'
and status_code between 400 and 599
group by method, path, status_code
order by count desc
limit 10
`
export const AUTH_TOP_ERROR_CODES_SQL_OTEL = safeSql`
select
log_attributes['response.headers.x_sb_error_code'] as error_code,
count() as count
from logs
where source = 'edge_logs'
and log_attributes['request.path'] like '%auth/v1%'
and toInt32OrZero(log_attributes['response.status_code']) between 400 and 599
and error_code != ''
group by error_code
order by count desc
limit 10
`
const countSchema = z.union([z.number(), z.string().min(1)]).pipe(z.coerce.number().finite())
const responseErrorSchema = z.object({
method: z.string(),
path: z.string(),
status_code: countSchema,
count: countSchema,
})
const authErrorCodeSchema = z.object({ error_code: z.string(), count: countSchema })
export const parseResponseErrors = (rows: unknown[]): ResponseErrorRow[] =>
rows.flatMap((row) => {
const result = responseErrorSchema.safeParse(row)
return result.success ? [result.data] : []
})
export const parseAuthErrorCodes = (rows: unknown[]): AuthErrorCodeRow[] =>
rows.flatMap((row) => {
const result = authErrorCodeSchema.safeParse(row)
return result.success ? [result.data] : []
})
export const fetchTopResponseErrors = async (projectRef: string, useOtel = false) => {
const { start, end } = getDateRange()
const sql = pickLogsQueryBuilder(
useOtel,
AUTH_TOP_RESPONSE_ERRORS_SQL_OTEL,
AUTH_TOP_RESPONSE_ERRORS_SQL
)
const data = await fetchLogs(projectRef, sql, start, end, useOtel)
if (data?.error) {
throw new Error(typeof data.error === 'string' ? data.error : data.error.message)
}
return data
}
export const fetchTopAuthErrorCodes = async (projectRef: string, useOtel = false) => {
const { start, end } = getDateRange()
const sql = pickLogsQueryBuilder(useOtel, AUTH_TOP_ERROR_CODES_SQL_OTEL, AUTH_TOP_ERROR_CODES_SQL)
const data = await fetchLogs(projectRef, sql, start, end, useOtel)
if (data?.error) {
throw new Error(typeof data.error === 'string' ? data.error : data.error.message)
}
return data
}