mirror of
https://github.com/supabase/supabase.git
synced 2026-09-06 09:59:03 +08:00
<img width="1510" height="862" alt="image" src="https://github.com/user-attachments/assets/f7157bad-9b23-4d73-a9aa-2a7a7c179318" /> ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Feature and bug fix. ## What is the current behavior? `query_logs` can return rows to the assistant, but the chat UI does not hydrate those rows into the query result by default. The query only becomes visible after clicking **Run query**, even though the same SQL and time range work when rerun manually. ## What is the new behavior? - Renders `query_logs` tool output through a dedicated logs message part using the shared assistant query cell. - Parses the exact MCP untrusted-data envelope into the initial query result, without changing what the assistant model receives. - Preserves the logs source and time range for manual reruns. - Infers a useful table or chart presentation from the returned rows while retaining explicit display settings. - Adds focused tests for MCP result parsing, timestamps, errors, query source handling, and visualization inference. ## How to test 1. Check out this PR and run Studio against a project that has recent logs. Generate some project activity first, such as an API request, if needed. 2. Open the AI Assistant and ask: `Show log counts by minute for the last 15 minutes and summarize any spikes.` 3. Wait for `query_logs` to finish. Verify the query cell appears with results already populated; do not click **Run query** first. 4. Verify the aggregate result opens as a chart, then switch to the table view and confirm the underlying rows are present. 5. Click **Run query** and verify the query runs successfully again using the same logs source and 15-minute time range. 6. Ask: `Show the 20 most recent log entries from the last 15 minutes.` Verify this non-aggregate result opens as a table with rows already populated. 7. Confirm the assistant's written summary agrees with the displayed rows and does not report zero rows when results are visible. ## Additional context This is the top PR in stack #49294 and depends on the back-end knowledge change in #49292. Verified with 59 focused tests across assistant context, Studio/MCP tools, query display, and logs result parsing. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added AI Assistant support for querying and displaying application logs. * Added automatic visualization selection, including charts for time-based and categorical data. * Added source-aware query handling with dedicated titles, time ranges, and result displays. * Added clearer loading, parsing, and error states for log queries. * **Bug Fixes** * Improved handling of streamed results, source changes, and query display updates. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
133 lines
4.5 KiB
TypeScript
133 lines
4.5 KiB
TypeScript
import { untrustedSql } from '@supabase/pg-meta'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
changeAssistantQuerySource,
|
|
createAssistantQueryModel,
|
|
getAssistantQueryDisplay,
|
|
setAssistantQuerySql,
|
|
shouldClearAssistantQueryResult,
|
|
toAssistantQueryResult,
|
|
} from './AssistantQueryCell.utils'
|
|
import { DEFAULT_CELL_ROW_LIMIT } from '@/components/interfaces/Explorer/QueryCell/QueryCell.utils'
|
|
import { untrustedLogSql } from '@/data/logs/safe-analytics-sql'
|
|
|
|
describe('getAssistantQueryDisplay', () => {
|
|
it('defaults to a table view with no chart when axes are missing', () => {
|
|
expect(getAssistantQueryDisplay({})).toEqual({ view: 'table', chart: undefined })
|
|
})
|
|
|
|
it('builds a bar chart config from axis hints', () => {
|
|
expect(getAssistantQueryDisplay({ view: 'chart', xAxis: 'day', yAxis: 'signups' })).toEqual({
|
|
view: 'chart',
|
|
chart: {
|
|
type: 'bar',
|
|
x_column: 'day',
|
|
y_series: ['signups'],
|
|
cumulative: false,
|
|
scale: 'linear',
|
|
show_labels: false,
|
|
},
|
|
})
|
|
})
|
|
|
|
it('keeps an empty y-axis list when only the x-axis is provided', () => {
|
|
expect(getAssistantQueryDisplay({ xAxis: 'day' }).chart?.y_series).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('toAssistantQueryResult', () => {
|
|
it('returns undefined when the output is not an array of row objects', () => {
|
|
expect(toAssistantQueryResult(undefined)).toBeUndefined()
|
|
expect(toAssistantQueryResult('error')).toBeUndefined()
|
|
expect(toAssistantQueryResult({ rows: [] })).toBeUndefined()
|
|
})
|
|
|
|
it('keeps row objects and drops primitives, arrays, and nulls', () => {
|
|
expect(toAssistantQueryResult([{ id: 1 }, null, ['x'], 4, { id: 2 }])).toEqual({
|
|
rows: [{ id: 1 }, { id: 2 }],
|
|
})
|
|
})
|
|
|
|
it('accepts an empty array as a successful empty result', () => {
|
|
expect(toAssistantQueryResult([])).toEqual({ rows: [] })
|
|
})
|
|
})
|
|
|
|
describe('assistant query model', () => {
|
|
it('starts as a database query with the notebook default row limit', () => {
|
|
expect(createAssistantQueryModel('select 1')).toEqual({
|
|
_tag: 'database',
|
|
uncheckedSql: untrustedSql('select 1'),
|
|
rowLimit: DEFAULT_CELL_ROW_LIMIT,
|
|
})
|
|
})
|
|
|
|
it('starts as a logs query when the source is logs', () => {
|
|
const time_range = { _tag: 'relative_time_range' as const, unit: 'day' as const, amount: 1 }
|
|
expect(createAssistantQueryModel('select 1 from logs', { _tag: 'logs', time_range })).toEqual({
|
|
_tag: 'logs',
|
|
uncheckedSql: untrustedLogSql('select 1 from logs'),
|
|
time_range,
|
|
})
|
|
})
|
|
|
|
it('rebrands the live SQL for the current backend', () => {
|
|
const database = createAssistantQueryModel('select 1')
|
|
expect(setAssistantQuerySql(database, 'select 2').uncheckedSql).toBe(untrustedSql('select 2'))
|
|
|
|
const logs = changeAssistantQuerySource(database, {
|
|
_tag: 'logs',
|
|
time_range: { _tag: 'relative_time_range', unit: 'hour', amount: 1 },
|
|
})
|
|
expect(logs._tag).toBe('logs')
|
|
expect(setAssistantQuerySql(logs, 'select 3').uncheckedSql).toBe(untrustedLogSql('select 3'))
|
|
})
|
|
|
|
it('carries the SQL across a source change and restores the default row limit onto logs → database', () => {
|
|
const logs = changeAssistantQuerySource(createAssistantQueryModel('select 1'), {
|
|
_tag: 'logs',
|
|
time_range: { _tag: 'relative_time_range', unit: 'hour', amount: 1 },
|
|
})
|
|
const database = changeAssistantQuerySource(logs, { _tag: 'database' })
|
|
|
|
expect(database).toEqual({
|
|
_tag: 'database',
|
|
uncheckedSql: untrustedSql('select 1'),
|
|
rowLimit: DEFAULT_CELL_ROW_LIMIT,
|
|
})
|
|
})
|
|
|
|
it('clears an existing result when only the logs time range changes', () => {
|
|
const logs = createAssistantQueryModel('select 1 from logs', {
|
|
_tag: 'logs',
|
|
time_range: { _tag: 'relative_time_range', unit: 'hour', amount: 1 },
|
|
})
|
|
|
|
expect(
|
|
shouldClearAssistantQueryResult(logs, {
|
|
_tag: 'logs',
|
|
time_range: { _tag: 'relative_time_range', unit: 'hour', amount: 3 },
|
|
})
|
|
).toBe(true)
|
|
expect(
|
|
shouldClearAssistantQueryResult(logs, {
|
|
_tag: 'logs',
|
|
time_range: { _tag: 'relative_time_range', unit: 'hour', amount: 1 },
|
|
})
|
|
).toBe(false)
|
|
})
|
|
|
|
it('compares canonical database bindings when deciding whether to clear results', () => {
|
|
const database = createAssistantQueryModel('select 1')
|
|
|
|
expect(shouldClearAssistantQueryResult(database, { _tag: 'database' })).toBe(false)
|
|
expect(
|
|
shouldClearAssistantQueryResult(database, {
|
|
_tag: 'database',
|
|
database_identifier: 'replica-1',
|
|
})
|
|
).toBe(true)
|
|
})
|
|
})
|