From f8fcdb0c278406111928fa920964d5b10f09fda4 Mon Sep 17 00:00:00 2001 From: David Whittington Date: Wed, 12 Aug 2026 18:46:47 -0500 Subject: [PATCH] fix(logs): route the unified logs export through the safe SQL boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- apps/studio/data/logs/get-unified-logs.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/apps/studio/data/logs/get-unified-logs.ts b/apps/studio/data/logs/get-unified-logs.ts index fbcc0c4dc71..c037abe3e57 100644 --- a/apps/studio/data/logs/get-unified-logs.ts +++ b/apps/studio/data/logs/get-unified-logs.ts @@ -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)