Files
supabase/apps/studio/hooks/analytics/useLogsUrlState.ts
2aa1b52234 feat(studio): add feature to rewrite queries DEBUG-145 (#47266)
## Problem

Moving the Logs Explorer to ClickHouse means users' saved BigQuery
queries no longer run.
<img width="2430" height="1010" alt="CleanShot 2026-06-29 at 11 36
04@2x"
src="https://github.com/user-attachments/assets/ae0ab155-7d3d-4ae9-81c3-22bf3a88cf8c"
/>

## Fix

Rewrite the query with AI instead of a SQL transpiler. AI handles the
long tail of nested fields and dialect differences far better than a
rule-based rewriter, and it needs no extra runtime dependency.

- `rewriteLogsSqlWithAI` posts the current query to
`/api/ai/code/complete` with `dialect: 'clickhouse'`. The endpoint skips
the Postgres schema and best-practices for that dialect and uses
logs-specific instructions and model so the output is ClickHouse logs
SQL (FROM `logs` + `source` filter, no `unnest` joins, nested fields
read from `log_attributes['...']`).
- The query's `source` is detected and its real `log_attributes` keys
are fetched and passed to the model, so it maps to exact paths instead
of guessing.
- The rewrite runs in the background and is proposed as a side-by-side
accept/discard diff in the editor. The AI Assistant panel is not opened.
- Entry points: a banner shown only for legacy-looking queries
(dismissal persisted), and a "Fix Query" button next to Field Reference.
- The Field Reference drawers discover `log_attributes` keys from real
data so the listed fields match what the source actually emits.

## Dependencies

Built on top of #47265 (Logs Explorer -> OTEL endpoint) — that is the
base branch of this PR. Merge #47265 first. Behind `otelLegacyLogs` (off
by default).

Part of DEBUG-145 (split from #47087).

## How to test

- Open the Logs Explorer with a BigQuery logs query (the templates have
some), click "Fix Query", and confirm the diff shows valid ClickHouse
SQL. Accept it and confirm the applied query runs.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added an OTEL legacy logs workflow (behind a feature flag) with an
interactive banner and a “Fix Query” ClickHouse rewrite action,
including an accept/discard diff review overlay.
* Introduced OTEL-aware field reference rendering with dynamic discovery
of `log_attributes` keys and updated OTEL source insertion behavior.
* Enabled dialect-aware SQL completion for ClickHouse logs, using
logs-specific instructions and output constraints.
* **Bug Fixes**
* Improved rewrite flow validation and handling, including log source
detection and cleanup of AI-generated SQL formatting.
* **Tests**
* Added Vitest coverage for rewrite prompt generation,
detection/classification utilities, SQL fence stripping, OTEL field
mapping, and OTEL log attribute key discovery.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-06-29 14:31:18 +02:00

138 lines
3.3 KiB
TypeScript

import { createParser, useQueryState } from 'nuqs'
import { useCallback } from 'react'
import { Filters } from '@/components/interfaces/Settings/Logs/Logs.types'
interface LogsUrlState {
search: string
timestampStart: string
timestampEnd: string
selectedLogId: string | null
filters: Filters
}
const defaultState: LogsUrlState = {
search: '',
timestampStart: '',
timestampEnd: '',
selectedLogId: null,
filters: {},
}
const stringWithDefault = (defaultValue: string) =>
createParser({
parse: (v) => v ?? defaultValue,
serialize: (v) => v || '',
})
const jsonWithDefault = <T>(defaultValue: T) =>
createParser({
parse: (v) => {
if (!v) return defaultValue
try {
return JSON.parse(v) as T
} catch (e) {
return defaultValue
}
},
serialize: (v) => {
if (!v) return ''
try {
return JSON.stringify(v)
} catch (e) {
return ''
}
},
})
export function useLogsUrlState() {
const [searchValue, setSearchValue] = useQueryState('s', stringWithDefault(defaultState.search))
const [timestampStartValue, setTimestampStartValue] = useQueryState(
'its',
stringWithDefault(defaultState.timestampStart)
)
const [timestampEndValue, setTimestampEndValue] = useQueryState(
'ite',
stringWithDefault(defaultState.timestampEnd)
)
const [selectedLogIdValue, setSelectedLogIdValue] = useQueryState('log', stringWithDefault(''))
const [filtersValue, setFiltersValue] = useQueryState(
'f',
jsonWithDefault<Filters>(defaultState.filters)
)
// Ensure we never return null values
const search = searchValue || defaultState.search
const timestampStart = timestampStartValue || defaultState.timestampStart
const timestampEnd = timestampEndValue || defaultState.timestampEnd
const selectedLogId = selectedLogIdValue || null
const filters = filtersValue || defaultState.filters
const setSearch = useCallback(
(value: string) => {
setSearchValue(value || defaultState.search)
},
[setSearchValue]
)
const setTimeRange = useCallback(
(start: string, end: string) => {
setTimestampStartValue(start || defaultState.timestampStart)
setTimestampEndValue(end || defaultState.timestampEnd)
},
[setTimestampStartValue, setTimestampEndValue]
)
const setSelectedLogId = useCallback(
(value: string | null) => {
setSelectedLogIdValue(value || '')
},
[setSelectedLogIdValue]
)
const setFilters = useCallback(
(value: Filters) => {
setFiltersValue(value || defaultState.filters)
},
[setFiltersValue]
)
const updateFilter = useCallback(
(key: string, value: any) => {
setFilters({ ...filters, [key]: value })
},
[filters, setFilters]
)
const reset = useCallback(() => {
setSearch(defaultState.search)
setTimestampStartValue(defaultState.timestampStart)
setTimestampEndValue(defaultState.timestampEnd)
setSelectedLogIdValue('')
setFiltersValue(defaultState.filters)
}, [
setSearch,
setTimestampStartValue,
setTimestampEndValue,
setSelectedLogIdValue,
setFiltersValue,
])
return {
// State
search,
timestampStart,
timestampEnd,
selectedLogId,
filters,
// Setters
setSearch,
setTimeRange,
setSelectedLogId,
setFilters,
updateFilter,
reset,
}
}