mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
## Problem Auth observability charts always queried the legacy logs.all endpoint, even when the OTEL reports rollout was enabled. The existing OTEL SQL also had ClickHouse correctness and parity gaps around timestamp aliasing, JSON types, provider paths, missing values, and error-code attributes. ## Fix Route the ten Auth-specific charts through the OTEL query builders and logs.all.otel endpoint when otelReports is enabled. Preserve the BigQuery fallback, partition React Query caches by backend, and leave the shared API gateway charts on the legacy endpoint. Correct the OTEL queries by qualifying source timestamps, using typed and nullable JSON extraction, preserving missing actor and duration semantics, selecting the right provider path for each event shape, preferring the canonical Auth error-code attribute with a legacy fallback, and applying bounded result limits. Two-minute report intervals now use minute-level SQL buckets instead of falling through to hourly buckets. ## How to test - Run `CI=1 pnpm --filter studio exec vitest run data/reports/v2/auth.config.otel.test.ts hooks/misc/__tests__/useReportDateRange.test.ts` - Run `pnpm --filter studio run lint:ratchet` - Run `pnpm --filter studio run typecheck` - Expected result: all checks pass and generated OTEL SQL preserves legacy report semantics. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Auth observability charts can now use OpenTelemetry data when enabled, while retaining the existing reporting source otherwise. - Switching the data source automatically refreshes the relevant charts. - **Bug Fixes** - Improved Auth observability accuracy for provider, duration, actor, and error-code reporting. - Added safeguards to keep report queries within the supported result limit. - Corrected minute-level grouping for two-minute analytics intervals and three-hour date ranges. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1118 lines
39 KiB
TypeScript
1118 lines
39 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,
|
|
type Granularity,
|
|
} 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',
|
|
] as const
|
|
|
|
type MetricKey = (typeof METRIC_KEYS)[number]
|
|
|
|
type AuthReportFilters = {
|
|
status_code?: NumericFilter | null
|
|
provider?: string[] | null
|
|
}
|
|
|
|
const PROVIDER_SELECT_FRAGMENT = safeSql`COALESCE(JSON_VALUE(event_message, "$.provider"), 'unknown') as provider,`
|
|
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
|
|
}
|
|
|
|
function authFilterSql(filters?: AuthReportFilters): SafeLogSqlFragment {
|
|
const conditions: SafeLogSqlFragment[] = []
|
|
|
|
if (filters?.status_code) {
|
|
const op = SAFE_COMPARISON_OPERATOR_SQL[filters.status_code.operator]
|
|
conditions.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), ', ')
|
|
conditions.push(safeSql`JSON_VALUE(event_message, "$.provider") IN (${list})`)
|
|
}
|
|
|
|
if (conditions.length === 0) return EMPTY
|
|
return safeSql`AND ${joinSqlFragments(conditions, ' AND ')}`
|
|
}
|
|
|
|
function edgeLogsFilterSql(filters?: AuthReportFilters): SafeLogSqlFragment {
|
|
const conditions: SafeLogSqlFragment[] = []
|
|
|
|
if (filters?.status_code) {
|
|
const op = SAFE_COMPARISON_OPERATOR_SQL[filters.status_code.operator]
|
|
conditions.push(
|
|
safeSql`response.status_code ${op} ${analyticsLiteral(filters.status_code.value)}`
|
|
)
|
|
}
|
|
|
|
if (conditions.length === 0) return EMPTY
|
|
return safeSql`AND ${joinSqlFragments(conditions, ' AND ')}`
|
|
}
|
|
|
|
function authQuerySetup(interval: AnalyticsInterval, filters?: AuthReportFilters) {
|
|
return {
|
|
granularity: SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)],
|
|
filterSql: authFilterSql(filters),
|
|
groupByProvider: Boolean(filters?.provider && filters.provider.length > 0),
|
|
}
|
|
}
|
|
|
|
const AUTH_REPORT_SQL: Record<
|
|
MetricKey,
|
|
(interval: AnalyticsInterval, filters?: AuthReportFilters) => SafeLogSqlFragment
|
|
> = {
|
|
ActiveUsers: (interval, filters) => {
|
|
const { granularity, filterSql, groupByProvider } = authQuerySetup(interval, filters)
|
|
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'
|
|
)
|
|
${filterSql}
|
|
group by timestamp${providerGroupBy(groupByProvider)}
|
|
order by timestamp desc${providerGroupBy(groupByProvider)}
|
|
`
|
|
},
|
|
SignInAttempts: (interval, filters) => {
|
|
const { granularity, filterSql, groupByProvider } = authQuerySetup(interval, filters)
|
|
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"
|
|
${filterSql}
|
|
GROUP BY
|
|
timestamp, login_type_provider${providerGroupBy(groupByProvider)}
|
|
ORDER BY
|
|
timestamp desc, login_type_provider${providerGroupBy(groupByProvider)}
|
|
`
|
|
},
|
|
PasswordResetRequests: (interval, filters) => {
|
|
const { granularity, filterSql, groupByProvider } = authQuerySetup(interval, filters)
|
|
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'
|
|
${filterSql}
|
|
group by timestamp${providerGroupBy(groupByProvider)}
|
|
order by timestamp desc${providerGroupBy(groupByProvider)}
|
|
`
|
|
},
|
|
TotalSignUps: (interval, filters) => {
|
|
const { granularity, filterSql, groupByProvider } = authQuerySetup(interval, filters)
|
|
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'
|
|
${filterSql}
|
|
group by timestamp${providerGroupBy(groupByProvider)}
|
|
order by timestamp desc${providerGroupBy(groupByProvider)}
|
|
`
|
|
},
|
|
SignInProcessingTimeBasic: (interval, filters) => {
|
|
const { granularity, filterSql, groupByProvider } = authQuerySetup(interval, filters)
|
|
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'
|
|
${filterSql}
|
|
group by timestamp${providerGroupBy(groupByProvider)}
|
|
order by timestamp desc${providerGroupBy(groupByProvider)}
|
|
`
|
|
},
|
|
SignInProcessingTimePercentiles: (interval, filters) => {
|
|
const { granularity, filterSql, groupByProvider } = authQuerySetup(interval, filters)
|
|
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'
|
|
${filterSql}
|
|
group by timestamp${providerGroupBy(groupByProvider)}
|
|
order by timestamp desc${providerGroupBy(groupByProvider)}
|
|
`
|
|
},
|
|
SignUpProcessingTimeBasic: (interval, filters) => {
|
|
const { granularity, filterSql, groupByProvider } = authQuerySetup(interval, filters)
|
|
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'
|
|
${filterSql}
|
|
group by timestamp${providerGroupBy(groupByProvider)}
|
|
order by timestamp desc${providerGroupBy(groupByProvider)}
|
|
`
|
|
},
|
|
SignUpProcessingTimePercentiles: (interval, filters) => {
|
|
const { granularity, filterSql, groupByProvider } = authQuerySetup(interval, filters)
|
|
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'
|
|
${filterSql}
|
|
group by timestamp${providerGroupBy(groupByProvider)}
|
|
order by timestamp desc${providerGroupBy(groupByProvider)}
|
|
`
|
|
},
|
|
ErrorsByStatus: (interval, filters) => {
|
|
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
|
|
const filterSql = edgeLogsFilterSql(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
|
|
${filterSql}
|
|
group by timestamp, status_code
|
|
order by timestamp desc
|
|
`
|
|
},
|
|
ErrorsByAuthCode: (interval, filters) => {
|
|
const granularity = SAFE_GRANULARITY_SQL[analyticsIntervalToGranularity(interval)]
|
|
const filterSql = edgeLogsFilterSql(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
|
|
${filterSql}
|
|
group by timestamp, error_code
|
|
order by timestamp desc
|
|
`
|
|
},
|
|
}
|
|
|
|
// fillTimeseries/isUnixMicro expects a 16-digit unix-microsecond timestamp, matching BigQuery's timestamp_trunc.
|
|
const OTEL_TIMESTAMP: Record<Granularity, SafeLogSqlFragment> = {
|
|
minute: safeSql`toUnixTimestamp(toStartOfMinute(logs.timestamp)) * 1000000`,
|
|
hour: safeSql`toUnixTimestamp(toStartOfHour(logs.timestamp)) * 1000000`,
|
|
day: safeSql`toUnixTimestamp(toStartOfDay(logs.timestamp)) * 1000000`,
|
|
}
|
|
|
|
const AUDIT_PROVIDER_OTEL = safeSql`JSONExtractString(event_message, 'auth_event', 'traits', 'provider')`
|
|
const METERING_PROVIDER_OTEL = safeSql`JSONExtractString(event_message, 'provider')`
|
|
const AUTH_DURATION_OTEL = safeSql`JSONExtract(event_message, 'duration', 'Nullable(Int64)')`
|
|
const AUTH_ERROR_CODE_OTEL = safeSql`coalesce(nullIf(log_attributes['response.headers.sb_error_code'], ''), nullIf(log_attributes['response.headers.x_sb_error_code'], ''))`
|
|
|
|
function providerSelectFragmentOtel(
|
|
groupByProvider: boolean,
|
|
provider: SafeLogSqlFragment
|
|
): SafeLogSqlFragment {
|
|
return groupByProvider
|
|
? safeSql`coalesce(nullIf(${provider}, ''), 'unknown') as provider,`
|
|
: EMPTY
|
|
}
|
|
|
|
// auth_logs rows have no HTTP response fields, so status_code can't apply here.
|
|
function authOtelFilterSql(
|
|
provider: SafeLogSqlFragment,
|
|
filters?: AuthReportFilters
|
|
): SafeLogSqlFragment {
|
|
if (filters?.provider && filters.provider.length > 0) {
|
|
const list = joinSqlFragments(filters.provider.map(analyticsLiteral), ', ')
|
|
return safeSql`AND ${provider} IN (${list})`
|
|
}
|
|
return EMPTY
|
|
}
|
|
|
|
// edge_logs rows have no auth provider field, so provider can't apply here.
|
|
function edgeLogsOtelFilterSql(filters?: AuthReportFilters): SafeLogSqlFragment {
|
|
if (filters?.status_code) {
|
|
const op = SAFE_COMPARISON_OPERATOR_SQL[filters.status_code.operator]
|
|
return safeSql`AND toInt32OrZero(log_attributes['response.status_code']) ${op} ${analyticsLiteral(filters.status_code.value)}`
|
|
}
|
|
return EMPTY
|
|
}
|
|
|
|
function authOtelQuerySetup(
|
|
interval: AnalyticsInterval,
|
|
filters?: AuthReportFilters,
|
|
provider = AUDIT_PROVIDER_OTEL
|
|
) {
|
|
return {
|
|
ts: OTEL_TIMESTAMP[analyticsIntervalToGranularity(interval)],
|
|
filterSql: authOtelFilterSql(provider, filters),
|
|
groupByProvider: Boolean(filters?.provider && filters.provider.length > 0),
|
|
provider,
|
|
}
|
|
}
|
|
|
|
export const AUTH_REPORT_SQL_OTEL: Record<
|
|
MetricKey,
|
|
(interval: AnalyticsInterval, filters?: AuthReportFilters) => SafeLogSqlFragment
|
|
> = {
|
|
ActiveUsers: (interval, filters) => {
|
|
const { ts, filterSql, groupByProvider, provider } = authOtelQuerySetup(interval, filters)
|
|
return safeSql`
|
|
--active-users (otel)
|
|
select
|
|
${ts} as timestamp,
|
|
${providerSelectFragmentOtel(groupByProvider, provider)}
|
|
countDistinct(nullIf(JSONExtractString(event_message, 'auth_event', 'actor_id'), '')) as count
|
|
from logs
|
|
where source = 'auth_logs'
|
|
and JSONExtractString(event_message, 'auth_event', 'action') in (
|
|
'login', 'user_signedup', 'token_refreshed', 'user_modified',
|
|
'user_recovery_requested', 'user_reauthenticate_requested'
|
|
)
|
|
${filterSql}
|
|
group by ${ts}${providerGroupBy(groupByProvider)}
|
|
order by ${ts} desc${providerGroupBy(groupByProvider)}
|
|
limit 50000
|
|
`
|
|
},
|
|
SignInAttempts: (interval, filters) => {
|
|
const { ts, filterSql, groupByProvider, provider } = authOtelQuerySetup(
|
|
interval,
|
|
filters,
|
|
METERING_PROVIDER_OTEL
|
|
)
|
|
return safeSql`
|
|
--sign-in-attempts (otel)
|
|
select
|
|
${ts} as timestamp,
|
|
${providerSelectFragmentOtel(groupByProvider, provider)}
|
|
case
|
|
when JSONExtractString(event_message, 'provider') != ''
|
|
then concat(
|
|
JSONExtractString(event_message, 'login_method'),
|
|
' (',
|
|
JSONExtractString(event_message, 'provider'),
|
|
')'
|
|
)
|
|
else JSONExtractString(event_message, 'login_method')
|
|
end as login_type_provider,
|
|
count() as count
|
|
from logs
|
|
where source = 'auth_logs'
|
|
and JSONExtractString(event_message, 'action') = 'login'
|
|
and JSONExtractBool(event_message, 'metering') = 1
|
|
${filterSql}
|
|
group by ${ts}, login_type_provider${providerGroupBy(groupByProvider)}
|
|
order by ${ts} desc, login_type_provider${providerGroupBy(groupByProvider)}
|
|
limit 50000
|
|
`
|
|
},
|
|
PasswordResetRequests: (interval, filters) => {
|
|
const { ts, filterSql, groupByProvider, provider } = authOtelQuerySetup(interval, filters)
|
|
return safeSql`
|
|
--password-reset-requests (otel)
|
|
select
|
|
${ts} as timestamp,
|
|
${providerSelectFragmentOtel(groupByProvider, provider)}
|
|
count() as count
|
|
from logs
|
|
where source = 'auth_logs'
|
|
and JSONExtractString(event_message, 'auth_event', 'action') = 'user_recovery_requested'
|
|
${filterSql}
|
|
group by ${ts}${providerGroupBy(groupByProvider)}
|
|
order by ${ts} desc${providerGroupBy(groupByProvider)}
|
|
limit 50000
|
|
`
|
|
},
|
|
TotalSignUps: (interval, filters) => {
|
|
const { ts, filterSql, groupByProvider, provider } = authOtelQuerySetup(interval, filters)
|
|
return safeSql`
|
|
--total-signups (otel)
|
|
select
|
|
${ts} as timestamp,
|
|
${providerSelectFragmentOtel(groupByProvider, provider)}
|
|
count() as count
|
|
from logs
|
|
where source = 'auth_logs'
|
|
and JSONExtractString(event_message, 'auth_event', 'action') = 'user_signedup'
|
|
${filterSql}
|
|
group by ${ts}${providerGroupBy(groupByProvider)}
|
|
order by ${ts} desc${providerGroupBy(groupByProvider)}
|
|
limit 50000
|
|
`
|
|
},
|
|
SignInProcessingTimeBasic: (interval, filters) => {
|
|
const { ts, filterSql, groupByProvider, provider } = authOtelQuerySetup(interval, filters)
|
|
return safeSql`
|
|
--signin-processing-time-basic (otel)
|
|
select
|
|
${ts} as timestamp,
|
|
${providerSelectFragmentOtel(groupByProvider, provider)}
|
|
count() as count,
|
|
round(avg(${AUTH_DURATION_OTEL}) / 1000000, 2) as avg_processing_time_ms,
|
|
round(min(${AUTH_DURATION_OTEL}) / 1000000, 2) as min_processing_time_ms,
|
|
round(max(${AUTH_DURATION_OTEL}) / 1000000, 2) as max_processing_time_ms
|
|
from logs
|
|
where source = 'auth_logs'
|
|
and JSONExtractString(event_message, 'auth_event', 'action') = 'login'
|
|
${filterSql}
|
|
group by ${ts}${providerGroupBy(groupByProvider)}
|
|
order by ${ts} desc${providerGroupBy(groupByProvider)}
|
|
limit 50000
|
|
`
|
|
},
|
|
SignInProcessingTimePercentiles: (interval, filters) => {
|
|
const { ts, filterSql, groupByProvider, provider } = authOtelQuerySetup(interval, filters)
|
|
return safeSql`
|
|
--signin-processing-time-percentiles (otel)
|
|
select
|
|
${ts} as timestamp,
|
|
${providerSelectFragmentOtel(groupByProvider, provider)}
|
|
count() as count,
|
|
round(quantile(0.5)(${AUTH_DURATION_OTEL}) / 1000000, 2) as p50_processing_time_ms,
|
|
round(quantile(0.95)(${AUTH_DURATION_OTEL}) / 1000000, 2) as p95_processing_time_ms,
|
|
round(quantile(0.99)(${AUTH_DURATION_OTEL}) / 1000000, 2) as p99_processing_time_ms
|
|
from logs
|
|
where source = 'auth_logs'
|
|
and JSONExtractString(event_message, 'auth_event', 'action') = 'login'
|
|
${filterSql}
|
|
group by ${ts}${providerGroupBy(groupByProvider)}
|
|
order by ${ts} desc${providerGroupBy(groupByProvider)}
|
|
limit 50000
|
|
`
|
|
},
|
|
SignUpProcessingTimeBasic: (interval, filters) => {
|
|
const { ts, filterSql, groupByProvider, provider } = authOtelQuerySetup(interval, filters)
|
|
return safeSql`
|
|
--signup-processing-time-basic (otel)
|
|
select
|
|
${ts} as timestamp,
|
|
${providerSelectFragmentOtel(groupByProvider, provider)}
|
|
count() as count,
|
|
round(avg(${AUTH_DURATION_OTEL}) / 1000000, 2) as avg_processing_time_ms,
|
|
round(min(${AUTH_DURATION_OTEL}) / 1000000, 2) as min_processing_time_ms,
|
|
round(max(${AUTH_DURATION_OTEL}) / 1000000, 2) as max_processing_time_ms
|
|
from logs
|
|
where source = 'auth_logs'
|
|
and JSONExtractString(event_message, 'auth_event', 'action') = 'user_signedup'
|
|
${filterSql}
|
|
group by ${ts}${providerGroupBy(groupByProvider)}
|
|
order by ${ts} desc${providerGroupBy(groupByProvider)}
|
|
limit 50000
|
|
`
|
|
},
|
|
SignUpProcessingTimePercentiles: (interval, filters) => {
|
|
const { ts, filterSql, groupByProvider, provider } = authOtelQuerySetup(interval, filters)
|
|
return safeSql`
|
|
--signup-processing-time-percentiles (otel)
|
|
select
|
|
${ts} as timestamp,
|
|
${providerSelectFragmentOtel(groupByProvider, provider)}
|
|
count() as count,
|
|
round(quantile(0.5)(${AUTH_DURATION_OTEL}) / 1000000, 2) as p50_processing_time_ms,
|
|
round(quantile(0.95)(${AUTH_DURATION_OTEL}) / 1000000, 2) as p95_processing_time_ms,
|
|
round(quantile(0.99)(${AUTH_DURATION_OTEL}) / 1000000, 2) as p99_processing_time_ms
|
|
from logs
|
|
where source = 'auth_logs'
|
|
and JSONExtractString(event_message, 'auth_event', 'action') = 'user_signedup'
|
|
${filterSql}
|
|
group by ${ts}${providerGroupBy(groupByProvider)}
|
|
order by ${ts} desc${providerGroupBy(groupByProvider)}
|
|
limit 50000
|
|
`
|
|
},
|
|
ErrorsByStatus: (interval, filters) => {
|
|
const ts = OTEL_TIMESTAMP[analyticsIntervalToGranularity(interval)]
|
|
const filterSql = edgeLogsOtelFilterSql(filters)
|
|
return safeSql`
|
|
--auth-errors-by-status (otel)
|
|
select
|
|
${ts} as timestamp,
|
|
count() as count,
|
|
toInt32OrZero(log_attributes['response.status_code']) as status_code
|
|
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
|
|
${filterSql}
|
|
group by ${ts}, status_code
|
|
order by ${ts} desc
|
|
limit 50000
|
|
`
|
|
},
|
|
ErrorsByAuthCode: (interval, filters) => {
|
|
const ts = OTEL_TIMESTAMP[analyticsIntervalToGranularity(interval)]
|
|
const filterSql = edgeLogsOtelFilterSql(filters)
|
|
return safeSql`
|
|
--auth-errors-by-code (otel)
|
|
select
|
|
${ts} as timestamp,
|
|
count() as count,
|
|
${AUTH_ERROR_CODE_OTEL} as error_code
|
|
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
|
|
${filterSql}
|
|
group by ${ts}, error_code
|
|
order by ${ts} desc
|
|
limit 50000
|
|
`
|
|
},
|
|
}
|
|
|
|
export function defaultAuthReportFormatter(
|
|
rawData: unknown,
|
|
attributes: ReportDataProviderAttribute[],
|
|
groupByProvider = false
|
|
) {
|
|
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: attributes }
|
|
|
|
if (groupByProvider) {
|
|
const providers = new Set<string>()
|
|
result.forEach((p: any) => {
|
|
if (p.provider) {
|
|
providers.add(p.provider)
|
|
}
|
|
})
|
|
|
|
const providerAttributes: ReportDataProviderAttribute[] = []
|
|
providers.forEach((provider) => {
|
|
attributes.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 {
|
|
const timestamps = new Set<string>(result.map((p: any) => String(p.timestamp)))
|
|
const data = Array.from(timestamps)
|
|
.sort()
|
|
.map((timestamp) => {
|
|
const point: any = { timestamp }
|
|
attributes.forEach((attr) => {
|
|
point[attr.attribute] = 0
|
|
})
|
|
const matchingPoints = result.filter((p: any) => String(p.timestamp) === timestamp)
|
|
|
|
matchingPoints.forEach((p: any) => {
|
|
attributes.forEach((attr) => {
|
|
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: attributes }
|
|
}
|
|
}
|
|
|
|
export const createUsageReportConfig = ({
|
|
projectRef,
|
|
startDate,
|
|
endDate,
|
|
interval,
|
|
filters,
|
|
useOtel = false,
|
|
}: {
|
|
projectRef: string
|
|
startDate: string
|
|
endDate: string
|
|
interval: AnalyticsInterval
|
|
filters: AuthReportFilters
|
|
useOtel?: boolean
|
|
}): ReportConfig<AuthReportFilters>[] => {
|
|
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
|
|
const queries = useOtel ? AUTH_REPORT_SQL_OTEL : AUTH_REPORT_SQL
|
|
|
|
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 = queries.ActiveUsers(interval, filters)
|
|
|
|
const rawData = await fetchLogs(projectRef, sql, startDate, endDate, useOtel)
|
|
|
|
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 = queries.SignInAttempts(interval, filters)
|
|
const rawData = await fetchLogs(projectRef, sql, startDate, endDate, useOtel)
|
|
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 = queries.TotalSignUps(interval, filters)
|
|
const rawData = await fetchLogs(projectRef, sql, startDate, endDate, useOtel)
|
|
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 = queries.PasswordResetRequests(interval, filters)
|
|
const rawData = await fetchLogs(projectRef, sql, startDate, endDate, useOtel)
|
|
const transformedData = defaultAuthReportFormatter(rawData, attributes, groupByProvider)
|
|
|
|
return {
|
|
data: transformedData.data,
|
|
attributes: transformedData.chartAttributes,
|
|
query: sql,
|
|
}
|
|
},
|
|
},
|
|
]
|
|
}
|
|
|
|
export const createErrorsReportConfig = ({
|
|
projectRef,
|
|
startDate,
|
|
endDate,
|
|
interval,
|
|
filters,
|
|
useOtel = false,
|
|
}: {
|
|
projectRef: string
|
|
startDate: string
|
|
endDate: string
|
|
interval: AnalyticsInterval
|
|
filters: AuthReportFilters
|
|
useOtel?: boolean
|
|
}): ReportConfig<AuthReportFilters>[] => {
|
|
const queries = useOtel ? AUTH_REPORT_SQL_OTEL : AUTH_REPORT_SQL
|
|
return [
|
|
{
|
|
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 = queries.ErrorsByStatus(interval, filters)
|
|
const rawData = await fetchLogs(projectRef, sql, startDate, endDate, useOtel)
|
|
|
|
if (!rawData?.result) return { data: [], query: sql }
|
|
|
|
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 = queries.ErrorsByAuthCode(interval, filters)
|
|
const rawData = await fetchLogs(projectRef, sql, startDate, endDate, useOtel)
|
|
|
|
if (!rawData?.result) return { data: [], query: sql }
|
|
|
|
const rows = z
|
|
.array(z.object({ error_code: z.string().nullish() }).catchall(z.unknown()))
|
|
.parse(rawData.result)
|
|
|
|
const categories = rows
|
|
.map((r) => r.error_code)
|
|
.filter((v): v is string => 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(rows, 'error_code', distinct)
|
|
|
|
return { data: pivoted, attributes, query: sql }
|
|
},
|
|
},
|
|
]
|
|
}
|
|
|
|
export const createLatencyReportConfig = ({
|
|
projectRef,
|
|
startDate,
|
|
endDate,
|
|
interval,
|
|
filters,
|
|
useOtel = false,
|
|
}: {
|
|
projectRef: string
|
|
startDate: string
|
|
endDate: string
|
|
interval: AnalyticsInterval
|
|
filters: AuthReportFilters
|
|
useOtel?: boolean
|
|
}): ReportConfig<AuthReportFilters>[] => {
|
|
const groupByProvider = Boolean(filters?.provider && filters.provider.length > 0)
|
|
const queries = useOtel ? AUTH_REPORT_SQL_OTEL : AUTH_REPORT_SQL
|
|
|
|
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 = queries.SignInProcessingTimeBasic(interval, filters)
|
|
const rawData = await fetchLogs(projectRef, sql, startDate, endDate, useOtel)
|
|
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 = queries.SignInProcessingTimePercentiles(interval, filters)
|
|
const rawData = await fetchLogs(projectRef, sql, startDate, endDate, useOtel)
|
|
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 = queries.SignUpProcessingTimeBasic(interval, filters)
|
|
const rawData = await fetchLogs(projectRef, sql, startDate, endDate, useOtel)
|
|
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 = queries.SignUpProcessingTimePercentiles(interval, filters)
|
|
const rawData = await fetchLogs(projectRef, sql, startDate, endDate, useOtel)
|
|
const transformedData = defaultAuthReportFormatter(rawData, attributes, groupByProvider)
|
|
|
|
return {
|
|
data: transformedData.data,
|
|
attributes: transformedData.chartAttributes,
|
|
query: sql,
|
|
}
|
|
},
|
|
},
|
|
]
|
|
}
|