mirror of
https://github.com/supabase/supabase.git
synced 2026-06-12 08:29:15 +08:00
## Summary - Adds **Data API** (API Gateway / edge logs) as a new service row in the observability overview, positioned before PostgREST - Data API row is only shown when Data API is enabled for the project (gated on `useIsDataApiEnabled`) - Renames the existing PostgREST entry from "Data API" to "PostgREST" to correctly reflect the service - Adds the Data API description to `SERVICE_DESCRIPTIONS` ## Test plan - [ ] Enable Data API for a project — Data API row appears before PostgREST in the overview with chart data - [ ] Disable Data API for a project — Data API row is hidden, PostgREST row remains - [ ] PostgREST row label now reads "PostgREST" instead of "Data API" 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Observability dashboard can optionally show an “API Gateway” service when the Data API feature is enabled; it surfaces logs and health metrics. * The service health table now includes a description/tooltip for the API Gateway and aggregates its metrics. * **Bug Fixes** * Restored and relabeled the PostgREST entry so its observability report and reporting links appear correctly. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46266?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 --> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
184 lines
5.5 KiB
TypeScript
184 lines
5.5 KiB
TypeScript
import { useQuery } from '@tanstack/react-query'
|
|
import dayjs from 'dayjs'
|
|
import utc from 'dayjs/plugin/utc'
|
|
import { useMemo } from 'react'
|
|
|
|
import type { LogsBarChartDatum } from '../ProjectHome/ProjectUsage.metrics'
|
|
import {
|
|
calculateAggregatedMetrics,
|
|
calculateDateRange,
|
|
calculateHealthMetrics,
|
|
transformToBarChartData,
|
|
type RawChartData,
|
|
} from './useServiceHealthMetrics.utils'
|
|
import { analyticsKeys } from '@/data/analytics/keys'
|
|
import {
|
|
getServiceHealth,
|
|
type ServiceHealthGranularity,
|
|
type ServiceHealthResultRow,
|
|
} from '@/data/analytics/service-health-query'
|
|
import { useFillTimeseriesSorted } from '@/hooks/analytics/useFillTimeseriesSorted'
|
|
|
|
dayjs.extend(utc)
|
|
|
|
export type ServiceKey =
|
|
| 'db'
|
|
| 'functions'
|
|
| 'auth'
|
|
| 'storage'
|
|
| 'realtime'
|
|
| 'data_api'
|
|
| 'postgrest'
|
|
|
|
export type ServiceHealthData = {
|
|
total: number
|
|
errorRate: number
|
|
successRate: number
|
|
errorCount: number
|
|
warningCount: number
|
|
okCount: number
|
|
eventChartData: LogsBarChartDatum[]
|
|
isLoading: boolean
|
|
error: unknown | null
|
|
refresh: () => void
|
|
}
|
|
|
|
const INTERVAL_TO_GRANULARITY: Record<'1hr' | '1day' | '7day', ServiceHealthGranularity> = {
|
|
'1hr': 'minute',
|
|
'1day': 'hour',
|
|
'7day': 'day',
|
|
}
|
|
|
|
/** Maps our service keys to the field names returned by the service-health endpoint */
|
|
const SERVICE_RESPONSE_KEY: Record<
|
|
ServiceKey,
|
|
Exclude<keyof ServiceHealthResultRow, 'timestamp'>
|
|
> = {
|
|
db: 'postgres_logs',
|
|
auth: 'auth_logs',
|
|
functions: 'function_edge_logs',
|
|
storage: 'storage_logs',
|
|
realtime: 'realtime_logs',
|
|
data_api: 'edge_logs',
|
|
postgrest: 'postgrest_logs',
|
|
}
|
|
|
|
/** Extracts a single service's timeseries rows from the shared service-health response */
|
|
export function extractServiceRows(rows: ServiceHealthResultRow[], serviceKey: ServiceKey) {
|
|
const responseKey = SERVICE_RESPONSE_KEY[serviceKey]
|
|
return rows.map((row) => {
|
|
const svc = row[responseKey]
|
|
return {
|
|
timestamp: row.timestamp,
|
|
ok_count: svc?.ok ?? 0,
|
|
warning_count: svc?.warning ?? 0,
|
|
error_count: svc?.error ?? 0,
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Hook to fetch and process health metrics for a single service.
|
|
* All service hooks share one deduplicated network request via the same query key.
|
|
*/
|
|
const useServiceHealthQuery = ({
|
|
projectRef,
|
|
serviceKey,
|
|
startDate,
|
|
endDate,
|
|
granularity,
|
|
enabled,
|
|
}: {
|
|
projectRef: string
|
|
serviceKey: ServiceKey
|
|
startDate: string
|
|
endDate: string
|
|
granularity: ServiceHealthGranularity
|
|
enabled: boolean
|
|
}) => {
|
|
const queryResult = useQuery({
|
|
queryKey: analyticsKeys.serviceHealth(projectRef, { startDate, endDate, granularity }),
|
|
queryFn: ({ signal }) =>
|
|
getServiceHealth({ projectRef, startDate, endDate, granularity }, signal),
|
|
enabled: enabled && Boolean(projectRef),
|
|
staleTime: 1000 * 60,
|
|
})
|
|
|
|
const rawRows = useMemo(
|
|
() => extractServiceRows(queryResult.data?.result ?? [], serviceKey),
|
|
[queryResult.data, serviceKey]
|
|
)
|
|
|
|
// Snap start/end to the granularity boundary so fillTimeseries iterates
|
|
// in sync with the API's bucketed timestamps (e.g. midnight for 'day').
|
|
const fillStart = dayjs.utc(startDate).startOf(granularity).toISOString()
|
|
const fillEnd = dayjs.utc(endDate).startOf(granularity).toISOString()
|
|
|
|
// Fill gaps in timeseries
|
|
const { data: filledData } = useFillTimeseriesSorted({
|
|
data: rawRows,
|
|
timestampKey: 'timestamp',
|
|
valueKey: ['ok_count', 'warning_count', 'error_count'],
|
|
defaultValue: 0,
|
|
startDate: fillStart,
|
|
endDate: fillEnd,
|
|
})
|
|
|
|
const eventChartData: LogsBarChartDatum[] = useMemo(
|
|
() => transformToBarChartData(filledData as RawChartData[]),
|
|
[filledData]
|
|
)
|
|
|
|
const metrics = useMemo(() => calculateHealthMetrics(eventChartData), [eventChartData])
|
|
|
|
return {
|
|
...metrics,
|
|
eventChartData,
|
|
isLoading: queryResult.isLoading,
|
|
error: queryResult.error,
|
|
refresh: queryResult.refetch,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hook to fetch observability overview data for all services using the service-health endpoint.
|
|
* One network request is made; results are fanned out to each service by field name.
|
|
*/
|
|
export const useServiceHealthMetrics = (
|
|
projectRef: string,
|
|
interval: '1hr' | '1day' | '7day',
|
|
refreshKey: number
|
|
) => {
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
const { startDate, endDate } = useMemo(() => calculateDateRange(interval), [interval, refreshKey])
|
|
|
|
const granularity = INTERVAL_TO_GRANULARITY[interval]
|
|
const enabled = Boolean(projectRef)
|
|
|
|
const sharedParams = { projectRef, startDate, endDate, granularity, enabled }
|
|
|
|
const db = useServiceHealthQuery({ ...sharedParams, serviceKey: 'db' })
|
|
const auth = useServiceHealthQuery({ ...sharedParams, serviceKey: 'auth' })
|
|
const functions = useServiceHealthQuery({ ...sharedParams, serviceKey: 'functions' })
|
|
const storage = useServiceHealthQuery({ ...sharedParams, serviceKey: 'storage' })
|
|
const realtime = useServiceHealthQuery({ ...sharedParams, serviceKey: 'realtime' })
|
|
const data_api = useServiceHealthQuery({ ...sharedParams, serviceKey: 'data_api' })
|
|
const postgrest = useServiceHealthQuery({ ...sharedParams, serviceKey: 'postgrest' })
|
|
|
|
const services: Record<ServiceKey, ServiceHealthData> = useMemo(
|
|
() => ({ db, auth, functions, storage, realtime, data_api, postgrest }),
|
|
[db, auth, functions, storage, realtime, data_api, postgrest]
|
|
)
|
|
|
|
const aggregated = useMemo(() => calculateAggregatedMetrics(Object.values(services)), [services])
|
|
|
|
const isLoading = Object.values(services).some((s) => s.isLoading)
|
|
|
|
return {
|
|
services,
|
|
aggregated,
|
|
isLoading,
|
|
endDate,
|
|
}
|
|
}
|