Migrate console to coreEvents.emitFeedback or debugLogger (#15219)

This commit is contained in:
Adib234
2025-12-29 15:46:10 -05:00
committed by GitHub
parent dcd2449b1a
commit 10ae84869a
66 changed files with 564 additions and 425 deletions
@@ -14,6 +14,7 @@ import {
type HookPolicyDecision,
} from './types.js';
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
import { debugLogger } from '../utils/debugLogger.js';
export class MessageBus extends EventEmitter {
constructor(
@@ -45,7 +46,7 @@ export class MessageBus extends EventEmitter {
async publish(message: Message): Promise<void> {
if (this.debug) {
console.debug(`[MESSAGE_BUS] publish: ${safeJsonStringify(message)}`);
debugLogger.debug(`[MESSAGE_BUS] publish: ${safeJsonStringify(message)}`);
}
try {
if (!this.isValidMessage(message)) {
+1 -1
View File
@@ -30,7 +30,7 @@ const logger = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
debug: (...args: any[]) => debugLogger.debug('[DEBUG] [IDEClient]', ...args),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: (...args: any[]) => console.error('[ERROR] [IDEClient]', ...args),
error: (...args: any[]) => debugLogger.error('[ERROR] [IDEClient]', ...args),
};
export type DiffUpdateResult =
+4 -1
View File
@@ -418,7 +418,10 @@ export class OAuthUtils {
return payload.exp * 1000; // Convert seconds to milliseconds
}
} catch (e) {
console.error('Failed to parse ID token for expiry time with error:', e);
debugLogger.error(
'Failed to parse ID token for expiry time with error:',
e,
);
}
// Return undefined if try block fails or 'exp' is missing/invalid
@@ -324,7 +324,11 @@ export class KeychainTokenStorage
.filter((cred) => cred.account.startsWith(SECRET_PREFIX))
.map((cred) => cred.account.substring(SECRET_PREFIX.length));
} catch (error) {
console.error('Failed to list secrets from keychain:', error);
coreEvents.emitFeedback(
'error',
'Failed to list secrets from keychain',
error,
);
return [];
}
}
+2 -1
View File
@@ -28,6 +28,7 @@ import {
} from '../confirmation-bus/types.js';
import { type MessageBus } from '../confirmation-bus/message-bus.js';
import { coreEvents } from '../utils/events.js';
import { debugLogger } from '../utils/debugLogger.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -312,7 +313,7 @@ export function createPolicyUpdater(
existingData = toml.parse(fileContent) as { rule?: TomlRule[] };
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
console.warn(
debugLogger.warn(
`Failed to parse ${policyFile}, overwriting with new policy.`,
error,
);
@@ -14,6 +14,14 @@ import type {
} from '../routingStrategy.js';
import type { Config } from '../../config/config.js';
import type { BaseLlmClient } from '../../core/baseLlmClient.js';
import { debugLogger } from '../../utils/debugLogger.js';
import { coreEvents } from '../../utils/events.js';
vi.mock('../../utils/debugLogger.js', () => ({
debugLogger: {
warn: vi.fn(),
},
}));
describe('CompositeStrategy', () => {
let mockContext: RoutingContext;
@@ -22,6 +30,7 @@ describe('CompositeStrategy', () => {
let mockStrategy1: RoutingStrategy;
let mockStrategy2: RoutingStrategy;
let mockTerminalStrategy: TerminalStrategy;
let emitFeedbackSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
vi.clearAllMocks();
@@ -30,6 +39,8 @@ describe('CompositeStrategy', () => {
mockConfig = {} as Config;
mockBaseLlmClient = {} as BaseLlmClient;
emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
mockStrategy1 = {
name: 'strategy1',
route: vi.fn().mockResolvedValue(null),
@@ -112,9 +123,6 @@ describe('CompositeStrategy', () => {
});
it('should handle errors in non-terminal strategies and continue', async () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
vi.spyOn(mockStrategy1, 'route').mockRejectedValue(
new Error('Strategy 1 failed'),
);
@@ -130,18 +138,14 @@ describe('CompositeStrategy', () => {
mockBaseLlmClient,
);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(debugLogger.warn).toHaveBeenCalledWith(
"[Routing] Strategy 'strategy1' failed. Continuing to next strategy. Error:",
expect.any(Error),
);
expect(result.model).toBe('terminal-model');
consoleErrorSpy.mockRestore();
});
it('should re-throw an error from the terminal strategy', async () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const terminalError = new Error('Terminal strategy failed');
vi.spyOn(mockTerminalStrategy, 'route').mockRejectedValue(terminalError);
@@ -151,11 +155,11 @@ describe('CompositeStrategy', () => {
composite.route(mockContext, mockConfig, mockBaseLlmClient),
).rejects.toThrow(terminalError);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(emitFeedbackSpy).toHaveBeenCalledWith(
'error',
"[Routing] Critical Error: Terminal strategy 'terminal' failed. Routing cannot proceed. Error:",
terminalError,
);
consoleErrorSpy.mockRestore();
});
it('should correctly finalize the decision metadata', async () => {
@@ -6,6 +6,8 @@
import type { Config } from '../../config/config.js';
import type { BaseLlmClient } from '../../core/baseLlmClient.js';
import { debugLogger } from '../../utils/debugLogger.js';
import { coreEvents } from '../../utils/events.js';
import type {
RoutingContext,
RoutingDecision,
@@ -59,7 +61,7 @@ export class CompositeStrategy implements TerminalStrategy {
return this.finalizeDecision(decision, startTime);
}
} catch (error) {
console.error(
debugLogger.warn(
`[Routing] Strategy '${strategy.name}' failed. Continuing to next strategy. Error:`,
error,
);
@@ -76,7 +78,8 @@ export class CompositeStrategy implements TerminalStrategy {
return this.finalizeDecision(decision, startTime);
} catch (error) {
console.error(
coreEvents.emitFeedback(
'error',
`[Routing] Critical Error: Terminal strategy '${terminalStrategy.name}' failed. Routing cannot proceed. Error:`,
error,
);
@@ -277,7 +277,7 @@ export class ClearcutLogger {
this.enqueueHelper(event);
} catch (error) {
if (this.config?.getDebugMode()) {
console.error('ClearcutLogger: Failed to enqueue log event.', error);
debugLogger.warn('ClearcutLogger: Failed to enqueue log event.', error);
}
}
}
@@ -301,9 +301,7 @@ export class ClearcutLogger {
this.enqueueHelper(event);
});
} catch (error) {
if (this.config?.getDebugMode()) {
console.error('ClearcutLogger: Failed to enqueue log event.', error);
}
debugLogger.warn('ClearcutLogger: Failed to enqueue log event.', error);
}
}
@@ -435,7 +433,7 @@ export class ClearcutLogger {
};
} else {
if (this.config?.getDebugMode()) {
console.error(
debugLogger.warn(
`Error flushing log events: HTTP ${response.status}: ${response.statusText}`,
);
}
@@ -445,7 +443,7 @@ export class ClearcutLogger {
}
} catch (e: unknown) {
if (this.config?.getDebugMode()) {
console.error('Error flushing log events:', e as Error);
debugLogger.warn('Error flushing log events:', e as Error);
}
// Re-queue failed events for retry
+3 -4
View File
@@ -157,7 +157,6 @@ export async function initializeTelemetry(
) {
const message = `Telemetry credentials have changed (from ${activeTelemetryEmail} to ${credentials.client_email}), but telemetry cannot be re-initialized in this process. Please restart the CLI to use the new account for telemetry.`;
debugLogger.error(message);
console.error(message);
}
return;
}
@@ -309,7 +308,7 @@ export async function initializeTelemetry(
initializeMetrics(config);
void flushTelemetryBuffer();
} catch (error) {
console.error('Error starting OpenTelemetry SDK:', error);
debugLogger.error('Error starting OpenTelemetry SDK:', error);
}
// Note: We don't use process.on('exit') here because that callback is synchronous
@@ -343,7 +342,7 @@ export async function flushTelemetry(config: Config): Promise<void> {
debugLogger.log('OpenTelemetry SDK flushed successfully.');
}
} catch (error) {
console.error('Error flushing SDK:', error);
debugLogger.error('Error flushing SDK:', error);
}
}
@@ -361,7 +360,7 @@ export async function shutdownTelemetry(
debugLogger.log('OpenTelemetry SDK shut down successfully.');
}
} catch (error) {
console.error('Error shutting down SDK:', error);
debugLogger.error('Error shutting down SDK:', error);
} finally {
telemetryInitialized = false;
sdk = undefined;
+1 -1
View File
@@ -1159,7 +1159,7 @@ describe('EditTool', () => {
result.returnDisplay.diffStat?.model_removed_lines,
);
} else if (result.error) {
console.error(`Edit failed for ${file.path}:`, result.error);
throw result.error;
}
}
-7
View File
@@ -277,9 +277,6 @@ class MemoryToolInvocation extends BaseToolInvocation<
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.warn(
`[MemoryTool] Error executing save_memory for fact "${fact}": ${errorMessage}`,
);
return {
llmContent: JSON.stringify({
success: false,
@@ -367,10 +364,6 @@ export class MemoryTool
await fsAdapter.writeFile(memoryFilePath, newContent, 'utf-8');
} catch (error) {
console.error(
`[MemoryTool] Error adding memory entry to ${memoryFilePath}:`,
error,
);
throw new Error(
`[MemoryTool] Failed to add memory entry: ${error instanceof Error ? error.message : String(error)}`,
);
+1 -1
View File
@@ -729,7 +729,7 @@ describe('SmartEditTool', () => {
result.returnDisplay.diffStat?.model_removed_lines,
);
} else if (result.error) {
console.error(`Edit failed for ${file.path}:`, result.error);
throw result.error;
}
}
+1 -1
View File
@@ -416,7 +416,7 @@ export class ToolRegistry {
);
}
} catch (e) {
console.error(`Tool discovery command "${discoveryCmd}" failed:`, e);
debugLogger.error(`Tool discovery command "${discoveryCmd}" failed:`, e);
throw e;
}
}
+2 -1
View File
@@ -14,6 +14,7 @@ import { ToolErrorType } from './tool-error.js';
import { getErrorMessage } from '../utils/errors.js';
import { type Config } from '../config/config.js';
import { getResponseText } from '../utils/partUtils.js';
import { debugLogger } from '../utils/debugLogger.js';
interface GroundingChunkWeb {
uri?: string;
@@ -167,7 +168,7 @@ class WebSearchToolInvocation extends BaseToolInvocation<
const errorMessage = `Error during web search for query "${
this.params.query
}": ${getErrorMessage(error)}`;
console.error(errorMessage, error);
debugLogger.warn(errorMessage, error);
return {
llmContent: `Error: ${errorMessage}`,
returnDisplay: `Error performing web search.`,
+2 -1
View File
@@ -44,6 +44,7 @@ import { FileOperation } from '../telemetry/metrics.js';
import { getSpecificMimeType } from '../utils/fileUtils.js';
import { getLanguageFromFilePath } from '../utils/language-detection.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import { debugLogger } from '../utils/debugLogger.js';
/**
* Parameters for the WriteFile tool
@@ -377,7 +378,7 @@ class WriteFileToolInvocation extends BaseToolInvocation<
// Include stack trace in debug mode for better troubleshooting
if (this.config.getDebugMode() && error.stack) {
console.error('Write file error stack:', error.stack);
debugLogger.error('Write file error stack:', error.stack);
}
} else if (error instanceof Error) {
errorMsg = `Error writing to file: ${error.message}`;
+1
View File
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
/* eslint-disable no-console */
import * as fs from 'node:fs';
import * as util from 'node:util';
+5 -4
View File
@@ -22,6 +22,7 @@ import {
} from '../utils/messageInspectors.js';
import * as fs from 'node:fs';
import { promptIdContext } from './promptIdContext.js';
import { debugLogger } from './debugLogger.js';
const CODE_CORRECTION_SYSTEM_PROMPT = `
You are an expert code-editing assistant. Your task is to analyze a failed edit attempt and provide a corrected version of the text snippets.
@@ -434,7 +435,7 @@ Return ONLY the corrected target snippet in the specified JSON format with the k
throw error;
}
console.error(
debugLogger.warn(
'Error during LLM call for old string snippet correction:',
error,
);
@@ -523,7 +524,7 @@ Return ONLY the corrected string in the specified JSON format with the key 'corr
throw error;
}
console.error('Error during LLM call for new_string correction:', error);
debugLogger.warn('Error during LLM call for new_string correction:', error);
return originalNewString;
}
}
@@ -593,7 +594,7 @@ Return ONLY the corrected string in the specified JSON format with the key 'corr
throw error;
}
console.error(
debugLogger.warn(
'Error during LLM call for new_string escaping correction:',
error,
);
@@ -660,7 +661,7 @@ Return ONLY the corrected string in the specified JSON format with the key 'corr
throw error;
}
console.error(
debugLogger.warn(
'Error during LLM call for string escaping correction:',
error,
);
+25 -14
View File
@@ -9,12 +9,13 @@ import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { reportError } from './errorReporting.js';
import { debugLogger } from './debugLogger.js';
// Use a type alias for SpyInstance as it's not directly exported
type SpyInstance = ReturnType<typeof vi.spyOn>;
describe('reportError', () => {
let consoleErrorSpy: SpyInstance;
let debugLoggerErrorSpy: SpyInstance;
let testDir: string;
const MOCK_TIMESTAMP = '2025-01-01T00-00-00-000Z';
@@ -22,7 +23,9 @@ describe('reportError', () => {
// Create a temporary directory for logs
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gemini-report-test-'));
vi.resetAllMocks();
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
debugLoggerErrorSpy = vi
.spyOn(debugLogger, 'error')
.mockImplementation(() => {});
vi.spyOn(Date.prototype, 'toISOString').mockReturnValue(MOCK_TIMESTAMP);
});
@@ -54,9 +57,10 @@ describe('reportError', () => {
context,
});
// Verify the console log
expect(consoleErrorSpy).toHaveBeenCalledWith(
// Verify the user feedback
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
`${baseMessage} Full report available at: ${expectedReportPath}`,
error,
);
});
@@ -75,8 +79,9 @@ describe('reportError', () => {
error: { message: 'Test plain object error' },
});
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
`${baseMessage} Full report available at: ${expectedReportPath}`,
error,
);
});
@@ -95,8 +100,9 @@ describe('reportError', () => {
error: { message: 'Just a string error' },
});
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
`${baseMessage} Full report available at: ${expectedReportPath}`,
error,
);
});
@@ -109,15 +115,18 @@ describe('reportError', () => {
await reportError(error, baseMessage, context, type, nonExistentDir);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
`${baseMessage} Additionally, failed to write detailed error report:`,
expect.any(Error), // The actual write error
);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
'Original error that triggered report generation:',
error,
);
expect(consoleErrorSpy).toHaveBeenCalledWith('Original context:', context);
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
'Original context:',
context,
);
});
it('should handle stringification failure of report content (e.g. BigInt in context)', async () => {
@@ -146,15 +155,15 @@ describe('reportError', () => {
await reportError(error, baseMessage, context, type, testDir);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
`${baseMessage} Could not stringify report content (likely due to context):`,
stringifyError,
);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
'Original error that triggered report generation:',
error,
);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
'Original context could not be stringified or included in report.',
);
@@ -165,8 +174,9 @@ describe('reportError', () => {
error: { message: error.message, stack: error.stack },
});
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
`${baseMessage} Partial report (excluding context) available at: ${expectedMinimalReportPath}`,
error,
);
});
@@ -186,8 +196,9 @@ describe('reportError', () => {
error: { message: 'Error without context', stack: 'No context stack' },
});
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
`${baseMessage} Full report available at: ${expectedReportPath}`,
error,
);
});
});
+26 -12
View File
@@ -8,6 +8,7 @@ import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import type { Content } from '@google/genai';
import { debugLogger } from './debugLogger.js';
interface ErrorReportData {
error: { message: string; stack?: string } | { message: string };
@@ -16,7 +17,7 @@ interface ErrorReportData {
}
/**
* Generates an error report, writes it to a temporary file, and logs information to console.error.
* Generates an error report, writes it to a temporary file, and logs information to user
* @param error The error object.
* @param context The relevant context (e.g., chat history, request contents).
* @param type A string to identify the type of error (e.g., 'startChat', 'generateJson-api').
@@ -59,13 +60,16 @@ export async function reportError(
stringifiedReportContent = JSON.stringify(reportContent, null, 2);
} catch (stringifyError) {
// This can happen if context contains something like BigInt
console.error(
debugLogger.error(
`${baseMessage} Could not stringify report content (likely due to context):`,
stringifyError,
);
console.error('Original error that triggered report generation:', error);
debugLogger.error(
'Original error that triggered report generation:',
error,
);
if (context) {
console.error(
debugLogger.error(
'Original context could not be stringified or included in report.',
);
}
@@ -75,11 +79,12 @@ export async function reportError(
stringifiedReportContent = JSON.stringify(minimalReportContent, null, 2);
// Still try to write the minimal report
await fs.writeFile(reportPath, stringifiedReportContent);
console.error(
debugLogger.error(
`${baseMessage} Partial report (excluding context) available at: ${reportPath}`,
error,
);
} catch (minimalWriteError) {
console.error(
debugLogger.error(
`${baseMessage} Failed to write even a minimal error report:`,
minimalWriteError,
);
@@ -89,28 +94,37 @@ export async function reportError(
try {
await fs.writeFile(reportPath, stringifiedReportContent);
console.error(`${baseMessage} Full report available at: ${reportPath}`);
debugLogger.error(
`${baseMessage} Full report available at: ${reportPath}`,
error,
);
} catch (writeError) {
console.error(
debugLogger.error(
`${baseMessage} Additionally, failed to write detailed error report:`,
writeError,
);
// Log the original error as a fallback if report writing fails
console.error('Original error that triggered report generation:', error);
debugLogger.error(
'Original error that triggered report generation:',
error,
);
if (context) {
// Context was stringifiable, but writing the file failed.
// We already have stringifiedReportContent, but it might be too large for console.
// So, we try to log the original context object, and if that fails, its stringified version (truncated).
try {
console.error('Original context:', context);
debugLogger.error('Original context:', context);
} catch {
try {
console.error(
debugLogger.error(
'Original context (stringified, truncated):',
JSON.stringify(context).substring(0, 1000),
);
} catch {
console.error('Original context could not be logged or stringified.');
debugLogger.error(
'Original context could not be logged or stringified.',
);
}
}
}
@@ -342,7 +342,10 @@ export async function getFolderStructure(
return `${summary}\n\n${resolvedPath}${path.sep}\n${structureLines.join('\n')}`;
} catch (error: unknown) {
console.error(`Error getting folder structure for ${resolvedPath}:`, error);
debugLogger.warn(
`Error getting folder structure for ${resolvedPath}:`,
error,
);
return `Error processing directory "${resolvedPath}": ${getErrorMessage(error)}`;
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ const logger = {
debugLogger.warn('[WARN] [MemoryDiscovery]', ...args),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: (...args: any[]) =>
console.error('[ERROR] [MemoryDiscovery]', ...args),
debugLogger.error('[ERROR] [MemoryDiscovery]', ...args),
};
export interface GeminiFileContent {
+4 -4
View File
@@ -232,7 +232,7 @@ export async function retryWithBackoff<T>(
continue;
}
} catch (fallbackError) {
console.warn('Model fallback failed:', fallbackError);
debugLogger.warn('Model fallback failed:', fallbackError);
}
}
throw classifiedError instanceof RetryableQuotaError
@@ -241,7 +241,7 @@ export async function retryWithBackoff<T>(
}
if (classifiedError instanceof RetryableQuotaError) {
console.warn(
debugLogger.warn(
`Attempt ${attempt} failed: ${classifiedError.message}. Retrying after ${classifiedError.retryDelayMs}ms...`,
);
await delay(classifiedError.retryDelayMs, signal);
@@ -300,7 +300,7 @@ function logRetryAttempt(
if (errorStatus === 429) {
debugLogger.warn(message, error);
} else if (errorStatus && errorStatus >= 500 && errorStatus < 600) {
console.error(message, error);
debugLogger.warn(message, error);
} else if (error instanceof Error) {
// Fallback for errors that might not have a status but have a message
if (error.message.includes('429')) {
@@ -309,7 +309,7 @@ function logRetryAttempt(
error,
);
} else if (error.message.match(/5\d{2}/)) {
console.error(
debugLogger.warn(
`Attempt ${attempt} failed with 5xx error. Retrying with backoff...`,
error,
);
+3 -2
View File
@@ -19,6 +19,7 @@ import type {
ResolvedModelConfig,
} from '../services/modelConfigService.js';
import { DEFAULT_GEMINI_MODEL } from '../config/models.js';
import { debugLogger } from './debugLogger.js';
// Mock GeminiClient and Config constructor
vi.mock('../core/client.js');
@@ -58,11 +59,11 @@ describe('summarizers', () => {
(mockGeminiClient.generateContent as Mock) = vi.fn();
vi.spyOn(console, 'error').mockImplementation(() => {});
vi.spyOn(debugLogger, 'warn').mockImplementation(() => {});
});
afterEach(() => {
vi.clearAllMocks();
(console.error as Mock).mockRestore();
vi.restoreAllMocks();
});
describe('summarizeToolOutput', () => {