From 0db69a8cacf4fe3e321ad644bb0f09f8d55a6130 Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Fri, 13 Mar 2026 13:50:09 -0700 Subject: [PATCH 1/9] add more telemetry --- src/common/telemetry/constants.ts | 30 ++++++++++++++++++++++++++++-- src/common/utils/asyncUtils.ts | 7 +++++++ src/extension.ts | 16 ++++++++++++++-- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/common/telemetry/constants.ts b/src/common/telemetry/constants.ts index 4cd75372..70d92f7d 100644 --- a/src/common/telemetry/constants.ts +++ b/src/common/telemetry/constants.ts @@ -50,6 +50,14 @@ export enum EventNames { */ ENVIRONMENT_DISCOVERY = 'ENVIRONMENT_DISCOVERY', MANAGER_READY_TIMEOUT = 'MANAGER_READY.TIMEOUT', + /** + * Telemetry event for individual manager registration failure. + * Fires once per manager that fails during registration (inside safeRegister). + * Properties: + * - managerName: string (e.g. 'system', 'conda', 'pyenv', 'pipenv', 'poetry', 'shellStartupVars') + * - errorType: string (classified error category from classifyError) + */ + MANAGER_REGISTRATION_FAILED = 'MANAGER_REGISTRATION.FAILED', } // Map all events to their properties @@ -62,10 +70,17 @@ export interface IEventNamePropertyMapping { [EventNames.EXTENSION_ACTIVATION_DURATION]: never | undefined; /* __GDPR__ "extension.manager_registration_duration": { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "result" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "failureStage" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "errorType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } } */ - [EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION]: never | undefined; + [EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION]: { + result: 'success' | 'error'; + failureStage?: string; + errorType?: string; + }; /* __GDPR__ "environment_manager.registered": { @@ -239,4 +254,15 @@ export interface IEventNamePropertyMapping { managerId: string; managerKind: 'environment' | 'package'; }; + + /* __GDPR__ + "manager_registration.failed": { + "managerName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "errorType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ + [EventNames.MANAGER_REGISTRATION_FAILED]: { + managerName: string; + errorType: string; + }; } diff --git a/src/common/utils/asyncUtils.ts b/src/common/utils/asyncUtils.ts index b0de59e4..06380b29 100644 --- a/src/common/utils/asyncUtils.ts +++ b/src/common/utils/asyncUtils.ts @@ -1,4 +1,7 @@ import { traceError } from '../logging'; +import { EventNames } from '../telemetry/constants'; +import { classifyError } from '../telemetry/errorClassifier'; +import { sendTelemetryEvent } from '../telemetry/sender'; export async function timeout(milliseconds: number): Promise { return new Promise((resolve) => setTimeout(resolve, milliseconds)); @@ -13,5 +16,9 @@ export async function safeRegister(name: string, task: Promise): Promise { + let failureStage = 'nativeFinder'; try { // This is the finder that is used by all the built in environment managers const nativeFinder: NativePythonFinder = await createNativePythonFinder(outputChannel, api, context); @@ -529,6 +531,7 @@ export async function activate(context: ExtensionContext): Promise Date: Mon, 16 Mar 2026 14:11:23 -0700 Subject: [PATCH 2/9] add timeout --- src/common/telemetry/constants.ts | 18 ++++++++++++++++++ src/extension.ts | 21 +++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/common/telemetry/constants.ts b/src/common/telemetry/constants.ts index 70d92f7d..ce6fd8b0 100644 --- a/src/common/telemetry/constants.ts +++ b/src/common/telemetry/constants.ts @@ -58,6 +58,14 @@ export enum EventNames { * - errorType: string (classified error category from classifyError) */ MANAGER_REGISTRATION_FAILED = 'MANAGER_REGISTRATION.FAILED', + /** + * Telemetry event fired when the setup block appears to be hung. + * A watchdog timer fires after a deadline; if the setup completes normally, + * the timer is cancelled and this event never fires. + * Properties: + * - failureStage: string (which phase was in progress when the watchdog fired) + */ + SETUP_HANG_DETECTED = 'SETUP.HANG_DETECTED', } // Map all events to their properties @@ -265,4 +273,14 @@ export interface IEventNamePropertyMapping { managerName: string; errorType: string; }; + + /* __GDPR__ + "setup.hang_detected": { + "failureStage": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" } + } + */ + [EventNames.SETUP_HANG_DETECTED]: { + failureStage: string; + }; } diff --git a/src/extension.ts b/src/extension.ts index 720713da..a35c0fab 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -524,6 +524,25 @@ export async function activate(context: ExtensionContext): Promise { let failureStage = 'nativeFinder'; + // Watchdog: fires if setup hasn't completed within 120s, indicating a likely hang + const SETUP_HANG_TIMEOUT_MS = 120_000; + let hangWatchdogActive = true; + const clearHangWatchdog = () => { + if (!hangWatchdogActive) { + return; + } + hangWatchdogActive = false; + clearTimeout(hangWatchdog); + }; + const hangWatchdog = setTimeout(() => { + if (!hangWatchdogActive) { + return; + } + hangWatchdogActive = false; + traceError(`Setup appears hung during stage: ${failureStage}`); + sendTelemetryEvent(EventNames.SETUP_HANG_DETECTED, start.elapsedTime, { failureStage }); + }, SETUP_HANG_TIMEOUT_MS); + context.subscriptions.push({ dispose: clearHangWatchdog }); try { // This is the finder that is used by all the built in environment managers const nativeFinder: NativePythonFinder = await createNativePythonFinder(outputChannel, api, context); @@ -566,6 +585,7 @@ export async function activate(context: ExtensionContext): Promise Date: Mon, 16 Mar 2026 16:50:39 -0700 Subject: [PATCH 3/9] add more telemetry --- src/common/telemetry/constants.ts | 38 +++++++++++++++ src/extension.ts | 79 ++++++++++++++++++++++++++++++- src/managers/conda/main.ts | 40 ++++++++++------ src/managers/pipenv/main.ts | 6 +++ src/managers/poetry/main.ts | 6 +++ src/managers/pyenv/main.ts | 6 +++ 6 files changed, 159 insertions(+), 16 deletions(-) diff --git a/src/common/telemetry/constants.ts b/src/common/telemetry/constants.ts index ce6fd8b0..f5da6c85 100644 --- a/src/common/telemetry/constants.ts +++ b/src/common/telemetry/constants.ts @@ -66,6 +66,22 @@ export enum EventNames { * - failureStage: string (which phase was in progress when the watchdog fired) */ SETUP_HANG_DETECTED = 'SETUP.HANG_DETECTED', + /** + * Telemetry event for when a manager skips registration because its tool was not found. + * This is an expected outcome (not an error) and is distinct from MANAGER_REGISTRATION_FAILED. + * Properties: + * - managerName: string (e.g. 'conda', 'pyenv', 'pipenv', 'poetry') + * - reason: string ('tool_not_found') + */ + MANAGER_REGISTRATION_SKIPPED = 'MANAGER_REGISTRATION.SKIPPED', + /** + * Telemetry event fired after manager registration when PET discovered environments + * of a kind whose corresponding manager did not register. + * Properties: + * - managerName: string (e.g. 'conda', 'pyenv', 'pipenv', 'poetry') + * - petEnvCount: number (how many envs PET found for that kind) + */ + MANAGER_DISCOVERY_MISMATCH = 'MANAGER_DISCOVERY.MISMATCH', } // Map all events to their properties @@ -283,4 +299,26 @@ export interface IEventNamePropertyMapping { [EventNames.SETUP_HANG_DETECTED]: { failureStage: string; }; + + /* __GDPR__ + "manager_registration.skipped": { + "managerName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ + [EventNames.MANAGER_REGISTRATION_SKIPPED]: { + managerName: string; + reason: 'tool_not_found'; + }; + + /* __GDPR__ + "manager_discovery.mismatch": { + "managerName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "petEnvCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" } + } + */ + [EventNames.MANAGER_DISCOVERY_MISMATCH]: { + managerName: string; + petEnvCount: number; + }; } diff --git a/src/extension.ts b/src/extension.ts index a35c0fab..47bba46b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -96,13 +96,87 @@ import { collectEnvironmentInfo, getEnvManagerAndPackageManagerConfigLevels, run import { EnvironmentManagers, ProjectCreators, PythonProjectManager } from './internal.api'; import { registerSystemPythonFeatures } from './managers/builtin/main'; import { SysPythonManager } from './managers/builtin/sysPythonManager'; -import { createNativePythonFinder, NativePythonFinder } from './managers/common/nativePythonFinder'; +import { + createNativePythonFinder, + NativePythonEnvironmentKind, + NativePythonFinder, +} from './managers/common/nativePythonFinder'; import { IDisposable } from './managers/common/types'; import { registerCondaFeatures } from './managers/conda/main'; import { registerPipenvFeatures } from './managers/pipenv/main'; import { registerPoetryFeatures } from './managers/poetry/main'; import { registerPyenvFeatures } from './managers/pyenv/main'; +/** + * Map from PET NativePythonEnvironmentKind to the manager name used in registration. + * Only includes kinds that have a dedicated manager (not system/venv/etc.). + */ +const PET_KIND_TO_MANAGER: ReadonlyMap = new Map([ + [NativePythonEnvironmentKind.conda, 'conda'], + [NativePythonEnvironmentKind.pyenv, 'pyenv'], + [NativePythonEnvironmentKind.pyenvVirtualEnv, 'pyenv'], + [NativePythonEnvironmentKind.pipenv, 'pipenv'], + [NativePythonEnvironmentKind.poetry, 'poetry'], +]); + +/** + * Checks whether PET discovered environments whose corresponding manager did not register. + * Uses a soft (cached) refresh so it doesn't trigger additional PET work. + */ +async function checkPetManagerMismatch( + nativeFinder: NativePythonFinder, + envManagers: EnvironmentManagers, +): Promise { + const registeredIds = new Set(envManagers.managers.map((m) => m.id)); + + // Map manager names to their expected managerId prefix + const managerIdPrefixes: ReadonlyMap = new Map([ + ['conda', 'ms-python.python:conda'], + ['pyenv', 'ms-python.python:pyenv'], + ['pipenv', 'ms-python.python:pipenv'], + ['poetry', 'ms-python.python:poetry'], + ]); + + // Group PET kinds by manager name to avoid double-counting (e.g. pyenv + pyenvVirtualEnv) + const kindsByManager = new Map(); + for (const [kind, managerName] of PET_KIND_TO_MANAGER) { + const existing = kindsByManager.get(managerName) ?? []; + existing.push(kind); + kindsByManager.set(managerName, existing); + } + + for (const [managerName, kinds] of kindsByManager) { + const expectedPrefix = managerIdPrefixes.get(managerName); + if (!expectedPrefix) { + continue; + } + + // Check if the corresponding manager registered + const isRegistered = Array.from(registeredIds).some((id) => id.startsWith(expectedPrefix)); + if (isRegistered) { + continue; + } + + // Manager not registered — check if PET found environments of any related kind + let totalPetEnvs = 0; + for (const kind of kinds) { + try { + const petEnvs = await nativeFinder.refresh(false, kind); + totalPetEnvs += petEnvs.length; + } catch { + // PET query failed — don't block post-init; skip this kind + } + } + + if (totalPetEnvs > 0) { + sendTelemetryEvent(EventNames.MANAGER_DISCOVERY_MISMATCH, undefined, { + managerName, + petEnvCount: totalPetEnvs, + }); + } + } +} + export async function activate(context: ExtensionContext): Promise { // Only skip activation if user explicitly set useEnvironmentsExtension to false. // When disabled, the main Python extension handles environments instead (legacy mode). @@ -594,6 +668,9 @@ export async function activate(context: ExtensionContext): Promise { const api: PythonEnvironmentApi = await getPythonApi(); + let condaPath: string | undefined; try { // get Conda will return only ONE conda manager, that correlates to a single conda install - const condaPath: string = await getConda(nativeFinder); - const sourcingStatus: CondaSourcingStatus = await constructCondaSourcingStatus(condaPath); - traceInfo(sourcingStatus.toString()); - - const envManager = new CondaEnvManager(nativeFinder, api, log); - const packageManager = new CondaPackageManager(api, log); - - envManager.sourcingInformation = sourcingStatus; - - disposables.push( - envManager, - packageManager, - api.registerEnvironmentManager(envManager), - api.registerPackageManager(packageManager), - ); + condaPath = await getConda(nativeFinder); } catch (ex) { traceInfo('Conda not found, turning off conda features.', ex); + sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_SKIPPED, undefined, { + managerName: 'conda', + reason: 'tool_not_found', + }); await notifyMissingManagerIfDefault('ms-python.python:conda', projectManager, api); + return; } + + // Conda was found — errors below are real registration failures (let safeRegister handle them) + const sourcingStatus: CondaSourcingStatus = await constructCondaSourcingStatus(condaPath); + traceInfo(sourcingStatus.toString()); + + const envManager = new CondaEnvManager(nativeFinder, api, log); + const packageManager = new CondaPackageManager(api, log); + + envManager.sourcingInformation = sourcingStatus; + + disposables.push( + envManager, + packageManager, + api.registerEnvironmentManager(envManager), + api.registerPackageManager(packageManager), + ); } diff --git a/src/managers/pipenv/main.ts b/src/managers/pipenv/main.ts index d87d0d9b..c0710d7d 100644 --- a/src/managers/pipenv/main.ts +++ b/src/managers/pipenv/main.ts @@ -1,6 +1,8 @@ import { Disposable } from 'vscode'; import { PythonEnvironmentApi } from '../../api'; import { traceInfo } from '../../common/logging'; +import { EventNames } from '../../common/telemetry/constants'; +import { sendTelemetryEvent } from '../../common/telemetry/sender'; import { getPythonApi } from '../../features/pythonApi'; import { PythonProjectManager } from '../../internal.api'; import { NativePythonFinder } from '../common/nativePythonFinder'; @@ -35,6 +37,10 @@ export async function registerPipenvFeatures( traceInfo( 'Pipenv not found, turning off pipenv features. If you have pipenv installed in a non-standard location, set the "python.pipenvPath" setting.', ); + sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_SKIPPED, undefined, { + managerName: 'pipenv', + reason: 'tool_not_found', + }); await notifyMissingManagerIfDefault('ms-python.python:pipenv', projectManager, api); } } catch (ex) { diff --git a/src/managers/poetry/main.ts b/src/managers/poetry/main.ts index 5f74fb46..d4014801 100644 --- a/src/managers/poetry/main.ts +++ b/src/managers/poetry/main.ts @@ -1,6 +1,8 @@ import { Disposable, LogOutputChannel } from 'vscode'; import { PythonEnvironmentApi } from '../../api'; import { traceInfo } from '../../common/logging'; +import { EventNames } from '../../common/telemetry/constants'; +import { sendTelemetryEvent } from '../../common/telemetry/sender'; import { getPythonApi } from '../../features/pythonApi'; import { PythonProjectManager } from '../../internal.api'; import { NativePythonFinder } from '../common/nativePythonFinder'; @@ -36,6 +38,10 @@ export async function registerPoetryFeatures( ); } else { traceInfo('Poetry not found, turning off poetry features.'); + sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_SKIPPED, undefined, { + managerName: 'poetry', + reason: 'tool_not_found', + }); await notifyMissingManagerIfDefault('ms-python.python:poetry', projectManager, api); } } catch (ex) { diff --git a/src/managers/pyenv/main.ts b/src/managers/pyenv/main.ts index 2ca6a96a..e88b1718 100644 --- a/src/managers/pyenv/main.ts +++ b/src/managers/pyenv/main.ts @@ -1,6 +1,8 @@ import { Disposable } from 'vscode'; import { PythonEnvironmentApi } from '../../api'; import { traceInfo } from '../../common/logging'; +import { EventNames } from '../../common/telemetry/constants'; +import { sendTelemetryEvent } from '../../common/telemetry/sender'; import { getPythonApi } from '../../features/pythonApi'; import { PythonProjectManager } from '../../internal.api'; import { NativePythonFinder } from '../common/nativePythonFinder'; @@ -23,6 +25,10 @@ export async function registerPyenvFeatures( disposables.push(mgr, api.registerEnvironmentManager(mgr)); } else { traceInfo('Pyenv not found, turning off pyenv features.'); + sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_SKIPPED, undefined, { + managerName: 'pyenv', + reason: 'tool_not_found', + }); await notifyMissingManagerIfDefault('ms-python.python:pyenv', projectManager, api); } } catch (ex) { From afa631af432b7fbc71293e5c8d43670397d540f6 Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Mon, 16 Mar 2026 17:06:30 -0700 Subject: [PATCH 4/9] fix potential issue --- src/extension.ts | 25 +++++++++++++++---------- src/managers/conda/main.ts | 19 ++++++++++++------- src/managers/pipenv/main.ts | 5 +++++ src/managers/poetry/main.ts | 5 +++++ 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 47bba46b..7be621fb 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -98,6 +98,7 @@ import { registerSystemPythonFeatures } from './managers/builtin/main'; import { SysPythonManager } from './managers/builtin/sysPythonManager'; import { createNativePythonFinder, + isNativeEnvInfo, NativePythonEnvironmentKind, NativePythonFinder, } from './managers/common/nativePythonFinder'; @@ -137,6 +138,18 @@ async function checkPetManagerMismatch( ['poetry', 'ms-python.python:poetry'], ]); + // Use a single cached refresh to avoid triggering per-kind hard refreshes on cache miss + let allEnvs; + try { + allEnvs = await nativeFinder.refresh(false); + } catch { + // PET query failed — don't block post-init + return; + } + + // Filter to only environment entries (not manager entries) + const envInfos = allEnvs.filter(isNativeEnvInfo); + // Group PET kinds by manager name to avoid double-counting (e.g. pyenv + pyenvVirtualEnv) const kindsByManager = new Map(); for (const [kind, managerName] of PET_KIND_TO_MANAGER) { @@ -157,16 +170,8 @@ async function checkPetManagerMismatch( continue; } - // Manager not registered — check if PET found environments of any related kind - let totalPetEnvs = 0; - for (const kind of kinds) { - try { - const petEnvs = await nativeFinder.refresh(false, kind); - totalPetEnvs += petEnvs.length; - } catch { - // PET query failed — don't block post-init; skip this kind - } - } + // Manager not registered — count PET environments of any related kind + const totalPetEnvs = envInfos.filter((e) => e.kind !== undefined && kinds.includes(e.kind)).length; if (totalPetEnvs > 0) { sendTelemetryEvent(EventNames.MANAGER_DISCOVERY_MISMATCH, undefined, { diff --git a/src/managers/conda/main.ts b/src/managers/conda/main.ts index 664e6429..92e8ccc0 100644 --- a/src/managers/conda/main.ts +++ b/src/managers/conda/main.ts @@ -25,13 +25,18 @@ export async function registerCondaFeatures( // get Conda will return only ONE conda manager, that correlates to a single conda install condaPath = await getConda(nativeFinder); } catch (ex) { - traceInfo('Conda not found, turning off conda features.', ex); - sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_SKIPPED, undefined, { - managerName: 'conda', - reason: 'tool_not_found', - }); - await notifyMissingManagerIfDefault('ms-python.python:conda', projectManager, api); - return; + // Only treat the known "Conda not found" error as a skip; + // other errors (e.g. PET timeout/crash) should propagate to safeRegister. + if (ex instanceof Error && ex.message === 'Conda not found') { + traceInfo('Conda not found, turning off conda features.', ex); + sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_SKIPPED, undefined, { + managerName: 'conda', + reason: 'tool_not_found', + }); + await notifyMissingManagerIfDefault('ms-python.python:conda', projectManager, api); + return; + } + throw ex; } // Conda was found — errors below are real registration failures (let safeRegister handle them) diff --git a/src/managers/pipenv/main.ts b/src/managers/pipenv/main.ts index c0710d7d..75f567a1 100644 --- a/src/managers/pipenv/main.ts +++ b/src/managers/pipenv/main.ts @@ -2,6 +2,7 @@ import { Disposable } from 'vscode'; import { PythonEnvironmentApi } from '../../api'; import { traceInfo } from '../../common/logging'; import { EventNames } from '../../common/telemetry/constants'; +import { classifyError } from '../../common/telemetry/errorClassifier'; import { sendTelemetryEvent } from '../../common/telemetry/sender'; import { getPythonApi } from '../../features/pythonApi'; import { PythonProjectManager } from '../../internal.api'; @@ -48,6 +49,10 @@ export async function registerPipenvFeatures( 'Pipenv not found, turning off pipenv features. If you have pipenv installed in a non-standard location, set the "python.pipenvPath" setting.', ex, ); + sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_FAILED, undefined, { + managerName: 'pipenv', + errorType: classifyError(ex), + }); await notifyMissingManagerIfDefault('ms-python.python:pipenv', projectManager, api); } } diff --git a/src/managers/poetry/main.ts b/src/managers/poetry/main.ts index d4014801..bd28f5ec 100644 --- a/src/managers/poetry/main.ts +++ b/src/managers/poetry/main.ts @@ -2,6 +2,7 @@ import { Disposable, LogOutputChannel } from 'vscode'; import { PythonEnvironmentApi } from '../../api'; import { traceInfo } from '../../common/logging'; import { EventNames } from '../../common/telemetry/constants'; +import { classifyError } from '../../common/telemetry/errorClassifier'; import { sendTelemetryEvent } from '../../common/telemetry/sender'; import { getPythonApi } from '../../features/pythonApi'; import { PythonProjectManager } from '../../internal.api'; @@ -46,6 +47,10 @@ export async function registerPoetryFeatures( } } catch (ex) { traceInfo('Poetry not found, turning off poetry features.', ex); + sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_FAILED, undefined, { + managerName: 'poetry', + errorType: classifyError(ex), + }); await notifyMissingManagerIfDefault('ms-python.python:poetry', projectManager, api); } } From f26890f4b3cb374d1367a200ae47f26852518c89 Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Tue, 17 Mar 2026 10:27:36 -0700 Subject: [PATCH 5/9] clean up --- src/common/telemetry/constants.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/common/telemetry/constants.ts b/src/common/telemetry/constants.ts index f5da6c85..df7f2f02 100644 --- a/src/common/telemetry/constants.ts +++ b/src/common/telemetry/constants.ts @@ -95,9 +95,9 @@ export interface IEventNamePropertyMapping { /* __GDPR__ "extension.manager_registration_duration": { "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, - "result" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, - "failureStage" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, - "errorType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + "result" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" }, + "failureStage" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" }, + "errorType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" } } */ [EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION]: { @@ -281,8 +281,8 @@ export interface IEventNamePropertyMapping { /* __GDPR__ "manager_registration.failed": { - "managerName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, - "errorType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + "managerName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" }, + "errorType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" } } */ [EventNames.MANAGER_REGISTRATION_FAILED]: { @@ -292,8 +292,8 @@ export interface IEventNamePropertyMapping { /* __GDPR__ "setup.hang_detected": { - "failureStage": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, - "": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" } + "failureStage": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" }, + "": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "StellaHuang95" } } */ [EventNames.SETUP_HANG_DETECTED]: { @@ -302,8 +302,8 @@ export interface IEventNamePropertyMapping { /* __GDPR__ "manager_registration.skipped": { - "managerName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, - "reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + "managerName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" }, + "reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" } } */ [EventNames.MANAGER_REGISTRATION_SKIPPED]: { @@ -313,8 +313,8 @@ export interface IEventNamePropertyMapping { /* __GDPR__ "manager_discovery.mismatch": { - "managerName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, - "petEnvCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" } + "managerName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" }, + "petEnvCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "StellaHuang95" } } */ [EventNames.MANAGER_DISCOVERY_MISMATCH]: { From 3ce213e8776c1cd951f298de7516d533ef1c3c95 Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Tue, 17 Mar 2026 13:27:40 -0700 Subject: [PATCH 6/9] clean up --- src/managers/conda/main.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/managers/conda/main.ts b/src/managers/conda/main.ts index 92e8ccc0..664e6429 100644 --- a/src/managers/conda/main.ts +++ b/src/managers/conda/main.ts @@ -25,18 +25,13 @@ export async function registerCondaFeatures( // get Conda will return only ONE conda manager, that correlates to a single conda install condaPath = await getConda(nativeFinder); } catch (ex) { - // Only treat the known "Conda not found" error as a skip; - // other errors (e.g. PET timeout/crash) should propagate to safeRegister. - if (ex instanceof Error && ex.message === 'Conda not found') { - traceInfo('Conda not found, turning off conda features.', ex); - sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_SKIPPED, undefined, { - managerName: 'conda', - reason: 'tool_not_found', - }); - await notifyMissingManagerIfDefault('ms-python.python:conda', projectManager, api); - return; - } - throw ex; + traceInfo('Conda not found, turning off conda features.', ex); + sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_SKIPPED, undefined, { + managerName: 'conda', + reason: 'tool_not_found', + }); + await notifyMissingManagerIfDefault('ms-python.python:conda', projectManager, api); + return; } // Conda was found — errors below are real registration failures (let safeRegister handle them) From da3294ca2c15723bab8eb242b9630d3770f3c77b Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Tue, 17 Mar 2026 13:45:41 -0700 Subject: [PATCH 7/9] add failure telemetry --- src/managers/pyenv/main.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/managers/pyenv/main.ts b/src/managers/pyenv/main.ts index e88b1718..853c44d7 100644 --- a/src/managers/pyenv/main.ts +++ b/src/managers/pyenv/main.ts @@ -2,6 +2,7 @@ import { Disposable } from 'vscode'; import { PythonEnvironmentApi } from '../../api'; import { traceInfo } from '../../common/logging'; import { EventNames } from '../../common/telemetry/constants'; +import { classifyError } from '../../common/telemetry/errorClassifier'; import { sendTelemetryEvent } from '../../common/telemetry/sender'; import { getPythonApi } from '../../features/pythonApi'; import { PythonProjectManager } from '../../internal.api'; @@ -33,6 +34,10 @@ export async function registerPyenvFeatures( } } catch (ex) { traceInfo('Pyenv not found, turning off pyenv features.', ex); + sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_FAILED, undefined, { + managerName: 'pyenv', + errorType: classifyError(ex), + }); await notifyMissingManagerIfDefault('ms-python.python:pyenv', projectManager, api); } } From 3e446a56ecaf68310237f90f5a21d7cc2ed85a4f Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Tue, 17 Mar 2026 13:59:39 -0700 Subject: [PATCH 8/9] wrap conda in try catch --- src/managers/conda/main.ts | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/managers/conda/main.ts b/src/managers/conda/main.ts index 664e6429..a92da66e 100644 --- a/src/managers/conda/main.ts +++ b/src/managers/conda/main.ts @@ -34,19 +34,24 @@ export async function registerCondaFeatures( return; } - // Conda was found — errors below are real registration failures (let safeRegister handle them) - const sourcingStatus: CondaSourcingStatus = await constructCondaSourcingStatus(condaPath); - traceInfo(sourcingStatus.toString()); + // Conda was found — errors below are real registration failures (let safeRegister handle telemetry) + try { + const sourcingStatus: CondaSourcingStatus = await constructCondaSourcingStatus(condaPath); + traceInfo(sourcingStatus.toString()); - const envManager = new CondaEnvManager(nativeFinder, api, log); - const packageManager = new CondaPackageManager(api, log); + const envManager = new CondaEnvManager(nativeFinder, api, log); + const packageManager = new CondaPackageManager(api, log); - envManager.sourcingInformation = sourcingStatus; + envManager.sourcingInformation = sourcingStatus; - disposables.push( - envManager, - packageManager, - api.registerEnvironmentManager(envManager), - api.registerPackageManager(packageManager), - ); + disposables.push( + envManager, + packageManager, + api.registerEnvironmentManager(envManager), + api.registerPackageManager(packageManager), + ); + } catch (ex) { + await notifyMissingManagerIfDefault('ms-python.python:conda', projectManager, api); + throw ex; + } } From e8e0eeb2e5e5880d33ae851f967dbb56a32d07de Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Tue, 17 Mar 2026 15:49:39 -0700 Subject: [PATCH 9/9] remove duplicate telemetry --- src/common/telemetry/constants.ts | 19 ------- src/extension.ts | 84 +------------------------------ 2 files changed, 1 insertion(+), 102 deletions(-) diff --git a/src/common/telemetry/constants.ts b/src/common/telemetry/constants.ts index df7f2f02..91de136a 100644 --- a/src/common/telemetry/constants.ts +++ b/src/common/telemetry/constants.ts @@ -74,14 +74,6 @@ export enum EventNames { * - reason: string ('tool_not_found') */ MANAGER_REGISTRATION_SKIPPED = 'MANAGER_REGISTRATION.SKIPPED', - /** - * Telemetry event fired after manager registration when PET discovered environments - * of a kind whose corresponding manager did not register. - * Properties: - * - managerName: string (e.g. 'conda', 'pyenv', 'pipenv', 'poetry') - * - petEnvCount: number (how many envs PET found for that kind) - */ - MANAGER_DISCOVERY_MISMATCH = 'MANAGER_DISCOVERY.MISMATCH', } // Map all events to their properties @@ -310,15 +302,4 @@ export interface IEventNamePropertyMapping { managerName: string; reason: 'tool_not_found'; }; - - /* __GDPR__ - "manager_discovery.mismatch": { - "managerName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" }, - "petEnvCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "StellaHuang95" } - } - */ - [EventNames.MANAGER_DISCOVERY_MISMATCH]: { - managerName: string; - petEnvCount: number; - }; } diff --git a/src/extension.ts b/src/extension.ts index 7be621fb..a35c0fab 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -96,92 +96,13 @@ import { collectEnvironmentInfo, getEnvManagerAndPackageManagerConfigLevels, run import { EnvironmentManagers, ProjectCreators, PythonProjectManager } from './internal.api'; import { registerSystemPythonFeatures } from './managers/builtin/main'; import { SysPythonManager } from './managers/builtin/sysPythonManager'; -import { - createNativePythonFinder, - isNativeEnvInfo, - NativePythonEnvironmentKind, - NativePythonFinder, -} from './managers/common/nativePythonFinder'; +import { createNativePythonFinder, NativePythonFinder } from './managers/common/nativePythonFinder'; import { IDisposable } from './managers/common/types'; import { registerCondaFeatures } from './managers/conda/main'; import { registerPipenvFeatures } from './managers/pipenv/main'; import { registerPoetryFeatures } from './managers/poetry/main'; import { registerPyenvFeatures } from './managers/pyenv/main'; -/** - * Map from PET NativePythonEnvironmentKind to the manager name used in registration. - * Only includes kinds that have a dedicated manager (not system/venv/etc.). - */ -const PET_KIND_TO_MANAGER: ReadonlyMap = new Map([ - [NativePythonEnvironmentKind.conda, 'conda'], - [NativePythonEnvironmentKind.pyenv, 'pyenv'], - [NativePythonEnvironmentKind.pyenvVirtualEnv, 'pyenv'], - [NativePythonEnvironmentKind.pipenv, 'pipenv'], - [NativePythonEnvironmentKind.poetry, 'poetry'], -]); - -/** - * Checks whether PET discovered environments whose corresponding manager did not register. - * Uses a soft (cached) refresh so it doesn't trigger additional PET work. - */ -async function checkPetManagerMismatch( - nativeFinder: NativePythonFinder, - envManagers: EnvironmentManagers, -): Promise { - const registeredIds = new Set(envManagers.managers.map((m) => m.id)); - - // Map manager names to their expected managerId prefix - const managerIdPrefixes: ReadonlyMap = new Map([ - ['conda', 'ms-python.python:conda'], - ['pyenv', 'ms-python.python:pyenv'], - ['pipenv', 'ms-python.python:pipenv'], - ['poetry', 'ms-python.python:poetry'], - ]); - - // Use a single cached refresh to avoid triggering per-kind hard refreshes on cache miss - let allEnvs; - try { - allEnvs = await nativeFinder.refresh(false); - } catch { - // PET query failed — don't block post-init - return; - } - - // Filter to only environment entries (not manager entries) - const envInfos = allEnvs.filter(isNativeEnvInfo); - - // Group PET kinds by manager name to avoid double-counting (e.g. pyenv + pyenvVirtualEnv) - const kindsByManager = new Map(); - for (const [kind, managerName] of PET_KIND_TO_MANAGER) { - const existing = kindsByManager.get(managerName) ?? []; - existing.push(kind); - kindsByManager.set(managerName, existing); - } - - for (const [managerName, kinds] of kindsByManager) { - const expectedPrefix = managerIdPrefixes.get(managerName); - if (!expectedPrefix) { - continue; - } - - // Check if the corresponding manager registered - const isRegistered = Array.from(registeredIds).some((id) => id.startsWith(expectedPrefix)); - if (isRegistered) { - continue; - } - - // Manager not registered — count PET environments of any related kind - const totalPetEnvs = envInfos.filter((e) => e.kind !== undefined && kinds.includes(e.kind)).length; - - if (totalPetEnvs > 0) { - sendTelemetryEvent(EventNames.MANAGER_DISCOVERY_MISMATCH, undefined, { - managerName, - petEnvCount: totalPetEnvs, - }); - } - } -} - export async function activate(context: ExtensionContext): Promise { // Only skip activation if user explicitly set useEnvironmentsExtension to false. // When disabled, the main Python extension handles environments instead (legacy mode). @@ -673,9 +594,6 @@ export async function activate(context: ExtensionContext): Promise