Skip to content

Commit b6f2479

Browse files
committed
feat: add telemetry for PET process failures and exits
1 parent 8e3904b commit b6f2479

File tree

3 files changed

+91
-10
lines changed

3 files changed

+91
-10
lines changed

src/common/telemetry/constants.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export enum EventNames {
5050
*/
5151
ENVIRONMENT_DISCOVERY = 'ENVIRONMENT_DISCOVERY',
5252
MANAGER_READY_TIMEOUT = 'MANAGER_READY.TIMEOUT',
53+
PET_START_FAILED = 'PET.START_FAILED',
54+
PET_PROCESS_EXIT = 'PET.PROCESS_EXIT',
5355
}
5456

5557
// Map all events to their properties
@@ -238,4 +240,34 @@ export interface IEventNamePropertyMapping {
238240
managerId: string;
239241
managerKind: 'environment' | 'package';
240242
};
243+
244+
/* __GDPR__
245+
"pet.start_failed": {
246+
"errorCode": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
247+
"reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
248+
"platform": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
249+
"arch": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
250+
}
251+
*/
252+
[EventNames.PET_START_FAILED]: {
253+
errorCode: string;
254+
reason: 'binary_not_found' | 'spawn_failed' | 'unknown';
255+
platform: string;
256+
arch: string;
257+
};
258+
259+
/* __GDPR__
260+
"pet.process_exit": {
261+
"exitCode": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
262+
"signal": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
263+
"restartAttempt": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" },
264+
"wasExpected": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
265+
}
266+
*/
267+
[EventNames.PET_PROCESS_EXIT]: {
268+
exitCode: number | null;
269+
signal: string | null;
270+
restartAttempt: number;
271+
wasExpected: boolean;
272+
};
241273
}

