diff --git a/packages/core/src/core/loggingContentGenerator.test.ts b/packages/core/src/core/loggingContentGenerator.test.ts index dd354fa16f..45e5028553 100644 --- a/packages/core/src/core/loggingContentGenerator.test.ts +++ b/packages/core/src/core/loggingContentGenerator.test.ts @@ -32,6 +32,7 @@ import { LoggingContentGenerator } from './loggingContentGenerator.js'; import type { Config } from '../config/config.js'; import { UserTierId } from '../code_assist/types.js'; import { ApiRequestEvent, LlmRole } from '../telemetry/types.js'; +import { FatalAuthenticationError } from '../utils/errors.js'; describe('LoggingContentGenerator', () => { let wrapped: ContentGenerator; @@ -137,6 +138,19 @@ describe('LoggingContentGenerator', () => { const errorEvent = vi.mocked(logApiError).mock.calls[0][1]; expect(errorEvent.duration_ms).toBe(1000); }); + + describe('error type extraction', () => { + it('should extract error type correctly', async () => { + const req = { contents: [], model: 'm' }; + const error = new FatalAuthenticationError('test'); + vi.mocked(wrapped.generateContent).mockRejectedValue(error); + await expect( + loggingContentGenerator.generateContent(req, 'id', LlmRole.MAIN), + ).rejects.toThrow(); + const errorEvent = vi.mocked(logApiError).mock.calls[0][1]; + expect(errorEvent.error_type).toBe('FatalAuthenticationError'); + }); + }); }); describe('generateContentStream', () => { diff --git a/packages/core/src/core/loggingContentGenerator.ts b/packages/core/src/core/loggingContentGenerator.ts index 12a1722475..1544087ae0 100644 --- a/packages/core/src/core/loggingContentGenerator.ts +++ b/packages/core/src/core/loggingContentGenerator.ts @@ -36,6 +36,7 @@ import { toContents } from '../code_assist/converter.js'; import { isStructuredError } from '../utils/quotaErrorDetection.js'; import { runInDevTraceSpan, type SpanMetadata } from '../telemetry/trace.js'; import { debugLogger } from '../utils/debugLogger.js'; +import { getErrorType } from '../utils/errors.js'; interface StructuredError { status: number; @@ -167,7 +168,7 @@ export class LoggingContentGenerator implements ContentGenerator { serverDetails?: ServerDetails, ): void { const errorMessage = error instanceof Error ? error.message : String(error); - const errorType = error instanceof Error ? error.name : 'unknown'; + const errorType = getErrorType(error); logApiError( this.config, diff --git a/packages/core/src/utils/errors.test.ts b/packages/core/src/utils/errors.test.ts index 58c7004190..b3df89ef93 100644 --- a/packages/core/src/utils/errors.test.ts +++ b/packages/core/src/utils/errors.test.ts @@ -12,6 +12,14 @@ import { BadRequestError, ForbiddenError, getErrorMessage, + getErrorType, + FatalAuthenticationError, + FatalCancellationError, + FatalInputError, + FatalSandboxError, + FatalConfigError, + FatalTurnLimitedError, + FatalToolExecutionError, } from './errors.js'; describe('getErrorMessage', () => { @@ -201,3 +209,44 @@ describe('toFriendlyError', () => { expect(toFriendlyError(error)).toBe(error); }); }); + +describe('getErrorType', () => { + it('should return error name for standard errors', () => { + expect(getErrorType(new Error('test'))).toBe('Error'); + expect(getErrorType(new TypeError('test'))).toBe('TypeError'); + expect(getErrorType(new SyntaxError('test'))).toBe('SyntaxError'); + }); + + it('should return constructor name for custom errors', () => { + expect(getErrorType(new FatalAuthenticationError('test'))).toBe( + 'FatalAuthenticationError', + ); + expect(getErrorType(new FatalInputError('test'))).toBe('FatalInputError'); + expect(getErrorType(new FatalSandboxError('test'))).toBe( + 'FatalSandboxError', + ); + expect(getErrorType(new FatalConfigError('test'))).toBe('FatalConfigError'); + expect(getErrorType(new FatalTurnLimitedError('test'))).toBe( + 'FatalTurnLimitedError', + ); + expect(getErrorType(new FatalToolExecutionError('test'))).toBe( + 'FatalToolExecutionError', + ); + expect(getErrorType(new FatalCancellationError('test'))).toBe( + 'FatalCancellationError', + ); + expect(getErrorType(new ForbiddenError('test'))).toBe('ForbiddenError'); + expect(getErrorType(new UnauthorizedError('test'))).toBe( + 'UnauthorizedError', + ); + expect(getErrorType(new BadRequestError('test'))).toBe('BadRequestError'); + }); + + it('should return "unknown" for non-Error objects', () => { + expect(getErrorType('string error')).toBe('unknown'); + expect(getErrorType(123)).toBe('unknown'); + expect(getErrorType({})).toBe('unknown'); + expect(getErrorType(null)).toBe('unknown'); + expect(getErrorType(undefined)).toBe('unknown'); + }); +}); diff --git a/packages/core/src/utils/errors.ts b/packages/core/src/utils/errors.ts index 2bba4f8abe..dc7d5b33c5 100644 --- a/packages/core/src/utils/errors.ts +++ b/packages/core/src/utils/errors.ts @@ -26,6 +26,15 @@ export function getErrorMessage(error: unknown): string { } } +export function getErrorType(error: unknown): string { + if (!(error instanceof Error)) return 'unknown'; + + // Return constructor name if the generic 'Error' name is used (for custom errors) + return error.name === 'Error' + ? (error.constructor?.name ?? 'Error') + : error.name; +} + export class FatalError extends Error { constructor( message: string,