Files
supabase/apps/studio/components/interfaces/Observability/useServiceHealthMetrics.test.ts
Jordi Enric 4ca7e66153 feat(observability): migrate overview to service-health endpoint (#46100)
## Problem

The observability overview page fetched service health data by making
six separate calls to the generic \`logs.all\` endpoint with
hand-crafted SQL (via \`genChartQuery\`). This coupled the overview to
SQL internals and missed out on the purpose-built \`service-health\`
endpoint that accepts structured \`lql\` filters and a \`granularity\`
parameter.

## Fix

- Added \`/platform/projects/{ref}/analytics/endpoints/service-health\`
to \`platform.d.ts\`, including the \`ProjectServiceHealthResponse\`
schema and \`UsageApiController_getProjectServiceHealth\` operation.
- Created \`apps/studio/data/analytics/service-health-query.ts\` with a
\`getServiceHealth\` fetch function and \`useServiceHealthQuery\` hook
following the same pattern as other analytics query files.
- Added a \`serviceHealth\` key factory to
\`apps/studio/data/analytics/keys.ts\`.
- Rewrote \`useServiceHealthMetrics.ts\` to call the new endpoint per
service using \`lql\` selectors (\`s:postgres_logs\`, \`s:auth_logs\`,
etc.) and a \`granularity\` value derived from the selected interval
(\`1hr\` -> \`minute\`, \`1day\` -> \`hour\`, \`7day\` -> \`day\`). The
timeseries normalisation and chart data pipeline is unchanged.
- Updated the refresh handler in \`ObservabilityOverview.tsx\` to
invalidate the new query key prefix and removed the now-unused
\`postgrest-overview-metrics\` invalidation.

## How to test

- Navigate to a project's Observability > Overview page.
- Verify that the Service Health table loads data for all six services
(Database, Auth, Edge Functions, Realtime, Storage, Data API).
- Switch between the 1hr, 1day, and 7day interval selectors and confirm
the charts update.
- Click the Refresh button and confirm the charts reload.
- Click a bar in any chart and confirm navigation to the corresponding
logs page scoped to that time window.
- Confirm no regressions in the Database Infrastructure section (CPU,
RAM, disk, connections).

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

* **New Features**
* Centralized service‑health fetching for consistent cross‑service
metrics and improved charting.
* New analytics key and backend endpoint for project service‑health; API
schemas added.
* Backend support for an additional log‑drain type (hidden from the UI).

* **Bug Fixes**
  * Improved refresh behavior for service‑health data.
* Clear "No requests in this period" fallback and correct charts when
totals are zero.

* **Tests**
* Added unit tests for service‑health data extraction and
transformation.

<!-- 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/46100?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>
2026-05-22 10:01:54 +02:00

85 lines
3.2 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { extractServiceRows } from './useServiceHealthMetrics'
import type { ServiceHealthResultRow } from '@/data/analytics/service-health-query'
const makeRow = (overrides?: Partial<ServiceHealthResultRow>): ServiceHealthResultRow => ({
timestamp: '2026-05-20T10:00:00',
postgres_logs: { ok: 10, warning: 1, error: 2, total: 13 },
auth_logs: { ok: 5, warning: 0, error: 1, total: 6 },
function_edge_logs: { ok: 20, warning: 2, error: 0, total: 22 },
storage_logs: { ok: 3, warning: 0, error: 0, total: 3 },
realtime_logs: { ok: 0, warning: 0, error: 0, total: 0 },
postgrest_logs: { ok: 7, warning: 1, error: 1, total: 9 },
edge_logs: { ok: 100, warning: 5, error: 3, total: 108 },
supavisor_logs: { ok: 0, warning: 0, error: 0, total: 0 },
function_logs: { ok: 15, warning: 0, error: 0, total: 15 },
etl_replication_logs: { ok: 0, warning: 0, error: 0, total: 0 },
...overrides,
})
describe('extractServiceRows', () => {
it('maps postgres_logs fields to ok_count/warning_count/error_count for db', () => {
const rows = [makeRow()]
const result = extractServiceRows(rows, 'db')
expect(result).toEqual([
{ timestamp: '2026-05-20T10:00:00', ok_count: 10, warning_count: 1, error_count: 2 },
])
})
it('maps the correct service field for each service key', () => {
const row = makeRow()
expect(extractServiceRows([row], 'auth')[0]).toMatchObject({
ok_count: 5,
error_count: 1,
})
expect(extractServiceRows([row], 'functions')[0]).toMatchObject({
ok_count: 20,
error_count: 0,
})
expect(extractServiceRows([row], 'storage')[0]).toMatchObject({ ok_count: 3 })
expect(extractServiceRows([row], 'postgrest')[0]).toMatchObject({
ok_count: 7,
warning_count: 1,
error_count: 1,
})
})
it('returns an empty array when given no rows', () => {
expect(extractServiceRows([], 'db')).toEqual([])
})
it('preserves the timestamp from each row', () => {
const rows = [
makeRow({ timestamp: '2026-05-20T09:00:00' }),
makeRow({ timestamp: '2026-05-20T10:00:00' }),
]
const result = extractServiceRows(rows, 'db')
expect(result.map((r) => r.timestamp)).toEqual(['2026-05-20T09:00:00', '2026-05-20T10:00:00'])
})
it('handles multiple rows and maps each independently', () => {
const rows = [
makeRow({ postgres_logs: { ok: 100, warning: 0, error: 0, total: 100 } }),
makeRow({ postgres_logs: { ok: 50, warning: 5, error: 10, total: 65 } }),
]
const result = extractServiceRows(rows, 'db')
expect(result[0]).toMatchObject({ ok_count: 100, warning_count: 0, error_count: 0 })
expect(result[1]).toMatchObject({ ok_count: 50, warning_count: 5, error_count: 10 })
})
it('defaults to 0 when the service field is missing from the row', () => {
const row = makeRow()
// Simulate a backend response that omits a service field
const partial = { ...row } as Partial<ServiceHealthResultRow> & { timestamp: string }
delete (partial as any).realtime_logs
const result = extractServiceRows([partial as ServiceHealthResultRow], 'realtime')
expect(result[0]).toEqual({
timestamp: row.timestamp,
ok_count: 0,
warning_count: 0,
error_count: 0,
})
})
})