Skip to content

Commit c747151

Browse files
committed
fix(logs): attribute a revoked OAuth credential to its owner
A CredentialRevokedError anywhere in the cause chain classifies as a user failure, so the boundaries that log it after token resolution's WARN write INFO rather than ERROR.
1 parent 9b1b3bc commit c747151

2 files changed

Lines changed: 12 additions & 3 deletions

File tree

‎apps/sim/lib/core/errors/failure-log.test.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from '@/lib/core/errors/failure-log'
1111
import { RetryableSetupError } from '@/lib/core/errors/retryable-infrastructure'
1212
import { UserFailure } from '@/lib/core/errors/user-failure'
13+
import { CredentialRevokedError } from '@/lib/oauth/credential-revoked'
1314
import { HostedKeyRateLimitedError, HostedKeyUnavailableError } from '@/tools/errors'
1415

1516
const logger = createLogger('FailureLogTest')
@@ -56,6 +57,11 @@ describe('classifyFailure', () => {
5657
expect(classifyFailure(upstream)).toBe('third_party_client')
5758
})
5859

60+
it('attributes a revoked OAuth credential to its owner, not to Sim', () => {
61+
const revoked = new CredentialRevokedError('Reconnect your account')
62+
expect(classifyFailure(new Error('Tool failed', { cause: revoked }))).toBe('user')
63+
})
64+
5965
it('leaves an unattributed failure internal', () => {
6066
expect(classifyFailure(new Error('something broke'))).toBe('internal')
6167
expect(classifyFailure('a thrown string')).toBe('internal')

‎apps/sim/lib/core/errors/failure-log.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { findDatabaseQueryError } from '@/lib/core/errors/database-query-error'
33
import { isRetryableSetupError } from '@/lib/core/errors/retryable-infrastructure'
44
import { UserFailure } from '@/lib/core/errors/user-failure'
55
import { HttpError } from '@/lib/core/utils/http-error'
6+
import { CredentialRevokedError } from '@/lib/oauth/credential-revoked'
67

78
/**
89
* Who a failure is attributable to, which decides how loudly the server logs it. The user
@@ -58,8 +59,9 @@ export function markFailureKind<T>(error: T, kind: FailureKind): T {
5859

5960
/**
6061
* Attributes `error` from its cause chain. A database or retryable setup failure is always
61-
* internal, then the outermost link with an explicit mark, a {@link UserFailure}, a Sim `HttpError`
62-
* status, or an upstream `status` decides. Anything unattributed is internal.
62+
* internal, then the outermost link with an explicit mark, a {@link UserFailure} or revoked
63+
* credential, a Sim `HttpError` status, or an upstream `status` decides. Anything unattributed is
64+
* internal.
6365
*/
6466
export function classifyFailure(error: unknown): FailureKind {
6567
if (findDatabaseQueryError(error)) return 'internal'
@@ -69,7 +71,8 @@ export function classifyFailure(error: unknown): FailureKind {
6971
for (const link of chain) {
7072
const marked = failureKinds.get(link)
7173
if (marked) return marked
72-
if (link instanceof UserFailure) return 'user'
74+
/** Only the credential's owner reconnecting restores a revoked grant. */
75+
if (link instanceof UserFailure || link instanceof CredentialRevokedError) return 'user'
7376

7477
if (link instanceof HttpError) {
7578
return link.statusCode >= 400 && link.statusCode < 500 ? 'user' : 'internal'

0 commit comments

Comments
 (0)