From 0da41ea17c55925507725938553fb11aaaca22ec Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Fri, 27 Mar 2026 09:53:30 -0700 Subject: [PATCH 1/2] add more error types --- src/common/telemetry/errorClassifier.ts | 43 ++++++++++++- .../telemetry/errorClassifier.unit.test.ts | 61 +++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/common/telemetry/errorClassifier.ts b/src/common/telemetry/errorClassifier.ts index 55daf5a3..ba9106a5 100644 --- a/src/common/telemetry/errorClassifier.ts +++ b/src/common/telemetry/errorClassifier.ts @@ -1,5 +1,7 @@ import { CancellationError } from 'vscode'; +import * as rpc from 'vscode-jsonrpc/node'; import { RpcTimeoutError } from '../../managers/common/nativePythonFinder'; +import { BaseError } from '../errors/types'; export type DiscoveryErrorType = | 'spawn_timeout' @@ -7,6 +9,12 @@ export type DiscoveryErrorType = | 'permission_denied' | 'canceled' | 'parse_error' + | 'tool_not_found' + | 'command_failed' + | 'connection_error' + | 'rpc_error' + | 'process_crash' + | 'already_registered' | 'unknown'; /** @@ -22,6 +30,21 @@ export function classifyError(ex: unknown): DiscoveryErrorType { return 'spawn_timeout'; } + // JSON-RPC connection errors (e.g., PET process died mid-request, connection closed/disposed) + if (ex instanceof rpc.ConnectionError) { + return 'connection_error'; + } + + // JSON-RPC response errors (PET returned an error response, e.g., internal error) + if (ex instanceof rpc.ResponseError) { + return 'rpc_error'; + } + + // BaseError subclasses: EnvironmentManagerAlreadyRegisteredError, PackageManagerAlreadyRegisteredError + if (ex instanceof BaseError) { + return 'already_registered'; + } + if (!(ex instanceof Error)) { return 'unknown'; } @@ -35,7 +58,7 @@ export function classifyError(ex: unknown): DiscoveryErrorType { return 'permission_denied'; } - // Check message patterns + // Check message patterns (order matters — more specific patterns first) const msg = ex.message.toLowerCase(); if (msg.includes('timed out') || msg.includes('timeout')) { return 'spawn_timeout'; @@ -44,6 +67,24 @@ export function classifyError(ex: unknown): DiscoveryErrorType { return 'parse_error'; } + // Tool/executable not found — e.g., "Conda not found", "Python extension not found", + // "Poetry executable not found" + if (msg.includes('not found')) { + return 'tool_not_found'; + } + + // CLI command execution failures — e.g., 'Failed to run "conda ..."', + // "Failed to run poetry ...", "Error spawning conda: ..." + if (msg.includes('failed to run') || msg.includes('error spawning')) { + return 'command_failed'; + } + + // PET process crash/hang recovery failures — e.g., "PET is currently restarting", + // "failed after 3 restart attempts", "Failed to create stdio streams for PET process" + if (msg.includes('restart') || msg.includes('stdio stream')) { + return 'process_crash'; + } + // Check error name for cancellation variants if (ex.name === 'CancellationError' || ex.name === 'AbortError') { return 'canceled'; diff --git a/src/test/common/telemetry/errorClassifier.unit.test.ts b/src/test/common/telemetry/errorClassifier.unit.test.ts index ba04a3ff..dfd3e25a 100644 --- a/src/test/common/telemetry/errorClassifier.unit.test.ts +++ b/src/test/common/telemetry/errorClassifier.unit.test.ts @@ -1,5 +1,7 @@ import assert from 'node:assert'; import { CancellationError } from 'vscode'; +import * as rpc from 'vscode-jsonrpc/node'; +import { BaseError } from '../../../common/errors/types'; import { classifyError } from '../../../common/telemetry/errorClassifier'; import { RpcTimeoutError } from '../../../managers/common/nativePythonFinder'; @@ -64,5 +66,64 @@ suite('Error Classifier', () => { test('should classify unrecognized errors as unknown', () => { assert.strictEqual(classifyError(new Error('something went wrong')), 'unknown'); }); + + test('should classify ConnectionError as connection_error', () => { + assert.strictEqual( + classifyError(new rpc.ConnectionError(rpc.ConnectionErrors.Closed, 'Connection closed')), + 'connection_error', + ); + assert.strictEqual( + classifyError(new rpc.ConnectionError(rpc.ConnectionErrors.Disposed, 'Connection disposed')), + 'connection_error', + ); + }); + + test('should classify ResponseError as rpc_error', () => { + assert.strictEqual(classifyError(new rpc.ResponseError(-32600, 'Invalid request')), 'rpc_error'); + assert.strictEqual(classifyError(new rpc.ResponseError(-32601, 'Method not found')), 'rpc_error'); + }); + + test('should classify BaseError subclasses as already_registered', () => { + // Using a concrete subclass to test (BaseError is abstract) + class TestRegisteredError extends BaseError { + constructor(message: string) { + super('InvalidArgument', message); + } + } + assert.strictEqual( + classifyError(new TestRegisteredError('Environment manager with id test already registered')), + 'already_registered', + ); + }); + + test('should classify "not found" messages as tool_not_found', () => { + assert.strictEqual(classifyError(new Error('Conda not found')), 'tool_not_found'); + assert.strictEqual(classifyError(new Error('Python extension not found')), 'tool_not_found'); + assert.strictEqual(classifyError(new Error('Poetry executable not found')), 'tool_not_found'); + }); + + test('should classify command execution failures as command_failed', () => { + assert.strictEqual( + classifyError(new Error('Failed to run "conda info --envs --json":\n some error')), + 'command_failed', + ); + assert.strictEqual(classifyError(new Error('Failed to run poetry install')), 'command_failed'); + assert.strictEqual(classifyError(new Error('Error spawning conda: ENOENT')), 'command_failed'); + }); + + test('should classify PET process crash/restart errors as process_crash', () => { + assert.strictEqual( + classifyError(new Error('Python Environment Tools (PET) is currently restarting. Please try again.')), + 'process_crash', + ); + assert.strictEqual( + classifyError(new Error('Python Environment Tools (PET) failed after 3 restart attempts.')), + 'process_crash', + ); + assert.strictEqual( + classifyError(new Error('Failed to create stdio streams for PET process')), + 'process_crash', + ); + }); }); }); From cc7a789bf8e813aca8bdf942d4a196beaf48caf9 Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Fri, 27 Mar 2026 14:31:11 -0700 Subject: [PATCH 2/2] fix test --- src/common/telemetry/errorClassifier.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/common/telemetry/errorClassifier.ts b/src/common/telemetry/errorClassifier.ts index ba9106a5..910c179e 100644 --- a/src/common/telemetry/errorClassifier.ts +++ b/src/common/telemetry/errorClassifier.ts @@ -63,6 +63,13 @@ export function classifyError(ex: unknown): DiscoveryErrorType { if (msg.includes('timed out') || msg.includes('timeout')) { return 'spawn_timeout'; } + + // CLI command execution failures — checked before parse_error because command args + // may contain words like "json" (e.g., 'Failed to run "conda info --envs --json"') + if (msg.includes('failed to run') || msg.includes('error spawning')) { + return 'command_failed'; + } + if (msg.includes('parse') || msg.includes('unexpected token') || msg.includes('json')) { return 'parse_error'; } @@ -73,12 +80,6 @@ export function classifyError(ex: unknown): DiscoveryErrorType { return 'tool_not_found'; } - // CLI command execution failures — e.g., 'Failed to run "conda ..."', - // "Failed to run poetry ...", "Error spawning conda: ..." - if (msg.includes('failed to run') || msg.includes('error spawning')) { - return 'command_failed'; - } - // PET process crash/hang recovery failures — e.g., "PET is currently restarting", // "failed after 3 restart attempts", "Failed to create stdio streams for PET process" if (msg.includes('restart') || msg.includes('stdio stream')) {