Files
supabase/apps/studio/components/interfaces/UnifiedLogs/ServiceFlowPanel.tsx
David Whittington 591b621567 fix(studio): tighten time bounds on single-log inspection query (#47978)
## 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 -->
2026-07-16 11:25:26 +02:00

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>
</>
)
}