Skip to content

Commit cab7da7

Browse files
committed
Improve error classifier with additional classification logic and tests
1 parent c29567b commit cab7da7

2 files changed

Lines changed: 103 additions & 4 deletions

File tree

src/common/telemetry/errorClassifier.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ import { RpcTimeoutError } from '../../managers/common/nativePythonFinder';
44
export type DiscoveryErrorType =
55
| 'spawn_timeout'
66
| 'spawn_enoent'
7+
| 'spawn_error'
78
| 'permission_denied'
89
| 'canceled'
910
| 'parse_error'
11+
| 'pet_crash'
12+
| 'pet_not_found'
13+
| 'tool_exec_failed'
1014
| 'unknown';
1115

1216
/**
@@ -35,12 +39,50 @@ export function classifyError(ex: unknown): DiscoveryErrorType {
3539
return 'permission_denied';
3640
}
3741

38-
// Check message patterns
39-
const msg = ex.message.toLowerCase();
40-
if (msg.includes('timed out') || msg.includes('timeout')) {
42+
const msg = ex.message;
43+
const msgLower = msg.toLowerCase();
44+
45+
// PET process failures (crash, restart exhaustion, stdio failure)
46+
if (
47+
msgLower.includes('python environment tools (pet)') ||
48+
msgLower.includes('failed to create stdio streams for pet')
49+
) {
50+
return 'pet_crash';
51+
}
52+
53+
// Missing PET binary / Python extension not found
54+
if (msgLower.includes('python extension not found')) {
55+
return 'pet_not_found';
56+
}
57+
58+
// Wrapped spawn errors from condaUtils / other managers (e.g. "Error spawning conda: spawn conda ENOENT")
59+
if (msgLower.includes('error spawning')) {
60+
if (msgLower.includes('enoent')) {
61+
return 'spawn_enoent';
62+
}
63+
if (msgLower.includes('eacces') || msgLower.includes('eperm')) {
64+
return 'permission_denied';
65+
}
66+
return 'spawn_error';
67+
}
68+
69+
// Non-zero exit code failures (e.g. "Failed to run "conda info --envs --json":\n ...")
70+
if (msgLower.includes('failed to run')) {
71+
return 'tool_exec_failed';
72+
}
73+
74+
// Check message patterns for timeouts
75+
if (msgLower.includes('timed out') || msgLower.includes('timeout')) {
4176
return 'spawn_timeout';
4277
}
43-
if (msg.includes('parse') || msg.includes('unexpected token') || msg.includes('json')) {
78+
79+
// Parse / JSON errors (including "conda info returned invalid data type")
80+
if (
81+
msgLower.includes('parse') ||
82+
msgLower.includes('unexpected token') ||
83+
msgLower.includes('json') ||
84+
msgLower.includes('invalid data type')
85+
) {
4486
return 'parse_error';
4587
}
4688

src/test/common/telemetry/errorClassifier.unit.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,62 @@ suite('Error Classifier', () => {
6464
test('should classify unrecognized errors as unknown', () => {
6565
assert.strictEqual(classifyError(new Error('something went wrong')), 'unknown');
6666
});
67+
68+
test('should classify PET restart failure as pet_crash', () => {
69+
assert.strictEqual(
70+
classifyError(
71+
new Error(
72+
'Python Environment Tools (PET) failed after 3 restart attempts. Check the Output panel.',
73+
),
74+
),
75+
'pet_crash',
76+
);
77+
});
78+
79+
test('should classify PET currently restarting as pet_crash', () => {
80+
assert.strictEqual(
81+
classifyError(new Error('Python Environment Tools (PET) is currently restarting. Please try again.')),
82+
'pet_crash',
83+
);
84+
});
85+
86+
test('should classify PET stdio failure as pet_crash', () => {
87+
assert.strictEqual(classifyError(new Error('Failed to create stdio streams for PET process')), 'pet_crash');
88+
});
89+
90+
test('should classify missing PET binary as pet_not_found', () => {
91+
assert.strictEqual(classifyError(new Error('Python extension not found')), 'pet_not_found');
92+
});
93+
94+
test('should classify wrapped spawn ENOENT as spawn_enoent', () => {
95+
assert.strictEqual(classifyError(new Error('Error spawning conda: spawn conda ENOENT')), 'spawn_enoent');
96+
});
97+
98+
test('should classify wrapped spawn EACCES as permission_denied', () => {
99+
assert.strictEqual(
100+
classifyError(new Error('Error spawning python: spawn python EACCES')),
101+
'permission_denied',
102+
);
103+
});
104+
105+
test('should classify wrapped spawn with other cause as spawn_error', () => {
106+
assert.strictEqual(classifyError(new Error('Error spawning uv: some unexpected failure')), 'spawn_error');
107+
});
108+
109+
test('should classify non-zero exit code failures as tool_exec_failed', () => {
110+
assert.strictEqual(
111+
classifyError(new Error('Failed to run "conda info --envs --json":\n conda not initialized')),
112+
'tool_exec_failed',
113+
);
114+
assert.strictEqual(classifyError(new Error('Failed to run uv pip install numpy')), 'tool_exec_failed');
115+
assert.strictEqual(classifyError(new Error('Failed to run poetry install')), 'tool_exec_failed');
116+
});
117+
118+
test('should classify invalid data type errors as parse_error', () => {
119+
assert.strictEqual(
120+
classifyError(new Error('conda info returned invalid data type: string')),
121+
'parse_error',
122+
);
123+
});
67124
});
68125
});

0 commit comments

Comments
 (0)