From cab7da74a1d85e9b0b4bcd46e91b6a6be82d28af Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Thu, 26 Mar 2026 16:16:43 -0700 Subject: [PATCH] Improve error classifier with additional classification logic and tests --- src/common/telemetry/errorClassifier.ts | 50 ++++++++++++++-- .../telemetry/errorClassifier.unit.test.ts | 57 +++++++++++++++++++ 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/src/common/telemetry/errorClassifier.ts b/src/common/telemetry/errorClassifier.ts index 55daf5a3..0fd012cb 100644 --- a/src/common/telemetry/errorClassifier.ts +++ b/src/common/telemetry/errorClassifier.ts @@ -4,9 +4,13 @@ import { RpcTimeoutError } from '../../managers/common/nativePythonFinder'; export type DiscoveryErrorType = | 'spawn_timeout' | 'spawn_enoent' + | 'spawn_error' | 'permission_denied' | 'canceled' | 'parse_error' + | 'pet_crash' + | 'pet_not_found' + | 'tool_exec_failed' | 'unknown'; /** @@ -35,12 +39,50 @@ export function classifyError(ex: unknown): DiscoveryErrorType { return 'permission_denied'; } - // Check message patterns - const msg = ex.message.toLowerCase(); - if (msg.includes('timed out') || msg.includes('timeout')) { + const msg = ex.message; + const msgLower = msg.toLowerCase(); + + // PET process failures (crash, restart exhaustion, stdio failure) + if ( + msgLower.includes('python environment tools (pet)') || + msgLower.includes('failed to create stdio streams for pet') + ) { + return 'pet_crash'; + } + + // Missing PET binary / Python extension not found + if (msgLower.includes('python extension not found')) { + return 'pet_not_found'; + } + + // Wrapped spawn errors from condaUtils / other managers (e.g. "Error spawning conda: spawn conda ENOENT") + if (msgLower.includes('error spawning')) { + if (msgLower.includes('enoent')) { + return 'spawn_enoent'; + } + if (msgLower.includes('eacces') || msgLower.includes('eperm')) { + return 'permission_denied'; + } + return 'spawn_error'; + } + + // Non-zero exit code failures (e.g. "Failed to run "conda info --envs --json":\n ...") + if (msgLower.includes('failed to run')) { + return 'tool_exec_failed'; + } + + // Check message patterns for timeouts + if (msgLower.includes('timed out') || msgLower.includes('timeout')) { return 'spawn_timeout'; } - if (msg.includes('parse') || msg.includes('unexpected token') || msg.includes('json')) { + + // Parse / JSON errors (including "conda info returned invalid data type") + if ( + msgLower.includes('parse') || + msgLower.includes('unexpected token') || + msgLower.includes('json') || + msgLower.includes('invalid data type') + ) { return 'parse_error'; } diff --git a/src/test/common/telemetry/errorClassifier.unit.test.ts b/src/test/common/telemetry/errorClassifier.unit.test.ts index ba04a3ff..0a732e69 100644 --- a/src/test/common/telemetry/errorClassifier.unit.test.ts +++ b/src/test/common/telemetry/errorClassifier.unit.test.ts @@ -64,5 +64,62 @@ suite('Error Classifier', () => { test('should classify unrecognized errors as unknown', () => { assert.strictEqual(classifyError(new Error('something went wrong')), 'unknown'); }); + + test('should classify PET restart failure as pet_crash', () => { + assert.strictEqual( + classifyError( + new Error( + 'Python Environment Tools (PET) failed after 3 restart attempts. Check the Output panel.', + ), + ), + 'pet_crash', + ); + }); + + test('should classify PET currently restarting as pet_crash', () => { + assert.strictEqual( + classifyError(new Error('Python Environment Tools (PET) is currently restarting. Please try again.')), + 'pet_crash', + ); + }); + + test('should classify PET stdio failure as pet_crash', () => { + assert.strictEqual(classifyError(new Error('Failed to create stdio streams for PET process')), 'pet_crash'); + }); + + test('should classify missing PET binary as pet_not_found', () => { + assert.strictEqual(classifyError(new Error('Python extension not found')), 'pet_not_found'); + }); + + test('should classify wrapped spawn ENOENT as spawn_enoent', () => { + assert.strictEqual(classifyError(new Error('Error spawning conda: spawn conda ENOENT')), 'spawn_enoent'); + }); + + test('should classify wrapped spawn EACCES as permission_denied', () => { + assert.strictEqual( + classifyError(new Error('Error spawning python: spawn python EACCES')), + 'permission_denied', + ); + }); + + test('should classify wrapped spawn with other cause as spawn_error', () => { + assert.strictEqual(classifyError(new Error('Error spawning uv: some unexpected failure')), 'spawn_error'); + }); + + test('should classify non-zero exit code failures as tool_exec_failed', () => { + assert.strictEqual( + classifyError(new Error('Failed to run "conda info --envs --json":\n conda not initialized')), + 'tool_exec_failed', + ); + assert.strictEqual(classifyError(new Error('Failed to run uv pip install numpy')), 'tool_exec_failed'); + assert.strictEqual(classifyError(new Error('Failed to run poetry install')), 'tool_exec_failed'); + }); + + test('should classify invalid data type errors as parse_error', () => { + assert.strictEqual( + classifyError(new Error('conda info returned invalid data type: string')), + 'parse_error', + ); + }); }); });