mirror of
https://github.com/supabase/supabase.git
synced 2026-09-06 09:59:03 +08:00
## Summary - The unified log inspection point-lookup (`getUnifiedLogInspection` in `apps/studio/data/logs/unified-log-inspection-query.ts`, used by `ServiceFlowPanel` when a user selects a row to view its detail panel) previously reused the whole selected search date range for its `iso_timestamp_start`/`iso_timestamp_end` bounds, even though it looks up exactly one row by `id`. With a wide search range selected (days/weeks), this scans far more of the ClickHouse-backed `logs` table than necessary. - Since the selected row's own timestamp is already known client-side, the query now bounds itself to a ±1 hour window around that timestamp instead, falling back to the previous search-range behavior when no timestamp is available. - No SQL text changes for the time bound — the `iso_timestamp_start`/`iso_timestamp_end` params are the existing mechanism by which every other query in this file (and sibling logs queries) bounds time server-side, so this follows that same convention rather than adding a redundant inline `WHERE timestamp` clause. - Also added an explicit `AND source = '...'` filter to the OTEL point-lookup SQL. The logs table's primary key is `(project, source, timestamp)`, so filtering on `source` narrows the sorted range before the timestamp bound even applies — the service flow `type` already maps 1:1 to a `source` value, so no new data was needed at the call site. ## Test plan - [ ] Typecheck (couldn't run locally in this environment — no `node_modules` installed) - [ ] Manually verify in Studio: open Logs Explorer with a wide time range (e.g. 7 days), select a log row, confirm the detail/service-flow panel still loads the correct enriched data - [ ] Confirm behavior is unchanged when `logTimestampMs` is unavailable (falls back to search range) 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved unified log inspection accuracy by narrowing the inspection window to ±1 minute around the selected log event when a timestamp is available. * Updated service-flow and OTEL inspection lookups to use the selected log entry’s timestamp for tighter, more relevant results. * Preserved the prior broader time-range behavior when a timestamp isn’t available. * **Refactor** * Centralized log type → source mapping and generated the corresponding query filters from that shared mapping for consistency. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
270 lines
9.6 KiB
TypeScript
270 lines
9.6 KiB
TypeScript
import { useParams } from 'common'
|
|
import { Check, Clock, Copy } from 'lucide-react'
|
|
import { useEffect, useRef, useState } from 'react'
|
|
import {
|
|
Button,
|
|
copyToClipboard,
|
|
ResizableHandle,
|
|
ResizablePanel,
|
|
Skeleton,
|
|
Tabs,
|
|
TabsContent,
|
|
TabsList,
|
|
TabsTrigger,
|
|
} from 'ui'
|
|
import { CodeBlock } from 'ui-patterns/CodeBlock'
|
|
|
|
import { PostgresFlowDetail } from './ServiceFlow/components/PostgresFlowDetail'
|
|
import {
|
|
MemoizedEdgeFunctionBlock,
|
|
MemoizedGoTrueBlock,
|
|
MemoizedNetworkBlock,
|
|
MemoizedPostgresBlock,
|
|
MemoizedPostgRESTBlock,
|
|
MemoizedStorageBlock,
|
|
} from './ServiceFlow/components/ServiceBlocks'
|
|
import { ServiceFlowPanelControls } from './ServiceFlow/components/ServiceFlowPanelControls'
|
|
import { DetailSectionHeader } from './ServiceFlow/components/shared/DetailSection'
|
|
import { ColumnSchema } from './UnifiedLogs.schema'
|
|
import { QuerySearchParamsType } from './UnifiedLogs.types'
|
|
import { getRowTimestampMs } from './UnifiedLogs.utils'
|
|
import { useDataTable } from '@/components/ui/DataTable/providers/DataTableProvider'
|
|
import {
|
|
SERVICE_FLOW_TYPES,
|
|
ServiceFlowType,
|
|
useUnifiedLogInspectionQuery,
|
|
} from '@/data/logs/unified-log-inspection-query'
|
|
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
|
|
|
interface ServiceFlowPanelProps {
|
|
dock: 'bottom' | 'right'
|
|
setDock: (value: 'bottom' | 'right') => void
|
|
selectedRow?: ColumnSchema
|
|
selectedRowKey: string
|
|
searchParameters: QuerySearchParamsType
|
|
}
|
|
|
|
export function ServiceFlowPanel({
|
|
dock,
|
|
setDock,
|
|
selectedRow,
|
|
selectedRowKey,
|
|
searchParameters,
|
|
}: ServiceFlowPanelProps) {
|
|
const { table, filterFields } = useDataTable()
|
|
const { ref: projectRef } = useParams()
|
|
const [activeTab, setActiveTab] = useState('overview')
|
|
const [jsonCopied, setJsonCopied] = useState(false)
|
|
const overviewScrollRef = useRef<HTMLDivElement>(null)
|
|
const jsonScrollRef = useRef<HTMLDivElement>(null)
|
|
|
|
useEffect(() => {
|
|
overviewScrollRef.current?.scrollTo({ top: 0 })
|
|
jsonScrollRef.current?.scrollTo({ top: 0 })
|
|
}, [selectedRowKey])
|
|
|
|
const logType = selectedRow?.log_type
|
|
const normalizedLogType = logType === 'edge function' ? 'edge-function' : logType
|
|
const serviceFlowType: ServiceFlowType | undefined = SERVICE_FLOW_TYPES.includes(
|
|
normalizedLogType as ServiceFlowType
|
|
)
|
|
? (normalizedLogType as ServiceFlowType)
|
|
: undefined
|
|
const shouldShowServiceFlow = !!serviceFlowType
|
|
|
|
useEffect(() => {
|
|
if (!shouldShowServiceFlow && activeTab === 'overview') {
|
|
setActiveTab('raw-json')
|
|
}
|
|
}, [shouldShowServiceFlow, activeTab])
|
|
|
|
const { logsMetadata } = useIsFeatureEnabled(['logs:metadata'])
|
|
|
|
const timestampMs = getRowTimestampMs(selectedRow)
|
|
|
|
// Query the logs API directly
|
|
const {
|
|
data: serviceFlowData,
|
|
isPending: isLoading,
|
|
error,
|
|
} = useUnifiedLogInspectionQuery(
|
|
{
|
|
projectRef: projectRef,
|
|
logId: selectedRow?.id,
|
|
type: serviceFlowType,
|
|
search: searchParameters,
|
|
logTimestampMs: timestampMs,
|
|
},
|
|
{ enabled: Boolean(selectedRow?.id) && Boolean(serviceFlowType) }
|
|
)
|
|
|
|
if (!selectedRowKey || !selectedRow) return null
|
|
|
|
const formattedTime = timestampMs ? new Date(timestampMs).toLocaleString() : null
|
|
|
|
// Prepare JSON data for Raw JSON tab
|
|
const jsonData =
|
|
shouldShowServiceFlow && serviceFlowData?.result?.[0] ? serviceFlowData.result[0] : selectedRow
|
|
|
|
const formattedJsonData =
|
|
!logsMetadata && 'raw_log_data' in jsonData && 'metadata' in jsonData.raw_log_data
|
|
? {
|
|
...jsonData,
|
|
raw_log_data: { ...jsonData.raw_log_data, metadata: undefined },
|
|
}
|
|
: jsonData
|
|
|
|
return (
|
|
<>
|
|
<ResizableHandle withHandle />
|
|
<ResizablePanel
|
|
id="log-sidepanel"
|
|
defaultSize={400}
|
|
minSize={dock === 'bottom' ? 300 : 400}
|
|
className="bg-dash-sidebar"
|
|
>
|
|
<div className="flex h-full flex-col overflow-hidden">
|
|
<Tabs
|
|
defaultValue={shouldShowServiceFlow ? 'overview' : 'raw-json'}
|
|
value={activeTab}
|
|
onValueChange={setActiveTab}
|
|
className="flex h-full w-full flex-col"
|
|
>
|
|
<div className="flex items-center justify-between px-4 border-b border-border">
|
|
<TabsList className="flex h-auto gap-x-4 rounded-none border-none!">
|
|
{shouldShowServiceFlow && (
|
|
<TabsTrigger
|
|
value="overview"
|
|
className="border-b py-3 font-mono text-xs uppercase"
|
|
>
|
|
Overview
|
|
</TabsTrigger>
|
|
)}
|
|
<TabsTrigger value="raw-json" className="border-b py-3 font-mono text-xs uppercase">
|
|
Raw JSON
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<ServiceFlowPanelControls dock={dock} setDock={setDock} />
|
|
</div>
|
|
|
|
{shouldShowServiceFlow && (
|
|
<TabsContent
|
|
ref={overviewScrollRef}
|
|
value="overview"
|
|
className="mt-0 grow overflow-auto py-2"
|
|
>
|
|
{error ? (
|
|
<div className="py-8 text-center text-destructive">Error: {error.toString()}</div>
|
|
) : serviceFlowType === 'postgres' ? (
|
|
<PostgresFlowDetail
|
|
data={selectedRow}
|
|
enrichedData={serviceFlowData?.result?.[0]}
|
|
isLoading={isLoading}
|
|
filterFields={filterFields}
|
|
table={table}
|
|
/>
|
|
) : (
|
|
<div>
|
|
<DetailSectionHeader
|
|
title="Request started"
|
|
icon={Clock}
|
|
summary={formattedTime ?? undefined}
|
|
/>
|
|
<MemoizedNetworkBlock
|
|
data={selectedRow}
|
|
enrichedData={serviceFlowData?.result?.[0]}
|
|
isLoading={isLoading}
|
|
filterFields={filterFields}
|
|
table={table}
|
|
/>
|
|
{serviceFlowType === 'auth' ? (
|
|
<MemoizedGoTrueBlock
|
|
data={selectedRow}
|
|
enrichedData={serviceFlowData?.result?.[0]}
|
|
isLoading={isLoading}
|
|
filterFields={filterFields}
|
|
table={table}
|
|
/>
|
|
) : serviceFlowType === 'edge-function' ? (
|
|
<MemoizedEdgeFunctionBlock
|
|
data={selectedRow}
|
|
enrichedData={serviceFlowData?.result?.[0]}
|
|
isLoading={isLoading}
|
|
filterFields={filterFields}
|
|
table={table}
|
|
/>
|
|
) : serviceFlowType === 'storage' ? (
|
|
<MemoizedStorageBlock
|
|
data={selectedRow}
|
|
enrichedData={serviceFlowData?.result?.[0]}
|
|
isLoading={isLoading}
|
|
filterFields={filterFields}
|
|
table={table}
|
|
/>
|
|
) : (
|
|
<>
|
|
<MemoizedPostgRESTBlock
|
|
data={selectedRow}
|
|
enrichedData={serviceFlowData?.result?.[0]}
|
|
isLoading={isLoading}
|
|
filterFields={filterFields}
|
|
table={table}
|
|
/>
|
|
|
|
<MemoizedPostgresBlock
|
|
data={selectedRow}
|
|
enrichedData={serviceFlowData?.result?.[0]}
|
|
isLoading={isLoading}
|
|
filterFields={filterFields}
|
|
table={table}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</TabsContent>
|
|
)}
|
|
|
|
<TabsContent
|
|
ref={jsonScrollRef}
|
|
value="raw-json"
|
|
className="mt-0 grow overflow-auto bg-surface-100/50"
|
|
>
|
|
{isLoading && shouldShowServiceFlow && (
|
|
<div className="flex items-center gap-3 border-b border-border bg-surface-100 p-3 text-foreground-light">
|
|
<Skeleton className="h-4 w-4 animate-pulse rounded-full" />
|
|
<span className="text-sm">Enriching log...</span>
|
|
</div>
|
|
)}
|
|
<div className="sticky top-2 z-10 flex justify-end px-2 -mb-9 pointer-events-none">
|
|
<Button
|
|
size="tiny"
|
|
variant="default"
|
|
className="pointer-events-auto px-1.5"
|
|
icon={jsonCopied ? <Check size={12} /> : <Copy size={12} />}
|
|
onClick={() => {
|
|
copyToClipboard(JSON.stringify(formattedJsonData, null, 2))
|
|
setJsonCopied(true)
|
|
setTimeout(() => setJsonCopied(false), 1000)
|
|
}}
|
|
>
|
|
{jsonCopied ? 'Copied' : ''}
|
|
</Button>
|
|
</div>
|
|
<CodeBlock
|
|
language="json"
|
|
hideCopy
|
|
wrapperClassName="!overflow-visible bg-surface-100/50 [&_pre]:!bg-surface-100/50"
|
|
className="rounded-none border-none [&_code]:!leading-tight [&_pre]:!leading-tight"
|
|
>
|
|
{JSON.stringify(formattedJsonData, null, 2)}
|
|
</CodeBlock>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
</ResizablePanel>
|
|
</>
|
|
)
|
|
}
|