Files
supabase/apps/studio/components/interfaces/UnifiedLogs/ServiceFlow/config/serviceFlowFields.ts
Jordi Enric 37fcfce07c feat(logs): show query and details in unified PG log dashboards DEBUG-138 (#47026)
## Problem

The unified log dashboards' Postgres detail panel hid the `query` and
`detail` fields. They were present in the raw log message but never
surfaced in the structured view, making them harder to use when
debugging.

## Fix

- Select `pgl_parsed.query` and `pgl_parsed.detail` in the Postgres
service flow query.
- Add `Query` and `Details` field configs to the Postgres primary
fields, both with `wrap: true` so long values display in full instead of
truncating.

The parsed Postgres field is `detail` (singular); it is labeled
"Details" in the UI.

## How to test

- Open Studio and navigate to the unified logs dashboard for a project.
- Filter to Postgres logs and select a log row to open the detail panel.
- Confirm the Postgres section now shows `Query` and `Details` rows
below `User`.
- Expected result: rows render the parsed query and detail text,
wrapping for long values, and show an em dash when empty.

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

* **New Features**
* PostgreSQL service flow logs now show additional always-visible
**Query** and **Details** fields, bringing parsed database query content
and expanded information directly into the log view.
* **Tests**
* Updated log inspection coverage to ensure the new parsed fields are
correctly surfaced in the flattened inspection output.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 08:45:11 +02:00

753 lines
22 KiB
TypeScript

import { BlockFieldConfig } from '../types'
import { getStorageMetadata } from '../utils/storageUtils'
import { formatBytes, tryParseJson } from '@/lib/helpers'
// Helper functions that avoid duplication with existing storage utilities
const getFileName = (path: string): string => {
if (!path) return ''
// Remove query parameters and hash
const cleanPath = path.split('?')[0].split('#')[0]
// Get the last part after the last slash
const segments = cleanPath.split('/')
return segments[segments.length - 1] || ''
}
const formatStorageDate = (dateString: string): string => {
try {
return new Date(dateString).toLocaleString()
} catch {
return dateString
}
}
// =============================================================================
// NETWORK FIELDS
// =============================================================================
// Primary Network Fields (Always Visible) - FILTERABLE
export const networkPrimaryFields: BlockFieldConfig[] = [
{
id: 'host', // Matches filterFields 'host' (input) - FILTERABLE
label: 'Host',
getValue: (data, enrichedData) =>
enrichedData?.request_host || enrichedData?.host || data?.host,
},
{
id: 'method', // Matches filterFields 'method' (checkbox) - FILTERABLE
label: 'Method',
getValue: (data, enrichedData) =>
enrichedData?.request_method || enrichedData?.method || data?.method,
},
{
id: 'pathname', // Matches filterFields 'pathname' (input) - FILTERABLE
label: 'Path',
getValue: (data, enrichedData) =>
enrichedData?.request_path || enrichedData?.pathname || data?.pathname,
},
{
id: 'user_agent',
label: 'Client',
getValue: (_data, enrichedData) => {
const userAgent = enrichedData?.headers_user_agent
if (!userAgent) return null
// TODO: Parse user agent for nice display with icons
return userAgent.length > 50 ? userAgent.substring(0, 50) + '...' : userAgent
},
requiresEnrichedData: true,
},
]
// Primary API Key Field (Always Visible) - Shows API key type only
export const apiKeyPrimaryField: BlockFieldConfig = {
id: 'api_key_role',
label: 'API Key',
getValue: (_data, enrichedData) => {
const prefix = enrichedData?.api_key_prefix
const jwtRole = enrichedData?.jwt_key_role
const authRole = enrichedData?.authorization_role
if (prefix) {
// Extract key type from prefix
if (prefix.includes('publishable')) return 'publishable'
else if (prefix.includes('secret')) return 'secret'
return 'unknown'
}
// Fallback to JWT apikey postgres role if no API key prefix
if (jwtRole) {
return jwtRole
}
// Fallback to authorization postgres role if no apikey JWT
if (authRole) {
return authRole
}
return null
},
requiresEnrichedData: true,
}
// Additional API Key Fields (Collapsible)
export const apiKeyAdditionalFields: BlockFieldConfig[] = [
{
id: 'api_key_prefix_full',
label: 'API Key Prefix',
getValue: (_data, enrichedData) => {
const prefix = enrichedData?.api_key_prefix
return prefix || null
},
requiresEnrichedData: true,
},
{
id: 'postgres_role',
label: 'Postgres Role',
getValue: (_data, enrichedData) => enrichedData?.jwt_key_role,
requiresEnrichedData: true,
},
{
id: 'api_key_error',
label: 'API Key Error',
getValue: (_data, enrichedData) => enrichedData?.api_key_error,
requiresEnrichedData: true,
},
{
id: 'api_key_hash',
label: 'API Key Hash',
getValue: (_data, enrichedData) => {
const hash = enrichedData?.api_key_hash
return hash ? `${hash.substring(0, 12)}...` : null
},
requiresEnrichedData: true,
},
{
id: 'authorization_role',
label: 'Postgres Role',
getValue: (_data, enrichedData) => enrichedData?.authorization_role,
requiresEnrichedData: true,
},
]
// Primary User Field (Always Visible)
export const userPrimaryField: BlockFieldConfig = {
id: 'user_id',
label: 'User',
getValue: (_data, enrichedData) => {
const userId = enrichedData?.user_id
return userId ? `${userId.substring(0, 8)}...` : null
},
requiresEnrichedData: true,
}
// Additional User Fields (Collapsible)
export const userAdditionalFields: BlockFieldConfig[] = [
{
id: 'auth_user', // Matches filterFields 'auth_user' (input) - FILTERABLE
label: 'Auth User',
getValue: (data, enrichedData) => enrichedData?.auth_user || data?.auth_user,
},
{
id: 'user_email',
label: 'User Email',
getValue: (_data, enrichedData) => enrichedData?.user_email,
requiresEnrichedData: true,
},
]
// Primary Location Field (Always Visible)
export const locationPrimaryField: BlockFieldConfig = {
id: 'client_country',
label: 'Location',
getValue: (_data, enrichedData) => {
const country = enrichedData?.client_country || enrichedData?.cf_country
const city = enrichedData?.client_city
if (country && city) return `${city}, ${country}`
if (country) return country
if (city) return city
return null
},
requiresEnrichedData: true,
}
// Additional Location Fields (Collapsible) - includes IP addresses
export const locationAdditionalFields: BlockFieldConfig[] = [
{
id: 'client_continent',
label: 'Continent',
getValue: (_data, enrichedData) => enrichedData?.client_continent,
requiresEnrichedData: true,
},
{
id: 'client_region',
label: 'Region',
getValue: (_data, enrichedData) => enrichedData?.client_region,
requiresEnrichedData: true,
},
{
id: 'client_timezone',
label: 'Timezone',
getValue: (_data, enrichedData) => enrichedData?.client_timezone,
requiresEnrichedData: true,
},
{
id: 'x_real_ip',
label: 'Real IP',
getValue: (_data, enrichedData) => enrichedData?.headers_x_real_ip,
requiresEnrichedData: true,
},
{
id: 'client_ip',
label: 'Client IP',
getValue: (_data, enrichedData) => enrichedData?.client_ip,
requiresEnrichedData: true,
},
]
// Authorization Fields (Collapsible) - JWT authorization details
export const authorizationFields: BlockFieldConfig[] = [
{
id: 'jwt_auth_key_id',
label: 'Key ID',
getValue: (_data, enrichedData) =>
enrichedData?.jwt_auth_key_id || enrichedData?.jwt_apikey_key_id,
requiresEnrichedData: true,
},
{
id: 'jwt_auth_session_id',
label: 'Session ID',
getValue: (_data, enrichedData) =>
enrichedData?.jwt_auth_session_id || enrichedData?.jwt_apikey_session_id,
requiresEnrichedData: true,
},
{
id: 'jwt_auth_subject',
label: 'Subject',
getValue: (_data, enrichedData) =>
enrichedData?.jwt_auth_subject || enrichedData?.jwt_apikey_subject,
requiresEnrichedData: true,
},
{
id: 'jwt_auth_issuer',
label: 'Issuer',
getValue: (_data, enrichedData) =>
enrichedData?.jwt_auth_issuer || enrichedData?.jwt_apikey_issuer,
requiresEnrichedData: true,
},
{
id: 'jwt_auth_algorithm',
label: 'Algorithm',
getValue: (_data, enrichedData) =>
enrichedData?.jwt_auth_algorithm || enrichedData?.jwt_apikey_algorithm,
requiresEnrichedData: true,
},
{
id: 'jwt_auth_expires_at',
label: 'Expires At',
getValue: (_data, enrichedData) => {
const expiresAt = enrichedData?.jwt_auth_expires_at || enrichedData?.jwt_apikey_expires_at
if (!expiresAt) return null
try {
return new Date(expiresAt * 1000).toLocaleString()
} catch {
return expiresAt
}
},
requiresEnrichedData: true,
},
]
// Tech Details Fields (Collapsible)
export const techDetailsFields: BlockFieldConfig[] = [
{
id: 'network_protocol',
label: 'Protocol',
getValue: (_data, enrichedData) => enrichedData?.network_protocol,
requiresEnrichedData: true,
},
{
id: 'cf_datacenter',
label: 'Datacenter',
getValue: (_data, enrichedData) =>
enrichedData?.network_datacenter || enrichedData?.cf_datacenter,
requiresEnrichedData: true,
},
{
id: 'cache_status',
label: 'Cache Status',
getValue: (_data, enrichedData) => enrichedData?.response_cache_status,
requiresEnrichedData: true,
},
{
id: 'cf_ray',
label: 'CF-Ray',
getValue: (_data, enrichedData) => enrichedData?.cf_ray,
requiresEnrichedData: true,
},
{
id: 'x_client_info',
label: 'SDK',
getValue: (_data, enrichedData) => enrichedData?.headers_x_client_info,
requiresEnrichedData: true,
},
{
id: 'x_forwarded_proto',
label: 'Forwarded Proto',
getValue: (_data, enrichedData) => enrichedData?.headers_x_forwarded_proto,
requiresEnrichedData: true,
},
]
// =============================================================================
// POSTGREST FIELDS
// =============================================================================
// Primary PostgREST Fields (Always Visible) - FILTERABLE
export const postgrestPrimaryFields: BlockFieldConfig[] = [
{
id: 'status', // Matches filterFields 'status' (checkbox) - FILTERABLE
label: 'Status',
getValue: (data, enrichedData) => enrichedData?.status || data?.status,
},
{
id: 'postgres_role',
label: 'Postgres Role',
getValue: (_data, enrichedData) => enrichedData?.api_role,
requiresEnrichedData: true,
},
{
id: 'response_time',
label: 'Response Time',
getValue: (data, enrichedData) => {
const time = enrichedData?.response_origin_time || data?.response_time_ms
return time ? `${time}ms` : null
},
requiresEnrichedData: true,
},
]
// PostgREST Response Details (Collapsible)
export const postgrestResponseFields: BlockFieldConfig[] = [
{
id: 'query_params',
label: 'Query',
getValue: (_data, enrichedData) => enrichedData?.request_search,
requiresEnrichedData: true,
},
{
id: 'content_type',
label: 'Content Type',
getValue: (_data, enrichedData) => enrichedData?.response_content_type,
requiresEnrichedData: true,
},
{
id: 'message',
label: 'Message',
getValue: (_data, enrichedData) => enrichedData?.message,
requiresEnrichedData: true,
},
]
// =============================================================================
// AUTH FIELDS
// =============================================================================
// Primary GoTrue/Auth Fields (Always Visible)
export const authPrimaryFields: BlockFieldConfig[] = [
{
id: 'log_id',
label: 'Log ID',
getValue: (data, enrichedData) => {
const logId = data?.id || enrichedData?.id
return logId ?? null
},
},
{
id: 'status',
label: 'Status',
getValue: (data) => {
return data.status
},
},
{
id: 'auth_path',
label: 'Auth Path',
getValue: (data, enrichedData) => {
return enrichedData?.path || enrichedData?.request_path || data?.path || data?.pathname
},
},
{
id: 'referer',
label: 'Referer',
getValue: (data, enrichedData) => {
const eventMessage = tryParseJson(data.event_message)
return eventMessage?.referer || enrichedData?.headers_referer || null
},
},
]
// =============================================================================
// EDGE FUNCTION FIELDS
// =============================================================================
// Primary Edge Function Fields (Always Visible)
export const edgeFunctionPrimaryFields: BlockFieldConfig[] = [
{
id: 'status',
label: 'Status',
getValue: (data, enrichedData) => enrichedData?.status || data?.status,
},
{
id: 'function_path',
label: 'Function Path',
getValue: (data, enrichedData) => {
return (
enrichedData?.path || enrichedData?.request_path || data?.path || enrichedData?.request_url
)
},
requiresEnrichedData: false,
},
{
id: 'execution_time',
label: 'Execution Time',
getValue: (data, enrichedData) => {
const time = enrichedData?.execution_time_ms || data?.execution_time_ms
return time ? `${time}ms` : null
},
requiresEnrichedData: false,
},
{
id: 'execution_id',
label: 'Execution ID',
getValue: (data, enrichedData) => {
const execId = enrichedData?.execution_id || data?.execution_id
return execId ? `${execId.substring(0, 8)}...` : null
},
requiresEnrichedData: false,
},
]
// Edge Function Details (Collapsible)
export const edgeFunctionDetailsFields: BlockFieldConfig[] = [
{
id: 'function_id',
label: 'Function ID',
getValue: (data, enrichedData) => {
const funcId = enrichedData?.function_id || data?.function_id
return funcId ? `${funcId.substring(0, 8)}...` : null
},
requiresEnrichedData: false,
},
{
id: 'execution_region',
label: 'Execution Region',
getValue: (data, enrichedData) => {
return enrichedData?.execution_region || data?.execution_region || null
},
requiresEnrichedData: false,
},
{
id: 'function_log_count',
label: 'Function Logs',
getValue: (data, enrichedData) => {
const count = enrichedData?.function_log_count || data?.function_log_count || data?.log_count
return count ? `${count} logs` : 'No logs'
},
requiresEnrichedData: false,
},
{
id: 'method',
label: 'Method',
getValue: (data, enrichedData) => enrichedData?.method || data?.method,
requiresEnrichedData: false,
},
{
id: 'user_agent',
label: 'User Agent',
getValue: (data, enrichedData) => {
const ua = enrichedData?.headers_user_agent || data?.headers_user_agent
return ua ? (ua.length > 30 ? `${ua.substring(0, 30)}...` : ua) : null
},
requiresEnrichedData: false,
},
]
// =============================================================================
// STORAGE FIELDS
// =============================================================================
// Primary Storage Fields (Always Visible)
export const storagePrimaryFields: BlockFieldConfig[] = [
{
id: 'status',
label: 'Status',
getValue: (data, enrichedData) => enrichedData?.status || data?.status,
},
{
id: 'filename',
label: 'File Name',
getValue: (data, enrichedData) => {
const path = enrichedData?.path || enrichedData?.request_path || data?.path
return path ? getFileName(path) : null
},
requiresEnrichedData: false,
},
{
id: 'content_type_size',
label: 'Type & Size',
getValue: (data, enrichedData) => {
const status = enrichedData?.status || data?.status
const isObjectDeleted = status === 404 || status === '404'
const hasError = status && Number(status) >= 400
// For deleted objects, show status message
if (isObjectDeleted) {
return 'Object deleted'
} else if (hasError) {
return `Error ${status}`
}
// Try to get metadata like PreviewPane does
const metadata = getStorageMetadata(data, enrichedData)
const mimeType = metadata?.mimetype
const size = metadata?.size
// Fallback to headers if metadata not available
const contentType =
mimeType ||
enrichedData?.response_content_type ||
enrichedData?.storage_request_content_type
const contentLength =
size ||
(enrichedData?.storage_content_length
? parseInt(enrichedData.storage_content_length)
: null)
if (contentType && contentLength) {
return `${contentType} - ${formatBytes(contentLength)}`
} else if (contentType) {
return contentType
} else if (contentLength) {
return formatBytes(contentLength)
}
return 'Unknown type'
},
requiresEnrichedData: true,
},
{
id: 'response_time',
label: 'Response Time',
getValue: (data, enrichedData) => {
const time = enrichedData?.response_origin_time || data?.response_time_ms
return time ? `${time}ms` : null
},
requiresEnrichedData: true,
},
]
// Storage Details (Collapsible)
export const storageDetailsFields: BlockFieldConfig[] = [
{
id: 'storage_path',
label: 'Storage Path',
getValue: (data, enrichedData) => {
const path = enrichedData?.path || enrichedData?.request_path || data?.path
// Extract just the object path from the full storage path
const match = path?.match(/\/storage\/v1\/object\/([^\/]+)\/(.+)/)
if (match) {
const [, bucketName, objectPath] = match
return `${bucketName}/${objectPath}`
}
return path
},
requiresEnrichedData: false,
},
{
id: 'last_modified',
label: 'Last Modified',
getValue: (data, enrichedData) => {
const status = enrichedData?.status || data?.status
const isObjectDeleted = status === 404 || status === '404'
const hasError = status && Number(status) >= 400
if (isObjectDeleted) {
return 'Object deleted'
} else if (hasError) {
return 'Unavailable'
}
// Try to get dates from metadata like PreviewPane does
const metadata = getStorageMetadata(data, enrichedData)
const updatedAt = metadata?.updated_at || data?.updated_at || enrichedData?.updated_at
const lastModified = enrichedData?.storage_last_modified || updatedAt
return lastModified ? formatStorageDate(lastModified) : null
},
requiresEnrichedData: true,
},
{
id: 'etag',
label: 'ETag',
getValue: (data, enrichedData) => {
const status = enrichedData?.status || data?.status
const isObjectDeleted = status === 404 || status === '404'
const hasError = status && Number(status) >= 400
if (isObjectDeleted) {
return 'Object deleted'
} else if (hasError) {
return 'Unavailable'
}
const etag = enrichedData?.storage_etag
return etag ? `${etag.substring(0, 12)}...` : null
},
requiresEnrichedData: true,
},
{
id: 'content_disposition',
label: 'Content Disposition',
getValue: (data, enrichedData) => {
const status = enrichedData?.status || data?.status
const isObjectDeleted = status === 404 || status === '404'
const hasError = status && Number(status) >= 400
if (isObjectDeleted) {
return 'Object deleted'
} else if (hasError) {
return 'Unavailable'
}
return enrichedData?.storage_content_disposition || null
},
requiresEnrichedData: true,
},
{
id: 'method',
label: 'Method',
getValue: (data, enrichedData) => enrichedData?.method || data?.method,
requiresEnrichedData: false,
},
{
id: 'user_agent',
label: 'User Agent',
getValue: (data, enrichedData) => {
const ua = enrichedData?.headers_user_agent || data?.headers_user_agent
return ua ? (ua.length > 30 ? `${ua.substring(0, 30)}...` : ua) : null
},
requiresEnrichedData: false,
},
]
// =============================================================================
// POSTGRES FIELDS
// =============================================================================
// Primary Postgres Fields (Always Visible)
export const postgresPrimaryFields: BlockFieldConfig[] = [
{
id: 'event_message',
label: 'Message',
getValue: (data, enrichedData) => enrichedData?.event_message || data?.event_message,
wrap: true,
},
{
id: 'status',
label: 'Status',
getValue: (data, enrichedData) => enrichedData?.status || data?.status,
},
{
id: 'command_tag',
label: 'Command',
getValue: (data, enrichedData) => enrichedData?.command_tag || data?.command_tag,
requiresEnrichedData: true,
},
{
id: 'database_name',
label: 'Database',
getValue: (data, enrichedData) => enrichedData?.database_name || data?.database_name,
requiresEnrichedData: true,
},
{
id: 'database_user',
label: 'User',
getValue: (data, enrichedData) => enrichedData?.database_user || data?.database_user,
requiresEnrichedData: true,
},
{
id: 'query',
label: 'Query',
getValue: (data, enrichedData) => enrichedData?.query || data?.query,
requiresEnrichedData: true,
wrap: true,
},
{
id: 'detail',
label: 'Details',
getValue: (data, enrichedData) => enrichedData?.detail || data?.detail,
requiresEnrichedData: true,
wrap: true,
},
]
// Postgres Details (Collapsible)
export const postgresDetailsFields: BlockFieldConfig[] = [
{
id: 'backend_type',
label: 'Backend Type',
getValue: (data, enrichedData) => enrichedData?.backend_type || data?.backend_type,
requiresEnrichedData: true,
},
{
id: 'connection_from',
label: 'Connection From',
getValue: (data, enrichedData) => enrichedData?.connection_from || data?.connection_from,
requiresEnrichedData: true,
},
{
id: 'session_id',
label: 'Session ID',
getValue: (data, enrichedData) => enrichedData?.session_id || data?.session_id,
requiresEnrichedData: true,
},
{
id: 'process_id',
label: 'Process ID',
getValue: (data, enrichedData) => enrichedData?.process_id || data?.process_id,
requiresEnrichedData: true,
},
{
id: 'query_id',
label: 'Query ID',
getValue: (data, enrichedData) => {
const queryId = enrichedData?.query_id || data?.query_id
return queryId ? String(queryId) : null
},
requiresEnrichedData: true,
},
{
id: 'transaction_id',
label: 'Transaction ID',
getValue: (data, enrichedData) => {
const txId = enrichedData?.transaction_id || data?.transaction_id
return txId ? String(txId) : null
},
requiresEnrichedData: true,
},
{
id: 'virtual_transaction_id',
label: 'Virtual TX ID',
getValue: (data, enrichedData) =>
enrichedData?.virtual_transaction_id || data?.virtual_transaction_id,
requiresEnrichedData: true,
},
{
id: 'session_start_time',
label: 'Session Started',
getValue: (data, enrichedData) => {
const startTime = enrichedData?.session_start_time || data?.session_start_time
return startTime ? new Date(startTime).toLocaleString() : null
},
requiresEnrichedData: true,
},
]