Skip to content

Commit 664ca82

Browse files
authored
fix(search): preserve Google sync diagnostics and retry transient failures (#7895)
* fix(search): preserve Google sync diagnostics and retry transient failures * fix(search): sanitize in-process database failure logs
1 parent 294de5f commit 664ca82

25 files changed

Lines changed: 756 additions & 266 deletions

‎apps/docs/content/docs/search/gmail.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ Updates, removals, and access refresh in the background. Empty mailboxes and fil
139139

140140
## Troubleshooting
141141

142+
An individual thread failure does not mean the whole mailbox failed. Sim retries temporary server and rate-limit errors with bounded backoff. Error diagnostics record the Google API operation, HTTP status, and a recognized error reason when available.
143+
142144
| What you see | What to do |
143145
| --- | --- |
144146
| A different email is requested | Use the Google account matching your verified Sim email. A separate personal account or alias does not satisfy the match. |

‎apps/docs/content/docs/search/google-calendar.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ Search schedules syncs hourly. Event edits, cancellations, access changes, inact
135135

136136
## Troubleshooting
137137

138+
When a sync fails, **Sync history** includes the Google API operation, HTTP status, and a recognized reason when available. A `403` alone does not establish a missing scope: `rateLimitExceeded` and `userRateLimitExceeded` are retried with bounded backoff. A persistent access error requires checking the affected user’s Calendar access.
139+
138140
| What you see | What to do |
139141
| --- | --- |
140142
| No events | Check the date range, search query, and calendar IDs. Use an empty calendar selection or `primary` for each person's own calendar. |

‎apps/docs/content/docs/search/google-drive.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ Search schedules syncs hourly. Central crawls revisit the selected users' files
155155

156156
## Troubleshooting
157157

158+
**Directory permission sync failed** means Sim could not fully verify group membership. Check the Directory administrator’s access to the affected group and any nested groups; this is separate from file-download access. An incomplete membership read does not replace the last verified membership, which remains subject to freshness checks.
159+
158160
| Problem | Next step |
159161
| --- | --- |
160162
| Google rejects authorization (`unauthorized_client`) | In **Manage Domain Wide Delegation**, verify the numeric **Client ID** matches `client_id` in the JSON key uploaded to Sim and all required scopes appear under **View details**. Check pending approval and allow time for recent changes to propagate. Changing the OAuth consent screen alone does not authorize delegation. |

‎apps/sim/background/knowledge-processing.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/**
22
* @vitest-environment node
33
*/
4+
import { DrizzleQueryError } from 'drizzle-orm/errors'
45
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
56

67
const {
@@ -566,6 +567,22 @@ describe('knowledge processing worker', () => {
566567
expect(mockTrigger).not.toHaveBeenCalled()
567568
})
568569

570+
it('keeps database failures retryable without sending SQL or parameters to Trigger', async () => {
571+
const error = new DrizzleQueryError(
572+
'insert private SQL',
573+
['private bound content'],
574+
Object.assign(new Error('private database detail'), { code: '57014' })
575+
)
576+
mockProcessDocumentAsync.mockRejectedValueOnce(error)
577+
const failure = await runDocumentProcessing(WORKSPACE_PAYLOAD).catch(
578+
(caught: unknown) => caught
579+
)
580+
expect(failure).toBeInstanceOf(Error)
581+
expect(failure).toMatchObject({ message: 'Database request failed (SQLSTATE 57014).' })
582+
expect(failure).not.toHaveProperty('cause')
583+
expect(JSON.stringify(failure)).not.toContain('private')
584+
})
585+
569586
it('retries failed provider continuation dispatch instead of reporting a successful deferral', async () => {
570587
const error = new Error('Trigger dispatch unavailable')
571588
mockTrigger.mockRejectedValue(error)

‎apps/sim/background/knowledge-processing.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
isBYOKEmbeddingCredentialRejection,
88
isEmbeddingQuotaExhaustion,
99
} from '@/lib/embeddings'
10+
import { getConnectorFailureDiagnostic } from '@/lib/knowledge/connectors/connector-error'
1011
import {
1112
getOcrRequestRejection,
1213
isPermanentDocumentProcessingError,
@@ -194,7 +195,12 @@ export async function runDocumentProcessing(
194195
processingTime: Date.now() - startedAt,
195196
}
196197
}
197-
logger.error(`[${requestId}] Failed to process document: ${docData.filename}`, error)
198+
const diagnostic = getConnectorFailureDiagnostic(error)
199+
logger.error(
200+
`[${requestId}] Failed to process document: ${docData.filename}`,
201+
diagnostic ?? error
202+
)
203+
if (diagnostic?.category === 'database') throw new Error(diagnostic.message)
198204
throw error
199205
}
200206
}

‎apps/sim/connectors/gmail/company-crawl.test.ts‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@ const { fetchProvider, listUsers, getUser } = vi.hoisted(() => ({
99
getUser: vi.fn(),
1010
}))
1111

12-
vi.mock('@/lib/knowledge/documents/utils', () => ({ VALIDATE_RETRY_OPTIONS: {} }))
13-
vi.mock('@/lib/knowledge/documents/secure-fetch.server', () => ({ fetchWithRetry: fetchProvider }))
12+
vi.mock('@/lib/knowledge/documents/secure-fetch.server', () => ({
13+
fetchWithRetry: (
14+
url: string,
15+
init: RequestInit,
16+
options?: import('@/lib/knowledge/documents/utils').RetryOptions
17+
) => (options?.fetcher ? options.fetcher(url, init, fetchProvider) : fetchProvider(url, init)),
18+
}))
1419
vi.mock('@/components/icons', () => ({ GmailIcon: () => null }))
1520
vi.mock('@/connectors/google-workspace/users', () => ({
1621
GOOGLE_WORKSPACE_USERS_PAGE_SIZE: 100,
@@ -180,8 +185,7 @@ describe('company-wide Gmail indexing', () => {
180185
).resolves.toEqual({ valid: true })
181186
expect(fetchProvider).toHaveBeenCalledWith(
182187
expect.stringContaining('/profile'),
183-
expect.objectContaining({ signal: controller.signal }),
184-
expect.any(Object)
188+
expect.objectContaining({ signal: controller.signal })
185189
)
186190
})
187191

‎apps/sim/connectors/gmail/gmail.test.ts‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
55

66
const { mockFetchWithRetry } = vi.hoisted(() => ({ mockFetchWithRetry: vi.fn() }))
77

8-
vi.mock('@/lib/knowledge/documents/utils', () => ({ VALIDATE_RETRY_OPTIONS: {} }))
98
vi.mock('@/lib/knowledge/documents/secure-fetch.server', () => ({
10-
fetchWithRetry: mockFetchWithRetry,
9+
fetchWithRetry: (
10+
url: string,
11+
init: RequestInit,
12+
options: {
13+
fetcher?: (url: string, init: RequestInit, transport: typeof fetch) => Promise<Response>
14+
}
15+
) =>
16+
options.fetcher
17+
? options.fetcher(url, init, mockFetchWithRetry)
18+
: mockFetchWithRetry(url, init),
1119
}))
1220
vi.mock('@/components/icons', () => ({ GmailIcon: () => null }))
1321
vi.mock('@/lib/knowledge/documents/service', () => ({
@@ -618,7 +626,9 @@ describe('Gmail separately stored message bodies', () => {
618626
.catch((caught: unknown) => caught)
619627

620628
expect(error).toBeInstanceOf(Error)
621-
expect(error).toMatchObject({ message: `Failed to fetch Gmail message body: ${status}` })
629+
expect(error).toMatchObject({
630+
message: `gmail.messages.attachments.get failed (HTTP ${status}).`,
631+
})
622632
expect(gmailConnector.isCredentialInvalidError?.(error)).toBe(status === 401)
623633
}
624634
)
@@ -899,7 +909,7 @@ describe('Gmail thread revisions and deferred content', () => {
899909
.mockResolvedValueOnce(Response.json({ threads: [{ id: 'thread-1' }] }))
900910
.mockResolvedValueOnce(new Response(null, { status }))
901911
await expect(gmailConnector.listDocuments('token', {}, undefined, {})).rejects.toThrow(
902-
`Failed to fetch thread thread-1: ${status}`
912+
`gmail.threads.get failed (HTTP ${status}).`
903913
)
904914
}
905915
)

0 commit comments

Comments
 (0)