refactor(studio): share cached log attributes with request timelines

This commit is contained in:
Saxon Fletcher committed 2026-09-24 16:31:07 +10:00
1 parent 308bf7bd77
commit e09ffb024b
5 files changed
+103 -26

No files matched your search

@@ -54,6 +54,7 @@ export function LogDetail({
})
const { data: fetchedAttributes } = useQuery({
...attributesQuery,
select: (row) => row?.log_attributes ?? null,
enabled: canFetchAttributes && attributesQuery.enabled,
})
const attributes = logsMetadata ? (row.metadata ?? fetchedAttributes) : undefined
@@ -38,7 +38,10 @@ describe('log attributes', () => {
})
},
})
await expect(fetchAttributes()).resolves.toEqual({ 'request.method': 'GET' })
await expect(fetchAttributes()).resolves.toEqual({
source: 'edge_logs',
log_attributes: { 'request.method': 'GET' },
})
expect(body).toMatchObject({
iso_timestamp_start: '2026-01-01T09:59:00.000Z',
iso_timestamp_end: '2026-01-01T10:01:00.000Z',
@@ -46,14 +49,14 @@ describe('log attributes', () => {
})
it.each([{ result: [] }, { result: [{ source: 'edge_logs', log_attributes: null }] }])(
'returns null for absent attributes: %j',
'preserves the source when attributes are absent: %j',
async ({ result }) => {
addAPIMock({
method: 'post',
path: '/platform/projects/:ref/analytics/endpoints/logs.all.otel',
response: () => HttpResponse.json<LogsResponse>({ result }),
})
await expect(fetchAttributes()).resolves.toBeNull()
await expect(fetchAttributes()).resolves.toEqual(result[0] ?? null)
}
)
@@ -42,8 +42,8 @@ export async function runOtelLogsSql(variables: Omit<ExecuteAnalyticsSqlVariable
return data?.result ?? []
}
/** One log's source and attributes, or undefined when it can't be found. */
export async function getLogAttributesRow(
/** One log's source and attributes, or null when it can't be found. */
async function getUnifiedLogAttributes(
{ projectRef, logId, source, logTimestampMs }: UnifiedLogAttributesVariables,
signal?: AbortSignal
) {
@@ -60,15 +60,7 @@ export async function getLogAttributesRow(
...getLogTimeWindow(logTimestampMs, LOG_WINDOW_MS),
signal,
})
return z.array(logAttributesRowSchema).parse(result)[0]
}
async function getUnifiedLogAttributes(
variables: UnifiedLogAttributesVariables,
signal?: AbortSignal
) {
const row = await getLogAttributesRow(variables, signal)
return row?.log_attributes ?? null
return z.array(logAttributesRowSchema).parse(result)[0] ?? null
}
export type UnifiedLogAttributesData = Awaited<ReturnType<typeof getUnifiedLogAttributes>>
@@ -3,6 +3,7 @@ import type { platformComponents } from 'api-types'
import { HttpResponse } from 'msw'
import { describe, expect, it } from 'vitest'
import { unifiedLogAttributesQueryOptions } from './unified-log-attributes-query'
import { unifiedLogRequestTimelineQueryOptions } from './unified-log-request-timeline-query'
import { addAPIMock, type APIErrorBody } from '@/tests/lib/msw'
@@ -34,6 +35,86 @@ const step = (id: string, source: string, metadata: Record<string, string>) => (
})
describe('request timeline query', () => {
it.each(['attributes first', 'timeline first', 'concurrently'])(
'shares the attribute lookup with the overview: %s',
async (order) => {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
const root = { source: 'edge_logs', log_attributes: { 'request.headers.cf_ray': 'ray-1' } }
const related = step('invocation', 'function_edge_logs', {})
const requests: string[] = []
addAPIMock({
method: 'post',
path: '/platform/projects/:ref/analytics/endpoints/logs.all.otel',
response: async ({ request }) => {
const { sql } = (await request.json()) as { sql: string }
requests.push(sql)
return HttpResponse.json<LogsResponse>({
result: sql.includes('log attributes') ? [root] : [related],
})
},
})
const attributes = () => client.fetchQuery(unifiedLogAttributesQueryOptions(variables))
const timeline = () => client.fetchQuery(unifiedLogRequestTimelineQueryOptions(variables))
if (order === 'attributes first') {
await attributes()
await timeline()
} else if (order === 'timeline first') {
await timeline()
await attributes()
} else {
await Promise.all([attributes(), timeline()])
}
expect(client.getQueryData(unifiedLogAttributesQueryOptions(variables).queryKey)).toEqual(
root
)
expect(
client
.getQueryData(unifiedLogRequestTimelineQueryOptions(variables).queryKey)
?.logs.map((log) => log.id)
).toEqual(['invocation'])
// Request count is the contract here: consumers share both cached and in-flight data.
expect(requests.filter((sql) => sql.includes('log attributes'))).toHaveLength(1)
expect(requests).toHaveLength(2)
}
)
it('cancels a timeline without cancelling the shared overview lookup', async () => {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
let releaseResponse = () => {}
const responseReady = new Promise<void>((resolve) => {
releaseResponse = resolve
})
let markStarted = () => {}
const started = new Promise<void>((resolve) => {
markStarted = resolve
})
const root = { source: 'edge_logs', log_attributes: { 'request.headers.cf_ray': 'ray-1' } }
const requests: string[] = []
addAPIMock({
method: 'post',
path: '/platform/projects/:ref/analytics/endpoints/logs.all.otel',
response: async ({ request }) => {
const { sql } = (await request.json()) as { sql: string }
requests.push(sql)
markStarted()
await responseReady
return HttpResponse.json<LogsResponse>({ result: [root] })
},
})
const timelineOptions = unifiedLogRequestTimelineQueryOptions(variables)
const cancelled = expect(client.fetchQuery(timelineOptions)).rejects.toThrow()
const attributes = client.fetchQuery(unifiedLogAttributesQueryOptions(variables))
await started
await client.cancelQueries({ queryKey: timelineOptions.queryKey, exact: true })
releaseResponse()
await cancelled
await expect(attributes).resolves.toEqual(root)
expect(requests).toHaveLength(1)
})
it('follows an execution ID from a request and stops after two lookups', async () => {
const requests: { sql: string; iso_timestamp_start: string; iso_timestamp_end: string }[] = []
const invocation = step('invocation', 'function_edge_logs', { execution_id: 'exec-1' })
@@ -1,11 +1,12 @@
import { queryOptions } from '@tanstack/react-query'
import { queryOptions, type QueryClient } from '@tanstack/react-query'
import { z } from 'zod'
import { logsKeys } from './keys'
import {
getLogAttributesRow,
getLogTimeWindow,
runOtelLogsSql,
unifiedLogAttributesQueryOptions,
type UnifiedLogAttributesVariables,
} from './unified-log-attributes-query'
import { UNIFIED_LOGS_QUERY_OPTIONS } from './unified-logs-infinite-query'
import { mapUnifiedLogRow, unifiedLogsQueryRowSchema } from './unified-logs.utils'
@@ -28,13 +29,7 @@ const MAX_LOOKUPS = 2
const timelineRowSchema = unifiedLogsQueryRowSchema.extend({ source: z.string() })
export type UnifiedLogRequestTimelineVariables = {
projectRef?: string
logId?: string
/** Source of the selected log, e.g. `edge_logs`. */
source?: string
logTimestampMs?: number | null
}
export type UnifiedLogRequestTimelineVariables = UnifiedLogAttributesVariables
export type UnifiedLogRequestTimelineError = ResponseError
@@ -44,6 +39,7 @@ const hasNewIds = (next: RequestCorrelationIds, prev: RequestCorrelationIds) =>
async function getUnifiedLogRequestTimeline(
{ projectRef, logId, source, logTimestampMs }: UnifiedLogRequestTimelineVariables,
queryClient: QueryClient,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('projectRef is required')
@@ -53,7 +49,11 @@ async function getUnifiedLogRequestTimeline(
throw new Error('logTimestampMs is required')
}
const root = await getLogAttributesRow({ projectRef, logId, source, logTimestampMs }, signal)
const root = await queryClient.fetchQuery(
unifiedLogAttributesQueryOptions({ projectRef, logId, source, logTimestampMs })
)
// A shared attribute lookup may outlive this timeline request.
signal?.throwIfAborted()
let ids = getRequestCorrelationIds(
root ? [{ source: root.source, attributes: root.log_attributes }] : []
@@ -100,8 +100,8 @@ export const unifiedLogRequestTimelineQueryOptions = ({
}: UnifiedLogRequestTimelineVariables) =>
queryOptions<UnifiedLogRequestTimelineData, UnifiedLogRequestTimelineError>({
queryKey: logsKeys.requestTimeline(projectRef, logId, source, logTimestampMs),
queryFn: ({ signal }) =>
getUnifiedLogRequestTimeline({ projectRef, logId, source, logTimestampMs }, signal),
queryFn: ({ client, signal }) =>
getUnifiedLogRequestTimeline({ projectRef, logId, source, logTimestampMs }, client, signal),
enabled:
IS_PLATFORM &&
typeof projectRef !== 'undefined' &&