import { type Table as TTable } from '@tanstack/react-table' import { createLoader } from 'nuqs' import { cn } from 'ui' import { EDGE_FUNCTION_LOG_TYPES, LOG_TYPES, LOG_TYPES_LABELS, SEARCH_PARAMS_PARSER, } from './UnifiedLogs.constants' import { parseLogsFilterUrlParams } from './UnifiedLogs.filters' import { ColumnSchema, FacetMetadataSchema } from './UnifiedLogs.schema' import type { QuerySearchParamsType, SearchParamsType, UnifiedLogsScope } from './UnifiedLogs.types' import { LEVELS } from '@/components/ui/DataTable/DataTable.constants' import { Option } from '@/components/ui/DataTable/DataTable.types' export type UnifiedLogType = keyof typeof LOG_TYPES_LABELS export function getComputeLogsAvailability({ isPlatform, flagsLoaded, computeEnabled, }: { isPlatform: boolean flagsLoaded?: boolean computeEnabled: boolean }) { const flagsReady = flagsLoaded === true return { canQueryCompute: isPlatform && flagsReady && computeEnabled, preserveComputeFilter: isPlatform && (!flagsReady || computeEnabled), readyToSyncFilters: !isPlatform || flagsReady, } } /** * Resolves which log types a view may filter on and query. A scoped view only offers its own * log types; the project-wide view hides edge function runtime rows, which it surfaces inside * each invocation's detail panel instead. */ export function getLogTypeVisibility( visibility: Partial>, scope?: UnifiedLogsScope ): Partial> { if (!scope) return { ...visibility, 'edge function runtime': false } const scopeLogTypes: readonly UnifiedLogType[] = EDGE_FUNCTION_LOG_TYPES return Object.fromEntries(LOG_TYPES.map((logType) => [logType, scopeLogTypes.includes(logType)])) } /** Reads unified logs URL params, applying the same defaults as the Logs page. */ export const loadUnifiedLogsSearchParams = createLoader(SEARCH_PARAMS_PARSER) /** * Turns URL search state into the search every unified logs query takes. Drops view-only params * (`id`, `live`) and empty values so they don't churn query keys, and attaches the scope. */ export function toQuerySearchParams( search: SearchParamsType, scope?: UnifiedLogsScope ): QuerySearchParamsType { const parameters = Object.entries(search).reduce( (acc, [key, value]) => { if (!['id', 'live'].includes(key) && value !== null && value !== undefined) { acc[key] = value } return acc }, {} as Record ) as QuerySearchParamsType if (scope) parameters.scope = scope return parameters } /** * Flattens infinite query pages into rows, keeping the first of any duplicate ids. Pages can * overlap because the cursor is a timestamp (see unified-logs-infinite-query). */ export function getUniqueLogRows( pages: { data?: T[] }[] | undefined ): T[] { const seenIds = new Set() return (pages ?? []) .flatMap((page) => page.data ?? []) .filter((row) => { if (seenIds.has(row.id)) return false seenIds.add(row.id) return true }) } /** Row styling for the logs list; rows older than the live mode anchor are dimmed. */ export function getLogRowClassName( row: Pick, liveTimestamp?: number ): string { const isPast = row.timestamp <= (liveTimestamp || -1) return cn(getLevelRowClassName(row.level), isPast ? 'opacity-50' : 'opacity-100', 'h-[30px]') } /** Serializes a time range into the unified logs `date` URL param. */ export const formatUnifiedLogsDateParam = (start: string | Date, end: string | Date) => `${new Date(start).valueOf()}-${new Date(end).valueOf()}` export const buildUnifiedLogsUrl = ({ projectRef, logType, user, start, end, }: { projectRef: string logType?: UnifiedLogType /** Pre-applies the cross-cutting "filter by user" (?user=) — an id or email. */ user?: string start?: string | Date end?: string | Date }) => { const params = new URLSearchParams() if (logType) params.append('filter', `log_type:eq:${logType}`) if (user) params.set('user', user) if (start && end) params.set('date', formatUnifiedLogsDateParam(start, end)) return `/project/${projectRef}/logs?${params.toString()}` } export const getFacetedUniqueValues = (facets?: Record) => { return (_table: TTable, columnId: string) => { return new Map(facets?.[columnId]?.rows?.map(({ value, total }) => [value, total]) || []) } } export const getFacetedMinMaxValues = (facets?: Record) => { return (_table: TTable, 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 } type ComputeRawLogData = Pick & { timestamp: string | number } export function getRawLogData(row: T): T | ComputeRawLogData { if (!('log_type' in row) || row.log_type !== 'compute') return row return { id: row.id, timestamp: row.timestamp, event_message: row.event_message, metadata: row.metadata, } } 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] | null | undefined): 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 = { 'edge function': 'Edge Function', 'edge function runtime': 'Edge Function runtime', postgrest: 'PostgREST', postgres: 'Postgres', auth: 'Auth', storage: 'Storage', realtime: 'Realtime', supavisor: 'Supavisor', pgbouncer: 'PgBouncer', multigres: 'Multigres', } 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 } } /** * Parses a Multigres log event_message, which is a stringified JSON object * (e.g. {"time":"...","level":"INFO","msg":"user pool capacity updated",...}). * Extracts the human-readable msg field, falling back to the raw string so * unexpected formats still render. */ export function parseMultigresEventMessage(value: string | undefined): string | undefined { if (!value) return value try { const parsed = JSON.parse(value) if ( parsed && typeof parsed === 'object' && typeof parsed.msg === 'string' && parsed.msg.trim() ) { return parsed.msg } return value } catch (error) { return value } } /** * Returns the display text for a log row's event_message alongside whether it * should be rendered as a capitalized sentence. Keeps the per-service parsing * and its capitalization rule in one place so callers don't re-derive the list. */ export function getEventMessageDisplay( logType: string, value: string | undefined ): { message: string | undefined; capitalize: boolean } { if (logType === 'auth') return { message: parseAuthLogEventMessage(value), capitalize: true } if (logType === 'multigres') return { message: parseMultigresEventMessage(value), capitalize: true } return { message: value, capitalize: false } } export function gateLogTypeOptions( fields: T[], visibility: Partial> ): T[] { const hiddenLogTypes = new Set( Object.entries(visibility) .filter(([, visible]) => !visible) .map(([logType]) => logType) ) if (hiddenLogTypes.size === 0) return fields return fields.map((field) => { if (field.value !== 'log_type' || !field.options) return field return { ...field, options: field.options.filter((option) => !hiddenLogTypes.has(option.value)), } }) } /** * Wraps a raw ILIKE/NOT ILIKE search term in `%...%` for a "contains" match, * unless it already includes a `%` or `_` wildcard — in which case the user * has crafted their own pattern and it's passed through unchanged. */ export function wrapIlikePattern(value: string): string { return value.includes('%') || value.includes('_') ? value : `%${value}%` } export function gateLogTypeFilters( filters: string[] | null | undefined, visibility: Partial> ): string[] | null | undefined { if (!filters) return filters const hiddenLogTypes = new Set( Object.entries(visibility) .filter(([, visible]) => !visible) .map(([logType]) => logType) ) if (hiddenLogTypes.size === 0) return filters return filters.filter((filter) => { const parsed = parseLogsFilterUrlParams([filter])[0] return parsed?.column !== 'log_type' || !hiddenLogTypes.has(parsed.value) }) }