Files
supabase/apps/studio/data/reports/v2/auth.config.ts
Jordi EnricandCharis c75bb1b60d Auth Report tests and improvements (#38550)
* move to new ReportConfig

* separate types

* add auth report config

* add auth report transofmration test

* add fetchlogs util for reports

* rm auth-charts

* rm auth-report-query

* rm dupped code

* simplify upsell compo

* rm unnecessary hook

* use upsell in database report instead of old component

* rip ReportChart you wont be missed

* more tests

* rm unnecessary import

* Fix SQL in active users

* support hide inside report chart component

* add max bar size to avoid having big ass bars when theres little data

* fix incorrect timestamp key passed on to fillTimeseries

* add tooltip to auth report

* fix bug with timestamp key inside tooltips

* add report settings

* rm debugging check

* Update apps/studio/data/reports/v2/auth.config.ts

Co-authored-by: Charis <26616127+charislam@users.noreply.github.com>

* safely access result

* fix sign in latency query

* add toggleable attributes

* handle toggleable attributes

* pass on hideHighlightedValue

* fix signup latency

* pass hideHighlighted

* report chart pass on hide highlighted value

* cleanup finalChartData

* simplify tests, use timestamp for everything

* fix test

* rm dupped attribute

* add tooltip to auth errors

* Update apps/studio/data/reports/v2/auth.config.ts

Co-authored-by: Charis <26616127+charislam@users.noreply.github.com>

* Update apps/studio/components/ui/Charts/ComposedChart.tsx

Co-authored-by: Charis <26616127+charislam@users.noreply.github.com>

* prettify

---------

Co-authored-by: Charis <26616127+charislam@users.noreply.github.com>
2025-09-17 17:41:10 +02:00

515 lines
17 KiB
TypeScript

import type { AnalyticsInterval } from 'data/analytics/constants'
import { analyticsIntervalToGranularity } from 'data/reports/report.utils'
import { ReportConfig, ReportDataProviderAttribute } from './reports.types'
import { NumericFilter } from 'components/interfaces/Reports/v2/ReportsNumericFilter'
import { fetchLogs } from 'data/reports/report.utils'
import z from 'zod'
const METRIC_KEYS = [
'ActiveUsers',
'SignInAttempts',
'PasswordResetRequests',
'TotalSignUps',
'SignInLatency',
'SignUpLatency',
'ErrorsByStatus',
]
type MetricKey = (typeof METRIC_KEYS)[number]
const AUTH_REPORT_SQL: Record<
MetricKey,
(interval: AnalyticsInterval, filters?: AuthReportFilters) => string
> = {
ActiveUsers: (interval) => {
const granularity = analyticsIntervalToGranularity(interval)
return `
--active-users
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
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'
)
group by timestamp
order by timestamp desc
`
},
SignInAttempts: (interval) => {
const granularity = analyticsIntervalToGranularity(interval)
return `
--sign-in-attempts
SELECT
timestamp_trunc(timestamp, ${granularity}) as timestamp,
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"
GROUP BY
timestamp, login_type_provider
ORDER BY
timestamp desc, login_type_provider
`
},
PasswordResetRequests: (interval) => {
const granularity = analyticsIntervalToGranularity(interval)
return `
--password-reset-requests
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
count(*) as count
from auth_logs f
where json_value(f.event_message, "$.auth_event.action") = 'user_recovery_requested'
group by timestamp
order by timestamp desc
`
},
TotalSignUps: (interval) => {
const granularity = analyticsIntervalToGranularity(interval)
return `
--total-signups
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
count(*) as count
from auth_logs
where json_value(event_message, "$.auth_event.action") = 'user_signedup'
group by timestamp
order by timestamp desc
`
},
SignInLatency: (interval) => {
const granularity = analyticsIntervalToGranularity(interval)
return `
--signin-latency
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
count(*) as count,
round(avg(cast(json_value(event_message, "$.duration") as int64)) / 1000000, 2) as avg_latency_ms,
round(min(cast(json_value(event_message, "$.duration") as int64)) / 1000000, 2) as min_latency_ms,
round(max(cast(json_value(event_message, "$.duration") as int64)) / 1000000, 2) as max_latency_ms,
round(approx_quantiles(cast(json_value(event_message, "$.duration") as int64), 100)[offset(50)] / 1000000, 2) as p50_latency_ms,
round(approx_quantiles(cast(json_value(event_message, "$.duration") as int64), 100)[offset(95)] / 1000000, 2) as p95_latency_ms,
round(approx_quantiles(cast(json_value(event_message, "$.duration") as int64), 100)[offset(99)] / 1000000, 2) as p99_latency_ms
from auth_logs
where json_value(event_message, "$.auth_event.action") = 'login'
group by timestamp
order by timestamp desc
`
},
SignUpLatency: (interval) => {
const granularity = analyticsIntervalToGranularity(interval)
return `
--signup-latency
select
timestamp_trunc(timestamp, ${granularity}) as timestamp,
count(*) as count,
round(avg(cast(json_value(event_message, "$.duration") as int64)) / 1000000, 2) as avg_latency_ms,
round(min(cast(json_value(event_message, "$.duration") as int64)) / 1000000, 2) as min_latency_ms,
round(max(cast(json_value(event_message, "$.duration") as int64)) / 1000000, 2) as max_latency_ms,
round(approx_quantiles(cast(json_value(event_message, "$.duration") as int64), 100)[offset(50)] / 1000000, 2) as p50_latency_ms,
round(approx_quantiles(cast(json_value(event_message, "$.duration") as int64), 100)[offset(95)] / 1000000, 2) as p95_latency_ms,
round(approx_quantiles(cast(json_value(event_message, "$.duration") as int64), 100)[offset(99)] / 1000000, 2) as p99_latency_ms
from auth_logs
where json_value(event_message, "$.auth_event.action") = 'user_signedup'
group by timestamp
order by timestamp desc
`
},
ErrorsByStatus: (interval) => {
const granularity = analyticsIntervalToGranularity(interval)
return `
--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
where path like '%/auth%'
and response.status_code >= 400 and response.status_code <= 599
group by timestamp, status_code
order by timestamp desc
`
},
}
type AuthReportFilters = {
status_code: NumericFilter | null
}
function filterToWhereClause(filters?: AuthReportFilters): string {
if (!filters) return ''
return ``
}
/**
* 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[]
) {
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 }
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 createAuthReportConfig = ({
projectRef,
startDate,
endDate,
interval,
filters,
}: {
projectRef: string
startDate: string
endDate: string
interval: AnalyticsInterval
filters: AuthReportFilters
}): ReportConfig<AuthReportFilters>[] => [
{
id: 'active-user',
label: 'Active Users',
valuePrecision: 0,
hide: false,
showTooltip: true,
showLegend: false,
showMaxValue: false,
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: 'The total number of active users over time.',
availableIn: ['free', 'pro', 'team', 'enterprise'],
dataProvider: async () => {
const attributes = [
{ attribute: 'ActiveUsers', provider: 'logs', label: 'Active Users', enabled: true },
]
const sql = AUTH_REPORT_SQL.ActiveUsers(interval, filters)
const rawData = await fetchLogs(projectRef, sql, startDate, endDate)
const transformedData = defaultAuthReportFormatter(rawData, attributes)
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.',
availableIn: ['free', 'pro', 'team', 'enterprise'],
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)
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.',
availableIn: ['free', 'pro', 'team', 'enterprise'],
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)
return { data: transformedData.data, attributes: transformedData.chartAttributes, query: sql }
},
},
{
id: 'auth-errors',
label: 'Auth Errors',
valuePrecision: 0,
hide: false,
showTooltip: true,
showLegend: false,
showMaxValue: false,
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: 'The total number of auth errors by status code.',
availableIn: ['free', 'pro', 'team', 'enterprise'],
dataProvider: async () => {
const attributes = [
{
attribute: 'ErrorsByStatus',
provider: 'logs',
label: 'Auth Errors',
},
]
const sql = AUTH_REPORT_SQL.ErrorsByStatus(interval, filters)
const rawData = await fetchLogs(projectRef, sql, startDate, endDate)
const transformedData = defaultAuthReportFormatter(rawData, attributes)
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.',
availableIn: ['free', 'pro', 'team', 'enterprise'],
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)
return { data: transformedData.data, attributes: transformedData.chartAttributes, query: sql }
},
},
{
id: 'sign-in-latency',
label: 'Sign In Latency',
valuePrecision: 2,
hide: false,
hideHighlightedValue: true,
showTooltip: true,
showLegend: true,
showMaxValue: false,
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: 'The average latency for sign in operations by grant type.',
availableIn: ['pro', 'team', 'enterprise'],
dataProvider: async () => {
const attributes = [
{
attribute: 'avg_latency_ms',
label: 'Avg. Latency (ms)',
},
{
attribute: 'max_latency_ms',
label: 'Max. Latency (ms)',
},
{
attribute: 'min_latency_ms',
label: 'Min. Latency (ms)',
},
{
attribute: 'p50_latency_ms',
label: 'P50 Latency (ms)',
},
{
attribute: 'p95_latency_ms',
label: 'P95 Latency (ms)',
},
{
attribute: 'p99_latency_ms',
label: 'P99 Latency (ms)',
},
{
attribute: 'request_count',
label: 'Request Count',
},
]
const sql = AUTH_REPORT_SQL.SignInLatency(interval, filters)
const rawData = await fetchLogs(projectRef, sql, startDate, endDate)
const transformedData = defaultAuthReportFormatter(rawData, attributes)
return { data: transformedData.data, attributes: transformedData.chartAttributes, query: sql }
},
},
{
id: 'sign-up-latency',
label: 'Sign Up Latency',
valuePrecision: 2,
hide: false,
hideHighlightedValue: true,
showTooltip: true,
showLegend: true,
showMaxValue: false,
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: 'The average latency for sign up operations by provider.',
availableIn: ['pro', 'team', 'enterprise'],
dataProvider: async () => {
const attributes = [
{
attribute: 'avg_latency_ms',
label: 'Avg. Latency (ms)',
},
{
attribute: 'max_latency_ms',
label: 'Max. Latency (ms)',
},
{
attribute: 'min_latency_ms',
label: 'Min. Latency (ms)',
},
{
attribute: 'p50_latency_ms',
label: 'P50 Latency (ms)',
},
{
attribute: 'p95_latency_ms',
label: 'P95 Latency (ms)',
},
{
attribute: 'p99_latency_ms',
label: 'P99 Latency (ms)',
},
{
attribute: 'request_count',
label: 'Request Count',
},
]
const sql = AUTH_REPORT_SQL.SignUpLatency(interval, filters)
const rawData = await fetchLogs(projectRef, sql, startDate, endDate)
const transformedData = defaultAuthReportFormatter(rawData, attributes)
return { data: transformedData.data, attributes: transformedData.chartAttributes, query: sql }
},
},
]