fix(studio): make worker logs actually load in the worker tab

Worker rows only exist on the OTEL logs endpoint; the BigQuery query
builder has no worker sources. The tab was routed by the otelUnifiedLogs
flag, so with the flag off (or not yet loaded) it queried an endpoint that
can never return a worker row. Add a useOtel override to the unified logs
query hooks and download button, and force it on for the worker tab.

Default the tab's time range to the last 24 hours (as master's Activity
tab did) instead of the unified logs' last hour, so one-off lifecycle
events such as deploys and builds are visible when the tab opens. The
seeded range is read by the first fetch and synced into the URL so the
picker, the query and the "Open in Logs" link agree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R5ev3QGKtXVansc1cz5uzR
This commit is contained in:
Claude committed 2026-09-09 16:32:34 +00:00
1 parent bb09c0047b
commit 2a60bd866f
8 files changed
+108 -34

No files matched your search

@@ -4,10 +4,12 @@ import { useQueryState } from 'nuqs'
import { useEffect, useMemo, useRef } from 'react'
import { SEARCH_PARAMS_PARSER } from './UnifiedLogs.constants'
import type { QuerySearchParamsType } from './UnifiedLogs.types'
import { useUnifiedLogsChartQuery } from '@/data/logs/unified-logs-chart-query'
import { useUnifiedLogsCountQuery } from '@/data/logs/unified-logs-count-query'
import { useUnifiedLogsInfiniteQuery } from '@/data/logs/unified-logs-infinite-query'
import {
useUnifiedLogsInfiniteQuery,
type UnifiedLogsVariables,
} from '@/data/logs/unified-logs-infinite-query'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
@@ -16,16 +18,10 @@ import { useShortcut } from '@/state/shortcuts/useShortcut'
* timeline chart) plus the flattened + de-duplicated row list. Shared by the
* Logs page and the embedded, worker-scoped logs tab.
*/
export const useUnifiedLogsData = ({
projectRef,
search,
}: {
projectRef?: string
search: QuerySearchParamsType
}) => {
const logs = useUnifiedLogsInfiniteQuery({ projectRef, search })
const counts = useUnifiedLogsCountQuery({ projectRef, search })
const chart = useUnifiedLogsChartQuery({ projectRef, search })
export const useUnifiedLogsData = ({ projectRef, search, useOtel }: UnifiedLogsVariables) => {
const logs = useUnifiedLogsInfiniteQuery({ projectRef, search, useOtel })
const counts = useUnifiedLogsCountQuery({ projectRef, search, useOtel })
const chart = useUnifiedLogsChartQuery({ projectRef, search, useOtel })
const rawFlatData = useMemo(() => {
return logs.data?.pages?.flatMap((page) => page.data ?? []) ?? []
@@ -37,9 +37,11 @@ const DEFAULT_DURATION = '1'
interface DownloadLogsButtonProps {
searchParameters: QuerySearchParamsType
/** Forces the OTEL endpoint for log types that only exist there (see UnifiedLogsVariables). */
useOtel?: boolean
}
export const DownloadLogsButton = ({ searchParameters }: DownloadLogsButtonProps) => {
export const DownloadLogsButton = ({ searchParameters, useOtel }: DownloadLogsButtonProps) => {
const { ref } = useParams()
const pathname = usePathname()
const isLogs = pathname?.includes?.('/logs') ?? false
@@ -88,6 +90,7 @@ export const DownloadLogsButton = ({ searchParameters }: DownloadLogsButtonProps
search: searchParameters,
limit: Number(numRows),
hoursAgo: !hasSpecificTimeRange ? Number(numHours) : undefined,
useOtel,
})
}
@@ -17,7 +17,7 @@ import { useMemo, useState } from 'react'
import { Button, cn, ResizablePanel, ResizablePanelGroup } from 'ui'
import { WorkerCommandLine } from '../WorkerCommandLine'
import { buildWorkerLogsSearchParameters } from '../Workers.utils'
import { buildWorkerLogsColumnFilters, buildWorkerLogsSearchParameters } from '../Workers.utils'
import { WorkerLogStreamToggle } from './WorkerLogStreamToggle'
import {
generateDynamicColumns,
@@ -29,10 +29,7 @@ import { ServiceFlowPanel } from '@/components/interfaces/UnifiedLogs/ServiceFlo
import { CHART_CONFIG } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.chart-config'
import { SEARCH_PARAMS_PARSER } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.constants'
import { filterFields as defaultFilterFields } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.fields'
import {
buildDefaultColumnFilters,
buildFilterSearchUpdate,
} from '@/components/interfaces/UnifiedLogs/UnifiedLogs.filters'
import { buildFilterSearchUpdate } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.filters'
import {
useFilterSearchSync,
useLiveMode,
@@ -70,12 +67,16 @@ const DEFAULT_COLUMN_VISIBILITY: VisibilityState = { select: false }
const serializeLogsSearch = createSerializer(SEARCH_PARAMS_PARSER)
// Worker logs only exist on the OTEL endpoint; the BigQuery path has no worker
// sources, so the `otelUnifiedLogs` flag must not route this tab there.
const USE_OTEL = true
export const WorkerLogsTab = ({ workerName }: WorkerLogsTabProps) => {
const { ref: projectRef } = useParams()
const [search, setSearch] = useQueryStates(SEARCH_PARAMS_PARSER)
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(() =>
buildDefaultColumnFilters(search)
buildWorkerLogsColumnFilters(search)
)
const [columnVisibility, setColumnVisibility] =
useState<VisibilityState>(DEFAULT_COLUMN_VISIBILITY)
@@ -87,8 +88,8 @@ export const WorkerLogsTab = ({ workerName }: WorkerLogsTabProps) => {
// Always scoped to this worker, whatever the URL says — see buildWorkerLogsSearchParameters.
const searchParameters = useMemo(
() => buildWorkerLogsSearchParameters(search, workerName),
[search, workerName]
() => buildWorkerLogsSearchParameters(search, workerName, columnFilters),
[search, workerName, columnFilters]
)
const {
@@ -109,7 +110,7 @@ export const WorkerLogsTab = ({ workerName }: WorkerLogsTabProps) => {
isFetchingChart,
refetchAll,
isRefetching,
} = useUnifiedLogsData({ projectRef, search: searchParameters })
} = useUnifiedLogsData({ projectRef, search: searchParameters, useOtel: USE_OTEL })
const liveMode = useLiveMode(flatData)
@@ -201,7 +202,7 @@ export const WorkerLogsTab = ({ workerName }: WorkerLogsTabProps) => {
<div className="ml-auto flex flex-wrap items-center gap-x-2 gap-y-2">
<WorkerLogStreamToggle />
<RefreshButton isLoading={isRefetching} onRefresh={refetchAll} />
<DownloadLogsButton searchParameters={searchParameters} />
<DownloadLogsButton searchParameters={searchParameters} useOtel={USE_OTEL} />
<LiveButton
fetchPreviousPage={fetchPreviousPage}
searchParamsParser={SEARCH_PARAMS_PARSER}
@@ -264,7 +265,8 @@ export const WorkerLogsTab = ({ workerName }: WorkerLogsTabProps) => {
<div className="mx-auto max-w-md space-y-3 py-16 text-center">
<p className="text-sm text-foreground">No logs in the selected time range</p>
<p className="text-sm text-foreground-lighter">
Follow them from the Supabase CLI while you wait for traffic.
Widen the time range to find earlier deploys and builds, or follow new logs
from the Supabase CLI.
</p>
<div className="pt-1 text-left">
<WorkerCommandLine
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
import { getWorkerStateMeta, WORKER_NAME_WORDS, workerUrl } from './Workers.constants'
import type { Worker } from './Workers.types'
import {
buildWorkerLogsColumnFilters,
buildWorkerLogsSearchParameters,
filterWorkers,
formatResources,
@@ -252,4 +253,36 @@ describe('buildWorkerLogsSearchParameters', () => {
'worker:eq:embed',
])
})
it('falls back to the seeded date column filter until the URL carries a range', () => {
const range = [new Date('2026-09-08T10:00:00Z'), new Date('2026-09-09T10:00:00Z')]
const seeded = buildWorkerLogsSearchParameters(search, 'embed', [{ id: 'date', value: range }])
expect(seeded.date).toEqual(range)
const urlRange = [new Date('2026-09-09T09:00:00Z'), new Date('2026-09-09T10:00:00Z')]
const fromUrl = buildWorkerLogsSearchParameters({ ...search, date: urlRange }, 'embed', [
{ id: 'date', value: range },
])
expect(fromUrl.date).toEqual(urlRange)
})
})
describe('buildWorkerLogsColumnFilters', () => {
const now = new Date('2026-09-09T10:00:00Z')
it('defaults the time range to the last 24 hours when the URL has none', () => {
expect(buildWorkerLogsColumnFilters({ filter: null, date: null }, now)).toEqual([
{ id: 'date', value: [new Date('2026-09-08T10:00:00Z'), now] },
])
})
it('keeps a range and filters that came from the URL', () => {
const range = [new Date('2026-09-01T00:00:00Z'), new Date('2026-09-02T00:00:00Z')]
expect(
buildWorkerLogsColumnFilters({ filter: ['event_message:ilike:boom'], date: range }, now)
).toEqual([
{ id: 'event_message', value: { operator: '~~*', values: ['boom'] } },
{ id: 'date', value: range },
])
})
})
@@ -1,11 +1,17 @@
import type { ColumnFiltersState } from '@tanstack/react-table'
import { RUNTIMES, WORKER_NAME_WORDS, type RuntimeMeta } from './Workers.constants'
import type { Worker, WorkerAccess, WorkerBuildState } from './Workers.types'
import { parseLogsFilterUrlParams } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.filters'
import {
buildDefaultColumnFilters,
parseLogsFilterUrlParams,
} from '@/components/interfaces/UnifiedLogs/UnifiedLogs.filters'
import type {
QuerySearchParamsType,
SearchParamsType,
} from '@/components/interfaces/UnifiedLogs/UnifiedLogs.types'
import { toQuerySearchParameters } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.utils'
import { isArrayOfDates } from '@/components/ui/DataTable/DataTable.utils'
import {
WORKER_LOG_SOURCES,
WORKER_LOG_STREAM_SEARCH_PARAM,
@@ -96,21 +102,44 @@ export const getVisibleWorkerLogStreams = (
): WorkerLogStream[] =>
WORKER_LOG_STREAMS.filter((stream) => search[WORKER_LOG_STREAM_SEARCH_PARAM[stream]] !== false)
// Unified logs default to the last hour, which is too narrow for a worker: its
// lifecycle events (deploy, build) happen once, so open on the last day instead.
export const DEFAULT_WORKER_LOGS_WINDOW_HOURS = 24
// Seeds the table's column filters from the URL, defaulting the time range when
// the URL doesn't carry one. The seeded range is synced back to the URL by the
// regular filter sync, so the picker, the query and the link all agree.
export const buildWorkerLogsColumnFilters = (
search: Pick<SearchParamsType, 'filter' | 'date'>,
now: Date = new Date()
): ColumnFiltersState => {
const filters = buildDefaultColumnFilters(search)
if (filters.some((filter) => filter.id === 'date')) return filters
const from = new Date(now.getTime() - DEFAULT_WORKER_LOGS_WINDOW_HOURS * 60 * 60 * 1000)
return [...filters, { id: 'date', value: [from, now] }]
}
// The worker logs tab is always scoped to one worker: whatever the URL says, the
// query only ever sees the `workers` log type and this worker's rows. Any
// `log_type` / `worker` filters coming from the URL are dropped so they can't
// widen (or duplicate) that scope.
// widen (or duplicate) that scope. Until the seeded default time range has been
// synced into the URL, it is read from the column filters so the very first
// fetch already uses it.
export const buildWorkerLogsSearchParameters = (
search: SearchParamsType,
workerName: string
workerName: string,
columnFilters: ColumnFiltersState = []
): QuerySearchParamsType => {
const parameters = toQuerySearchParameters(search)
const otherFilters = (parameters.filter ?? []).filter((raw) => {
const parsed = parseLogsFilterUrlParams([raw])[0]
return parsed !== undefined && parsed.column !== 'log_type' && parsed.column !== 'worker'
})
const dateFilter = columnFilters.find((filter) => filter.id === 'date')?.value
const date = parameters.date ?? (isArrayOfDates(dateFilter) ? dateFilter : undefined)
return {
...parameters,
...(date ? { date } : {}),
filter: [...otherFilters, 'log_type:eq:workers', `worker:eq:${workerName}`],
}
}
@@ -157,13 +157,14 @@ export type UnifiedLogsChartData = Awaited<ReturnType<typeof getUnifiedLogsChart
export type UnifiedLogsChartError = ResponseError
export const useUnifiedLogsChartQuery = <TData = UnifiedLogsChartData>(
{ projectRef, search }: UnifiedLogsVariables,
{ projectRef, search, useOtel: useOtelOverride }: UnifiedLogsVariables,
{
enabled = true,
...options
}: UseCustomQueryOptions<UnifiedLogsChartData, UnifiedLogsChartError, TData> = {}
) => {
const useOtel = useFlag('otelUnifiedLogs')
const otelFlag = useFlag('otelUnifiedLogs')
const useOtel = useOtelOverride ?? otelFlag
return useQuery<UnifiedLogsChartData, UnifiedLogsChartError, TData>({
queryKey: [...logsKeys.unifiedLogsChart(projectRef, search), { otel: useOtel }],
queryFn: ({ signal }) => getUnifiedLogsChart({ projectRef, search, useOtel }, signal),
@@ -84,13 +84,14 @@ export type UnifiedLogsCountData = Awaited<ReturnType<typeof getUnifiedLogsCount
export type UnifiedLogsCountError = ResponseError
export const useUnifiedLogsCountQuery = <TData = UnifiedLogsCountData>(
{ projectRef, search }: UnifiedLogsVariables,
{ projectRef, search, useOtel: useOtelOverride }: UnifiedLogsVariables,
{
enabled = true,
...options
}: UseCustomQueryOptions<UnifiedLogsCountData, UnifiedLogsCountError, TData> = {}
) => {
const useOtel = useFlag('otelUnifiedLogs')
const otelFlag = useFlag('otelUnifiedLogs')
const useOtel = useOtelOverride ?? otelFlag
return useQuery<UnifiedLogsCountData, UnifiedLogsCountError, TData>({
queryKey: [...logsKeys.unifiedLogsCount(projectRef, search), { otel: useOtel }],
queryFn: ({ signal }) => getUnifiedLogsCount({ projectRef, search, useOtel }, signal),
@@ -27,7 +27,15 @@ export const UNIFIED_LOGS_QUERY_OPTIONS = {
export type UnifiedLogsData = any
export type UnifiedLogsError = ResponseError
export type UnifiedLogsVariables = { projectRef?: string; search: QuerySearchParamsType }
export type UnifiedLogsVariables = {
projectRef?: string
search: QuerySearchParamsType
/**
* Overrides the `otelUnifiedLogs` flag. Some log types (e.g. workers) only
* exist on the OTEL endpoint, so their surfaces pass `true` regardless of the flag.
*/
useOtel?: boolean
}
export const getUnifiedLogsISOStartEnd = (
search: QuerySearchParamsType,
@@ -140,7 +148,7 @@ export async function getUnifiedLogs(
}
export const useUnifiedLogsInfiniteQuery = <TData = UnifiedLogsData>(
{ projectRef, search }: UnifiedLogsVariables,
{ projectRef, search, useOtel: useOtelOverride }: UnifiedLogsVariables,
{
enabled = true,
...options
@@ -152,7 +160,8 @@ export const useUnifiedLogsInfiniteQuery = <TData = UnifiedLogsData>(
PageParam | null
> = {}
) => {
const useOtel = useFlag('otelUnifiedLogs')
const otelFlag = useFlag('otelUnifiedLogs')
const useOtel = useOtelOverride ?? otelFlag
return useInfiniteQuery({
queryKey: [...logsKeys.unifiedLogsInfinite(projectRef, search), { otel: useOtel }],
queryFn: ({ signal, pageParam }) => {