src/extension.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -522,12 +522,39 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
522522
* Below are all the contributed features using the APIs.
523523
*/
524524
setImmediate(async () => {
525+
// Resolve the pet binary and spawn the finder process. Failures here are pet related.
526+
let nativeFinder: NativePythonFinder;
527+
try {
528+
nativeFinder = await createNativePythonFinder(outputChannel, api, context);
529+
} catch (error) {
530+
traceError('Failed to start Python finder (pet):', error);
531+
532+
const errnoError = error as NodeJS.ErrnoException;
533+
// Plain Error (no .code) = binary not found by getNativePythonToolsPath.
534+
// Errno error (has .code) = spawn failed (ENOENT, EACCES, EPERM, etc.).
535+
const reason = errnoError.code ? 'spawn_failed' : 'binary_not_found';
536+
sendTelemetryEvent(EventNames.PET_START_FAILED, undefined, {
537+
errorCode: errnoError.code ?? 'UNKNOWN',
538+
reason,
539+
platform: process.platform,
540+
arch: process.arch,
541+
});
542+
543+
window.showErrorMessage(
544+
l10n.t(
545+
'Python Environments: Failed to start the Python finder. Some features may not work correctly. Check the Output panel for details.',
546+
),
547+
);
548+
return;
549+
}
550+
551+
context.subscriptions.push(nativeFinder);
552+
const sysMgr = new SysPythonManager(nativeFinder, api, outputChannel);
553+
sysPythonManager.resolve(sysMgr);
554+
555+
// Manager registration and post-registration setup. safeRegister() absorbs
556+
// individual manager failures, so errors here are unexpected and non-pet-related.
525557
try {
526-
// This is the finder that is used by all the built in environment managers
527-
const nativeFinder: NativePythonFinder = await createNativePythonFinder(outputChannel, api, context);
528-
context.subscriptions.push(nativeFinder);
529-
const sysMgr = new SysPythonManager(nativeFinder, api, outputChannel);
530-
sysPythonManager.resolve(sysMgr);
531558
// Each manager registers independently — one failure must not block the others.
532559
await Promise.all([
533560
safeRegister(
@@ -567,7 +594,6 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
567594
await logDiscoverySummary(envManagers);
568595
} catch (error) {
569596
traceError('Failed to initialize environment managers:', error);
570-
// Show a user-friendly error message
571597
window.showErrorMessage(
572598
l10n.t(
573599
'Python Environments: Failed to initialize environment managers. Some features may not work correctly. Check the Output panel for details.',

src/managers/common/nativePythonFinder.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { spawnProcess } from '../../common/childProcess.apis';
99
import { ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID } from '../../common/constants';
1010
import { getExtension } from '../../common/extension.apis';
1111
import { traceError, traceVerbose, traceWarn } from '../../common/logging';
12+
import { EventNames } from '../../common/telemetry/constants';
13+
import { sendTelemetryEvent } from '../../common/telemetry/sender';
1214
import { untildify, untildifyArray } from '../../common/utils/pathUtils';
1315
import { isWindows } from '../../common/utils/platformUtils';
1416
import { createRunningWorkerPool, WorkerPool } from '../../common/utils/workerPool';
@@ -78,17 +80,27 @@ export async function getNativePythonToolsPath(): Promise<string> {
7880
const envsExt = getExtension(ENVS_EXTENSION_ID);
7981
if (envsExt) {
8082
const petPath = path.join(envsExt.extensionPath, 'python-env-tools', 'bin', isWindows() ? 'pet.exe' : 'pet');
81-
if (await fs.pathExists(petPath)) {
83+
const exists = await fs.pathExists(petPath);
84+
traceVerbose(`[pet] Primary path (envs-ext): ${petPath} — exists: ${exists}`);
85+
if (exists) {
8286
return petPath;
8387
}
88+
} else {
89+
traceVerbose(`[pet] Envs extension (${ENVS_EXTENSION_ID}) not found; trying Python extension fallback`);
8490
}
8591

8692
const python = getExtension(PYTHON_EXTENSION_ID);
8793
if (!python) {
88-
throw new Error('Python extension not found');
94+
throw new Error('Python extension not found and envs extension pet binary is missing');
8995
}
9096

91-
return path.join(python.extensionPath, 'python-env-tools', 'bin', isWindows() ? 'pet.exe' : 'pet');
97+
const fallbackPath = path.join(python.extensionPath, 'python-env-tools', 'bin', isWindows() ? 'pet.exe' : 'pet');
98+
const fallbackExists = await fs.pathExists(fallbackPath);
99+
traceVerbose(`[pet] Fallback path (python-ext): ${fallbackPath} — exists: ${fallbackExists}`);
100+
if (!fallbackExists) {
101+
throw new Error(`Python finder binary not found at: ${fallbackPath}`);
102+
}
103+
return fallbackPath;
92104
}
93105

94106
export interface NativeEnvInfo {
@@ -224,6 +236,7 @@ class NativePythonFinderImpl implements NativePythonFinder {
224236
private startFailed: boolean = false;
225237
private restartAttempts: number = 0;
226238
private isRestarting: boolean = false;
239+
private isDisposed: boolean = false;
227240
private readonly configureRetry = new ConfigureRetryState();
228241

229242
constructor(
@@ -426,6 +439,7 @@ class NativePythonFinderImpl implements NativePythonFinder {
426439
}
427440

428441
public dispose() {
442+
this.isDisposed = true;
429443
this.pool.stop();
430444
this.startDisposables.forEach((d) => d.dispose());
431445
this.connection.dispose();
@@ -475,11 +489,18 @@ class NativePythonFinderImpl implements NativePythonFinder {
475489
// Handle process exit - mark as exited so pending requests fail fast
476490
this.proc.on('exit', (code, signal) => {
477491
this.processExited = true;
492+
const wasExpected = this.isRestarting || this.isDisposed;
478493
if (code !== 0) {
479494
this.outputChannel.error(
480495
`[pet] Python Environment Tools exited unexpectedly with code ${code}, signal ${signal}`,
481496
);
482497
}
498+
sendTelemetryEvent(EventNames.PET_PROCESS_EXIT, undefined, {
499+
exitCode: code,
500+
signal: signal ?? null,
501+
restartAttempt: this.restartAttempts,
502+
wasExpected,
503+
});
483504
});
484505

485506
// Handle process errors (e.g., ENOENT if executable not found)
@@ -898,5 +919,7 @@ export async function createNativePythonFinder(
898919
api: PythonProjectApi,
899920
context: ExtensionContext,
900921
): Promise<NativePythonFinder> {
901-
return new NativePythonFinderImpl(outputChannel, await getNativePythonToolsPath(), api, getCacheDirectory(context));
922+
const petPath = await getNativePythonToolsPath();
923+
traceVerbose(`[pet] Resolved pet binary path: ${petPath}`);
924+
return new NativePythonFinderImpl(outputChannel, petPath, api, getCacheDirectory(context));
902925
}

0 commit comments

Comments
 (0)