From 2a60bd866f15724b82dd3ca02dc583ff81f58aaf Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 16:32:34 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01R5ev3QGKtXVansc1cz5uzR --- .../UnifiedLogs/UnifiedLogs.hooks.ts | 20 +++++------ .../components/DownloadLogsButton.tsx | 5 ++- .../Workers/WorkerDetail/WorkerLogsTab.tsx | 24 +++++++------ .../interfaces/Workers/Workers.utils.test.ts | 33 +++++++++++++++++ .../interfaces/Workers/Workers.utils.ts | 35 +++++++++++++++++-- .../data/logs/unified-logs-chart-query.ts | 5 +-- .../data/logs/unified-logs-count-query.ts | 5 +-- .../data/logs/unified-logs-infinite-query.ts | 15 ++++++-- 8 files changed, 108 insertions(+), 34 deletions(-) diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.hooks.ts b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.hooks.ts index d83902f5005..68e15428897 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.hooks.ts +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.hooks.ts @@ -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 ?? []) ?? [] diff --git a/apps/studio/components/interfaces/UnifiedLogs/components/DownloadLogsButton.tsx b/apps/studio/components/interfaces/UnifiedLogs/components/DownloadLogsButton.tsx index 54a8e4f0997..6cd74febbd8 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/components/DownloadLogsButton.tsx +++ b/apps/studio/components/interfaces/UnifiedLogs/components/DownloadLogsButton.tsx @@ -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, }) } diff --git a/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerLogsTab.tsx b/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerLogsTab.tsx index f00b27b0c18..a3ecc3c599e 100644 --- a/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerLogsTab.tsx +++ b/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerLogsTab.tsx @@ -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(() => - buildDefaultColumnFilters(search) + buildWorkerLogsColumnFilters(search) ) const [columnVisibility, setColumnVisibility] = useState(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) => {
- + {

No logs in the selected time range

- 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.

{ '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 }, + ]) + }) }) diff --git a/apps/studio/components/interfaces/Workers/Workers.utils.ts b/apps/studio/components/interfaces/Workers/Workers.utils.ts index f7515d90b02..a9c7df64c24 100644 --- a/apps/studio/components/interfaces/Workers/Workers.utils.ts +++ b/apps/studio/components/interfaces/Workers/Workers.utils.ts @@ -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, + 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}`], } } diff --git a/apps/studio/data/logs/unified-logs-chart-query.ts b/apps/studio/data/logs/unified-logs-chart-query.ts index 6a9315153d1..b893c6d32a5 100644 --- a/apps/studio/data/logs/unified-logs-chart-query.ts +++ b/apps/studio/data/logs/unified-logs-chart-query.ts @@ -157,13 +157,14 @@ export type UnifiedLogsChartData = Awaited( - { projectRef, search }: UnifiedLogsVariables, + { projectRef, search, useOtel: useOtelOverride }: UnifiedLogsVariables, { enabled = true, ...options }: UseCustomQueryOptions = {} ) => { - const useOtel = useFlag('otelUnifiedLogs') + const otelFlag = useFlag('otelUnifiedLogs') + const useOtel = useOtelOverride ?? otelFlag return useQuery({ queryKey: [...logsKeys.unifiedLogsChart(projectRef, search), { otel: useOtel }], queryFn: ({ signal }) => getUnifiedLogsChart({ projectRef, search, useOtel }, signal), diff --git a/apps/studio/data/logs/unified-logs-count-query.ts b/apps/studio/data/logs/unified-logs-count-query.ts index 0a49dfbadd6..0bfaa49a441 100644 --- a/apps/studio/data/logs/unified-logs-count-query.ts +++ b/apps/studio/data/logs/unified-logs-count-query.ts @@ -84,13 +84,14 @@ export type UnifiedLogsCountData = Awaited( - { projectRef, search }: UnifiedLogsVariables, + { projectRef, search, useOtel: useOtelOverride }: UnifiedLogsVariables, { enabled = true, ...options }: UseCustomQueryOptions = {} ) => { - const useOtel = useFlag('otelUnifiedLogs') + const otelFlag = useFlag('otelUnifiedLogs') + const useOtel = useOtelOverride ?? otelFlag return useQuery({ queryKey: [...logsKeys.unifiedLogsCount(projectRef, search), { otel: useOtel }], queryFn: ({ signal }) => getUnifiedLogsCount({ projectRef, search, useOtel }, signal), diff --git a/apps/studio/data/logs/unified-logs-infinite-query.ts b/apps/studio/data/logs/unified-logs-infinite-query.ts index cf564c9c81f..4e322e8b645 100644 --- a/apps/studio/data/logs/unified-logs-infinite-query.ts +++ b/apps/studio/data/logs/unified-logs-infinite-query.ts @@ -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 = ( - { projectRef, search }: UnifiedLogsVariables, + { projectRef, search, useOtel: useOtelOverride }: UnifiedLogsVariables, { enabled = true, ...options @@ -152,7 +160,8 @@ export const useUnifiedLogsInfiniteQuery = ( 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 }) => {