From 908617f625c6324df3910a1e4fbd61194458e703 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 11 Nov 2026 14:34:47 -0800 Subject: [PATCH 1/2] fix(selectors): redact email path segments from provider failure logs; correct table timeout guidance --- .../selectors/server/providers/http.test.ts | 20 +++++++++++++++++++ .../lib/selectors/server/providers/http.ts | 10 +++++++--- apps/sim/lib/table/application/rows.ts | 11 +++++----- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/apps/sim/lib/selectors/server/providers/http.test.ts b/apps/sim/lib/selectors/server/providers/http.test.ts index d79e3a5da42..b32e09c20f9 100644 --- a/apps/sim/lib/selectors/server/providers/http.test.ts +++ b/apps/sim/lib/selectors/server/providers/http.test.ts @@ -1,3 +1,4 @@ +import { getMockLogger } from '@sim/testing/mocks/logger.mock' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' import { SelectorConnectionUnavailableError, @@ -96,4 +97,23 @@ describe('provider HTTP selector boundary', () => { expect(cancel).toHaveBeenCalledOnce() } ) + + it('logs a failed provider call without its query string, search text, or email path segments', async () => { + mockFetch.mockResolvedValueOnce(new Response('{}', { status: 404 })) + + await expect( + fetchProviderJson( + "https://www.googleapis.com/calendar/v3/calendars/jane%40example.com/root/search(q='payroll')?access_token=secret" + ) + ).rejects.toBeInstanceOf(SelectorOptionsUnavailableError) + + expect(getMockLogger('SelectorProviderHttp').warn).toHaveBeenCalledWith( + 'Selector provider request failed', + { + endpoint: 'www.googleapis.com/calendar/v3/calendars/{redacted}/root/search()', + reason: 'status', + status: 404, + } + ) + }) }) diff --git a/apps/sim/lib/selectors/server/providers/http.ts b/apps/sim/lib/selectors/server/providers/http.ts index dc12d60b635..98f33b27d2a 100644 --- a/apps/sim/lib/selectors/server/providers/http.ts +++ b/apps/sim/lib/selectors/server/providers/http.ts @@ -48,13 +48,17 @@ async function readBoundedBody(response: Response): Promise { } /** - * Host and path of a provider request. The query string is dropped and OData-style path - * arguments (`search(q='...')`) are blanked, since both can carry tokens or a user's search text. + * Host and path of a provider request, with the parts that can carry user data removed: the query + * string (tokens, filters), OData-style path arguments (`search(q='...')` holds search text), and + * any segment holding an email address (a Google calendar id is usually its owner's address). */ function providerEndpoint(input: RequestInfo | URL): string | undefined { try { const url = new URL(input instanceof Request ? input.url : input) - return `${url.host}${url.pathname.replace(/\([^/]*\)/g, '()')}` + const path = url.pathname + .replace(/\([^/]*\)/g, '()') + .replace(/[^/]*(?:@|%40)[^/]*/gi, '{redacted}') + return `${url.host}${path}` } catch { return undefined } diff --git a/apps/sim/lib/table/application/rows.ts b/apps/sim/lib/table/application/rows.ts index d955d5744c4..29c5c93cedf 100644 --- a/apps/sim/lib/table/application/rows.ts +++ b/apps/sim/lib/table/application/rows.ts @@ -506,17 +506,18 @@ async function throwValidationResponse( } /** - * A structured row query cancelled by its own `statement_timeout`: the query shape costs more than - * the table's size allows. Sorting or range-filtering on a stored column reads every row's data, so it is the - * caller's to narrow, and retrying the same query only times out again. + * A structured row query cancelled by its own `statement_timeout`. Sorting, range-filtering, or + * counting on a stored column reads every row's full data, so on a large table the query only + * finishes when that data happens to be cached. The fix is the caller's (narrow the query), so this + * is a 400 with an actionable message rather than an opaque 500. */ function rethrowReadQueryError(error: unknown, tableId: string): never { if (getPostgresCancellationReason(error) === 'statement_timeout') { logger.warn('Table read hit its statement timeout', { tableId }) throw new TableRowsValidationError( `The query took longer than ${READ_STATEMENT_TIMEOUT_MS / 1000} seconds and was stopped. ` + - 'Sorting or range-filtering on a column reads every row in the table, which is too slow ' + - 'at this table size. Sort by createdAt, add an equality filter, or keep ' + + 'Sorting, range-filtering, or counting on a column reads every row in the table, which ' + + 'is too slow at this table size. Sort by createdAt, add an equality filter, or keep ' + 'fewer rows (an Expiration column deletes old rows automatically).', { code: 'TABLE_QUERY_TIMEOUT' } ) From 257eab5fc5ffee17b4752d03a78e7f6d03876056 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 11 Nov 2026 14:49:37 -0800 Subject: [PATCH 2/2] fix(table): keep timeout guidance to sorts and range filters --- apps/sim/lib/table/application/rows.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/sim/lib/table/application/rows.ts b/apps/sim/lib/table/application/rows.ts index 29c5c93cedf..11df8778be5 100644 --- a/apps/sim/lib/table/application/rows.ts +++ b/apps/sim/lib/table/application/rows.ts @@ -506,18 +506,18 @@ async function throwValidationResponse( } /** - * A structured row query cancelled by its own `statement_timeout`. Sorting, range-filtering, or - * counting on a stored column reads every row's full data, so on a large table the query only - * finishes when that data happens to be cached. The fix is the caller's (narrow the query), so this - * is a 400 with an actionable message rather than an opaque 500. + * A structured row query cancelled by its own `statement_timeout`. Sorting or range-filtering on a + * stored column reads every candidate row's full data, so on a large table the query only finishes + * when that data happens to be cached. The fix is the caller's (narrow the query), so this is a 400 + * with an actionable message rather than an opaque 500. */ function rethrowReadQueryError(error: unknown, tableId: string): never { if (getPostgresCancellationReason(error) === 'statement_timeout') { logger.warn('Table read hit its statement timeout', { tableId }) throw new TableRowsValidationError( `The query took longer than ${READ_STATEMENT_TIMEOUT_MS / 1000} seconds and was stopped. ` + - 'Sorting, range-filtering, or counting on a column reads every row in the table, which ' + - 'is too slow at this table size. Sort by createdAt, add an equality filter, or keep ' + + 'Sorting or range-filtering on a column reads every row in the table, which is too slow ' + + 'at this table size. Sort by createdAt, add an equality filter, or keep ' + 'fewer rows (an Expiration column deletes old rows automatically).', { code: 'TABLE_QUERY_TIMEOUT' } )