fix(logs): route the unified logs export through the safe SQL boundary

`retrieveUnifiedLogs` built its SQL with a plain template literal, which
stringifies the `SafeLogSqlFragment` returned by the query builder and
drops the brand, then posted it with a raw `post()` call. It was the one
place in the logs data layer that bypassed `executeAnalyticsSql`, the
wire boundary that rejects unbranded SQL at compile time.

Composes with `safeSql` and runs through `executeAnalyticsSql` instead,
matching the row-list query in `unified-logs-infinite-query.ts`. Like the
row list, it now also raises an error the endpoint reports inside a
successful response, instead of silently exporting an empty file.

No behaviour change for valid input. The limit goes through
`analyticsLiteral`, which rejects a non-finite value rather than emitting
`LIMIT NaN` — not reachable from the download dialog, which offers
100/500/1000, but the previous code would have sent it verbatim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
David WhittingtonandClaude Opus 5.5 committed 2026-09-30 13:12:24 -05:00
1 parent 2cb70302b0
commit f8fcdb0c27
1 file changed
+16 -7
+16 -7
View File
@@ -2,13 +2,15 @@ import { useMutation } from '@tanstack/react-query'
import { useFlag } from 'common'
import { toast } from 'sonner'
import { executeAnalyticsSql } from './execute-analytics-sql'
import { logsAllEndpointUrl, pickLogsQueryBuilder } from './logs-endpoint'
import { analyticsLiteral, safeSql } from './safe-analytics-sql'
import { getUnifiedLogsISOStartEnd } from './unified-logs-infinite-query'
import { mapUnifiedLogRow, parseUnifiedLogsQueryRows } from './unified-logs.utils'
import { getUnifiedLogsQuery } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.queries'
import { getUnifiedLogsQuery as getUnifiedLogsQueryBq } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.queries.bq'
import { QuerySearchParamsType } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.types'
import { handleError, post } from '@/data/fetchers'
import { handleError } from '@/data/fetchers'
import type { ResponseError, UseCustomMutationOptions } from '@/types'
export type getUnifiedLogsVariables = {
@@ -32,15 +34,22 @@ export async function retrieveUnifiedLogs({
const { isoTimestampStart, isoTimestampEnd } = getUnifiedLogsISOStartEnd(search, hoursAgo)
const buildQuery = pickLogsQueryBuilder(useOtel, getUnifiedLogsQuery, getUnifiedLogsQueryBq)
const sql = `${buildQuery(search)} ORDER BY timestamp DESC, id DESC LIMIT ${limit}`
// `safeSql` (not a plain template literal) keeps the SafeLogSqlFragment brand
// intact, and `analyticsLiteral` rejects a non-finite limit instead of
// emitting `LIMIT NaN`. Mirrors the row-list query in
// `unified-logs-infinite-query.ts`.
const sql = safeSql`${buildQuery(search)} ORDER BY timestamp DESC, id DESC LIMIT ${analyticsLiteral(limit)}`
const endpoint = logsAllEndpointUrl(useOtel)
const { data, error } = await post(endpoint, {
params: { path: { ref: projectRef } },
body: { iso_timestamp_start: isoTimestampStart, iso_timestamp_end: isoTimestampEnd, sql },
const data = await executeAnalyticsSql({
projectRef,
endpoint: logsAllEndpointUrl(useOtel),
sql,
iso_timestamp_start: isoTimestampStart,
iso_timestamp_end: isoTimestampEnd,
})
if (error) handleError(error)
// The endpoint can report a query error inside a successful response.
if (data?.error) handleError(new Error(data.error as string))
const resultData = parseUnifiedLogsQueryRows(data?.result)
const result = resultData.map(mapUnifiedLogRow)