Files
supabase/apps/studio/data/analytics/api-keys-last-used-query.ts
Jordi Enric 4096267623 feat(api-keys): migrate last-used indicator to ClickHouse endpoint (#47458)
## Problem

The "last used" indicator for the legacy `anon` / `service_role` API
keys (Project API keys settings) was disabled because it ran a BigQuery
`edge_logs` query. It is now re-enabled against the ClickHouse-backed
`api_keys.last_used.otel` analytics endpoint.

## Current behavior

- The `anon` / `service_role` "last used" indicator is off (the
BigQuery-backed query was disabled).

## New behavior

- New `useApiKeysLastUsedQuery` hook calls the `api_keys.last_used.otel`
endpoint (timestamp params only, no SQL sent), plus its query key and
the generated platform API type.
- `DisplayApiSettings` reads last-used from this hook instead of posting
BigQuery `edge_logs` SQL. The pure `getLastUsedAPIKeys` shaper is kept
and unit-tested. Still gated by the `showApiKeysLastUsed` flag.
- Removed the disabled secret-keys (`sb_secret_`) BigQuery last-used
path, which has no ClickHouse endpoint to migrate to: drops the dead
`useLastSeen` query, the `APIKeyRow` "Last Used" column, and the unused
`showLastSeen` prop.
- Reworded the delete-confirmation copy to be accurate for both secret
and publishable keys.

## Additional context

- Backed by the platform endpoint in supabase/platform#34892 (merged and
deployed).
- Scope: `anon` / `service_role` legacy keys. Secret/publishable and JWT
signing-key "last used" are follow-ups, pending the endpoint returning
those key types.

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

* **Improvements**
* Updated API key settings to show “last used” activity for the past 24
hours using a dedicated data source and time window.
  * Added clearer messaging when recent API key activity fails to load.
  * Removed the “Last Used” column from API key management tables.
* **Bug Fixes**
* Improved mapping so “last used” values correctly match the intended
key and role.
* Updated API key deletion confirmation to explain required backend
changes and resulting unauthorized behavior.
* **Tests**
* Added unit tests to validate “last used” computation and edge-case
filtering.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 14:35:32 +02:00

72 lines
2.2 KiB
TypeScript

import { useQuery } from '@tanstack/react-query'
import { analyticsKeys } from './keys'
import { get, handleError } from '@/data/fetchers'
import { UseCustomQueryOptions } from '@/types'
export type ApiKeysLastUsedVariables = {
projectRef?: string
isoTimestampStart?: string
isoTimestampEnd?: string
}
// One row per (role, signature_prefix). `timestamp` is unix millis of the most
// recent edge-log entry seen for that anon / service_role JWT api key fingerprint.
export type ApiKeyLastUsed = {
timestamp: number
role?: 'anon' | 'service_role' | string
signature_prefix?: string
}
export async function getApiKeysLastUsed(
{ projectRef, isoTimestampStart, isoTimestampEnd }: ApiKeysLastUsedVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
const { data, error } = await get(
'/platform/projects/{ref}/analytics/endpoints/api_keys.last_used.otel',
{
params: {
path: { ref: projectRef },
query: {
iso_timestamp_start: isoTimestampStart,
iso_timestamp_end: isoTimestampEnd,
},
},
signal,
}
)
if (error) handleError(error)
const response = data as { error?: string | object | null; result?: unknown[] }
if (response?.error) {
throw new Error(
typeof response.error === 'string' ? response.error : 'Failed to fetch last-used API keys'
)
}
return (response?.result ?? []) as ApiKeyLastUsed[]
}
export type ApiKeysLastUsedData = Awaited<ReturnType<typeof getApiKeysLastUsed>>
export type ApiKeysLastUsedError = unknown
export const useApiKeysLastUsedQuery = <TData = ApiKeysLastUsedData>(
{ projectRef, isoTimestampStart, isoTimestampEnd }: ApiKeysLastUsedVariables,
{
enabled = true,
...options
}: UseCustomQueryOptions<ApiKeysLastUsedData, ApiKeysLastUsedError, TData> = {}
) =>
useQuery<ApiKeysLastUsedData, ApiKeysLastUsedError, TData>({
queryKey: analyticsKeys.apiKeysLastUsed(projectRef, { isoTimestampStart, isoTimestampEnd }),
queryFn: ({ signal }) =>
getApiKeysLastUsed({ projectRef, isoTimestampStart, isoTimestampEnd }, signal),
enabled: enabled && typeof projectRef !== 'undefined',
...options,
})