Files
supabase/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.utils.ts
Jordi EnricandClaude Opus 4.8 91d70a8c38 feat(studio): chart bar links open unified logs when enabled (#47502)
## What

When the unified logs preview is enabled, clicking a chart bar that
links to a logs view now opens **unified logs** (scoped to the service
and time bucket) instead of the legacy logs explorer.

Surfaces updated:
- Homepage project usage charts (`ProjectUsageSectionDeltas`,
`ProjectUsageSection`)
- Observability overview service health table (`ObservabilityOverview`)
— also fixes the API Gateway row and passes the time range via the
`date` param unified logs actually reads

Adds a small `buildUnifiedLogsUrl` helper so the deep-link format
(`filter=log_type:eq:<type>` + `date` epoch-ms range) lives in one
place.

When the preview is off, behavior is unchanged (legacy logs explorer).

Resolves O11Y-2133.

🤖 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**
* Added unified logs navigation for project usage and observability
charts.
* Chart bar clicks now open the unified logs view with service-specific
filtering and a computed time window.
* **Bug Fixes**
* Updated observability and usage charts to generate the correct unified
logs URLs (including `log_type` filtering and optional date ranges).
* Preserved legacy log navigation behavior when unified logs are
disabled.
* **Tests**
* Added unit tests covering unified logs URL generation, query
parameters, and date handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

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

159 lines
5.2 KiB
TypeScript

import { type Table as TTable } from '@tanstack/react-table'
import { cn } from 'ui'
import { LOG_TYPES_LABELS } from './UnifiedLogs.constants'
import { FacetMetadataSchema } from './UnifiedLogs.schema'
import { LEVELS } from '@/components/ui/DataTable/DataTable.constants'
export type UnifiedLogType = keyof typeof LOG_TYPES_LABELS
export const buildUnifiedLogsUrl = ({
projectRef,
logType,
start,
end,
}: {
projectRef: string
logType: UnifiedLogType
start?: string | Date
end?: string | Date
}) => {
const params = new URLSearchParams()
params.append('filter', `log_type:eq:${logType}`)
if (start && end) {
params.set('date', `${new Date(start).valueOf()}-${new Date(end).valueOf()}`)
}
return `/project/${projectRef}/logs?${params.toString()}`
}
export const getFacetedUniqueValues = <TData>(facets?: Record<string, FacetMetadataSchema>) => {
return (_table: TTable<TData>, columnId: string) => {
return new Map(facets?.[columnId]?.rows?.map(({ value, total }) => [value, total]) || [])
}
}
export const getFacetedMinMaxValues = <TData>(facets?: Record<string, FacetMetadataSchema>) => {
return (_table: TTable<TData>, columnId: string) => {
const min = facets?.[columnId]?.min
const max = facets?.[columnId]?.max
if (typeof min === 'number' && typeof max === 'number') return [min, max]
if (typeof min === 'number') return [min, min]
if (typeof max === 'number') return [max, max]
return undefined
}
}
/**
* Returns a unified-logs row's timestamp in epoch milliseconds.
*
* The row mapper attaches a pre-parsed `date` (works for both BigQuery
* microsecond timestamps and OTEL ISO strings); fall back to the raw
* `timestamp` value when it's a number (older BQ-style microseconds).
*/
export function getRowTimestampMs(
row: { date?: Date | null; timestamp?: number | string | null } | null | undefined
): number | null {
if (row?.date instanceof Date) return row.date.getTime()
if (typeof row?.timestamp === 'number') return row.timestamp / 1000
return null
}
export const getLevelLabel = (value: (typeof LEVELS)[number]): string => {
switch (value) {
case 'success':
return '2xx'
case 'warning':
return '4xx'
case 'error':
return '5xx'
}
}
// Helper function to determine level from HTTP status code
export const getStatusLevel = (status?: number | string): string => {
if (!status) return 'success'
const statusNum = Number(status)
if (statusNum >= 500) return 'error'
if (statusNum >= 400) return 'warning'
if (statusNum >= 300) return 'info' // 3xx redirects are informational
if (statusNum >= 200) return 'success'
if (statusNum >= 100) return 'info'
return 'success'
}
export function getLevelRowClassName(value: (typeof LEVELS)[number]): string {
switch (value) {
case 'success':
return ''
case 'warning':
return cn(
'bg-warning/5 hover:bg-warning/10',
'data-[state=selected]:bg-warning/20 focus-visible:bg-warning/10',
'dark:bg-warning/10 dark:hover:bg-warning/20 dark:data-[state=selected]:bg-warning/30 dark:focus-visible:bg-warning/20'
)
case 'error':
return cn(
'bg-destructive/5 hover:bg-destructive/10',
'data-[state=selected]:bg-destructive/20 focus-visible:bg-destructive/10',
'dark:bg-error/10 dark:hover:bg-destructive/20 dark:data-[state=selected]:bg-destructive/30 dark:focus-visible:bg-destructive/20'
)
default:
return ''
}
}
/**
* Formats service type strings for display purposes
* Handles special cases like "edge function" -> "Edge Function"
* and applies proper capitalization to other service types
*/
export function formatServiceTypeForDisplay(serviceType: string): string {
if (!serviceType) return ''
// Handle special cases
const specialCases: Record<string, string> = {
'edge function': 'Edge Function',
postgrest: 'PostgREST',
postgres: 'Postgres',
auth: 'Auth',
storage: 'Storage',
realtime: 'Realtime',
supavisor: 'Supavisor',
pgbouncer: 'PgBouncer',
}
return specialCases[serviceType.toLowerCase()] || serviceType
}
/**
* Parses an auth log event_message that may be a stringified JSON object.
* Auth log entries store metadata as JSON in event_message (e.g. {"msg":"...","level":"info"}).
* Extracts the human-readable msg field, falling back to error, then the raw string.
* The fallback ensures self-hosted versions with different formats still render correctly.
*/
export function parseAuthLogEventMessage(value: string | undefined): string | undefined {
if (!value) return value
try {
const parsed = JSON.parse(value)
if (parsed && typeof parsed === 'object') {
const err = parsed.error || parsed.error_code
if (typeof err === 'string' && err.trim()) {
return !/^\d{3}:/.test(err) ? err.replaceAll('_', ' ') : err
}
const msg = parsed.msg
if (typeof msg === 'string' && msg.trim()) {
const action = parsed.action ?? parsed.auth_event?.action
const authEvent = typeof action === 'string' ? action.replaceAll('_', ' ') : undefined
return `${authEvent ? `${authEvent}: ` : ''}${msg}`
}
}
return value
} catch (error) {
return value
}
}