Skip to content

Commit 3d25de3

Browse files
authored
Merge branch 'main' into smart-donkey
2 parents a7d5d40 + e192ffe commit 3d25de3

File tree

16 files changed

+612
-34
lines changed

16 files changed

+612
-34
lines changed

src/common/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs';
44
export const PYTHON_EXTENSION_ID = 'ms-python.python';
55
export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter';
66
export const EXTENSION_ROOT_DIR = path.dirname(__dirname);
7+
export const ISSUES_URL = 'https://github.com/microsoft/vscode-python-environments/issues';
78

89
export const DEFAULT_PACKAGE_MANAGER_ID = 'ms-python.python:pip';
910
export const DEFAULT_ENV_MANAGER_ID = 'ms-python.python:venv';

src/common/pickers/environments.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ProgressLocation, QuickInputButtons, QuickPickItem, QuickPickItemKind, ThemeIcon, Uri } from 'vscode';
1+
import { ProgressLocation, QuickInputButtons, QuickPickItem, QuickPickItemKind, ThemeIcon, Uri, l10n } from 'vscode';
22
import { CreateEnvironmentOptions, IconPath, PythonEnvironment, PythonProject } from '../../api';
33
import { InternalEnvironmentManager } from '../../internal.api';
44
import { Common, Interpreter, Pickers } from '../localize';
@@ -7,7 +7,13 @@ import { EventNames } from '../telemetry/constants';
77
import { sendTelemetryEvent } from '../telemetry/sender';
88
import { isWindows } from '../utils/platformUtils';
99
import { handlePythonPath } from '../utils/pythonPath';
10-
import { showOpenDialog, showQuickPick, showQuickPickWithButtons, withProgress } from '../window.apis';
10+
import {
11+
showErrorMessage,
12+
showOpenDialog,
13+
showQuickPick,
14+
showQuickPickWithButtons,
15+
withProgress,
16+
} from '../window.apis';
1117
import { pickEnvironmentManager } from './managers';
1218

1319
type QuickPickIcon =
@@ -66,6 +72,11 @@ async function browseForPython(
6672
return env;
6773
},
6874
);
75+
76+
if (!environment) {
77+
showErrorMessage(l10n.t('Selected file is not a valid Python interpreter: {0}', uri.fsPath));
78+
}
79+
6980
return environment;
7081
}
7182

src/common/telemetry/constants.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ export enum EventNames {
3232
* - projectUnderRoot: number (count of projects nested under workspace roots)
3333
*/
3434
PROJECT_STRUCTURE = 'PROJECT_STRUCTURE',
35+
/**
36+
* Telemetry event for environment tool usage at extension startup.
37+
* Fires once per tool that has at least one project using it.
38+
* Use dcount(machineId) by toolName to get unique users per tool.
39+
* Properties:
40+
* - toolName: string (the tool being used: venv, conda, poetry, etc.)
41+
*/
42+
ENVIRONMENT_TOOL_USAGE = 'ENVIRONMENT_TOOL_USAGE',
3543
/**
3644
* Telemetry event for environment discovery per manager.
3745
* Properties:
@@ -196,6 +204,14 @@ export interface IEventNamePropertyMapping {
196204
projectUnderRoot: number;
197205
};
198206

207+
/* __GDPR__
208+
"environment_tool_usage": {
209+
"toolName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "stellaHuang95" }
210+
}
211+
*/
212+
[EventNames.ENVIRONMENT_TOOL_USAGE]: {
213+
toolName: string;
214+
};
199215
/* __GDPR__
200216
"environment_discovery": {
201217
"managerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },

src/common/telemetry/helpers.ts

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
import { getDefaultEnvManagerSetting, getDefaultPkgManagerSetting } from '../../features/settings/settingHelpers';
22
import { EnvironmentManagers, PythonProjectManager } from '../../internal.api';
3+
import { getUvEnvironments } from '../../managers/builtin/uvEnvironments';
4+
import { ISSUES_URL } from '../constants';
5+
import { traceInfo, traceVerbose, traceWarn } from '../logging';
36
import { getWorkspaceFolders } from '../workspace.apis';
47
import { EventNames } from './constants';
58
import { sendTelemetryEvent } from './sender';
69

10+
/**
11+
* Extracts the base tool name from a manager ID.
12+
* Example: 'ms-python.python:venv' -> 'venv'
13+
* Example: 'ms-python.python:conda' -> 'conda'
14+
*/
15+
function extractToolName(managerId: string): string {
16+
// Manager IDs follow the pattern 'extensionId:toolName'
17+
const parts = managerId.split(':');
18+
return parts.length > 1 ? parts[1].toLowerCase() : managerId.toLowerCase();
19+
}
20+
721
export function sendManagerSelectionTelemetry(pm: PythonProjectManager) {
822
const ems: Set<string> = new Set();
923
const ps: Set<string> = new Set();
@@ -58,7 +72,7 @@ export async function sendProjectStructureTelemetry(
5872
for (const wsFolder of workspaceFolders) {
5973
const workspacePath = wsFolder.uri.fsPath;
6074
const projectPath = project.uri.fsPath;
61-
75+
6276
// Check if project is a subdirectory of workspace folder:
6377
// - Path must start with workspace path
6478
// - Path must not be equal to workspace path
@@ -80,3 +94,98 @@ export async function sendProjectStructureTelemetry(
8094
projectUnderRoot,
8195
});
8296
}
97+
98+
/**
99+
* Sends telemetry about which environment tools are actively used across all projects.
100+
* This tracks ACTUAL USAGE (which environments are set for projects), not just what's installed.
101+
*
102+
* Fires one event per tool that has at least one project using it.
103+
* This allows simple deduplication: dcount(machineId) by toolName gives unique users per tool.
104+
*
105+
* Called once at extension activation to understand user's environment tool usage patterns.
106+
*/
107+
export async function sendEnvironmentToolUsageTelemetry(
108+
pm: PythonProjectManager,
109+
envManagers: EnvironmentManagers,
110+
): Promise<void> {
111+
try {
112+
const projects = pm.getProjects();
113+
114+
// Track which tools are used (Set ensures uniqueness)
115+
const toolsUsed = new Set<string>();
116+
117+
// Lazily loaded once when a venv environment is first encountered
118+
let uvEnvPaths: string[] | undefined;
119+
120+
// Check which environment manager is used for each project
121+
for (const project of projects) {
122+
try {
123+
const env = await envManagers.getEnvironment(project.uri);
124+
if (env?.envId?.managerId) {
125+
let toolName = extractToolName(env.envId.managerId);
126+
127+
// UV environments share the venv manager. Check the persistent UV env list instead
128+
if (toolName === 'venv' && env.environmentPath) {
129+
uvEnvPaths ??= await getUvEnvironments();
130+
if (uvEnvPaths.includes(env.environmentPath.fsPath)) {
131+
toolName = 'uv';
132+
}
133+
}
134+
135+
// Normalize 'global' to 'system' for consistency
136+
if (toolName === 'global') {
137+
toolName = 'system';
138+
}
139+
140+
toolsUsed.add(toolName);
141+
}
142+
} catch {
143+
// Ignore errors when getting environment for a project
144+
}
145+
}
146+
147+
// Fire one event per tool used
148+
toolsUsed.forEach((tool) => {
149+
sendTelemetryEvent(EventNames.ENVIRONMENT_TOOL_USAGE, undefined, {
150+
toolName: tool,
151+
});
152+
});
153+
} catch (error) {
154+
// Telemetry failures must never disrupt extension activation
155+
traceVerbose('Failed to send environment tool usage telemetry:', error);
156+
}
157+
}
158+
159+
/**
160+
* Logs a summary of environment discovery results after startup.
161+
* If no environments are found, logs guidance to help users troubleshoot.
162+
*/
163+
export async function logDiscoverySummary(envManagers: EnvironmentManagers): Promise<void> {
164+
const managers = envManagers.managers;
165+
let totalEnvCount = 0;
166+
const managerSummaries: string[] = [];
167+
168+
for (const manager of managers) {
169+
try {
170+
const envs = await manager.getEnvironments('all');
171+
totalEnvCount += envs.length;
172+
if (envs.length > 0) {
173+
managerSummaries.push(`${manager.displayName}: ${envs.length}`);
174+
}
175+
} catch {
176+
// Discovery errors are already logged by InternalEnvironmentManager.refresh()
177+
}
178+
}
179+
180+
if (totalEnvCount === 0) {
181+
traceWarn(
182+
`No Python environments were found. ` +
183+
`Try running "Python Environments: Run Python Environment Tool (PET) in Terminal..." from the Command Palette to diagnose. ` +
184+
`If environments should be detected, please report this: ${ISSUES_URL}/new`,
185+
);
186+
} else {
187+
traceInfo(
188+
`Environment discovery complete: ${totalEnvCount} environments found (${managerSummaries.join(', ')})`,
189+
);
190+
}
191+
}

src/common/utils/asyncUtils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
import { traceError } from '../logging';
2+
13
export async function timeout(milliseconds: number): Promise<void> {
24
return new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
35
}
6+
7+
/**
8+
* Wraps a promise so that rejection is caught and logged instead of propagated.
9+
* Use with `Promise.all` to run tasks independently — one failure won't block the others.
10+
*/
11+
export async function safeRegister(name: string, task: Promise<void>): Promise<void> {
12+
try {
13+
await task;
14+
} catch (error) {
15+
traceError(`Failed to register ${name} features:`, error);
16+
}
17+
}

src/common/utils/pythonPath.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { Uri, Progress, CancellationToken } from 'vscode';
1+
import { CancellationToken, Progress, Uri } from 'vscode';
22
import { PythonEnvironment } from '../../api';
33
import { InternalEnvironmentManager } from '../../internal.api';
4-
import { traceVerbose, traceError } from '../logging';
54
import { PYTHON_EXTENSION_ID } from '../constants';
6-
import { showErrorMessage } from '../window.apis';
5+
import { traceVerbose, traceWarn } from '../logging';
76

87
const priorityOrder = [
98
`${PYTHON_EXTENSION_ID}:pyenv`,
@@ -74,7 +73,6 @@ export async function handlePythonPath(
7473
}
7574
}
7675

77-
traceError(`Unable to handle ${interpreterUri.fsPath}`);
78-
showErrorMessage(`Unable to handle ${interpreterUri.fsPath}`);
76+
traceWarn(`Unable to handle ${interpreterUri.fsPath}`);
7977
return undefined;
8078
}

src/extension.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@ import { clearPersistentState, setPersistentState } from './common/persistentSta
1717
import { newProjectSelection } from './common/pickers/managers';
1818
import { StopWatch } from './common/stopWatch';
1919
import { EventNames } from './common/telemetry/constants';
20-
import { sendManagerSelectionTelemetry, sendProjectStructureTelemetry } from './common/telemetry/helpers';
20+
import {
21+
logDiscoverySummary,
22+
sendEnvironmentToolUsageTelemetry,
23+
sendManagerSelectionTelemetry,
24+
sendProjectStructureTelemetry,
25+
} from './common/telemetry/helpers';
2126
import { sendTelemetryEvent } from './common/telemetry/sender';
27+
import { safeRegister } from './common/utils/asyncUtils';
2228
import { createDeferred } from './common/utils/deferred';
2329

2430
import {
@@ -522,13 +528,23 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
522528
context.subscriptions.push(nativeFinder);
523529
const sysMgr = new SysPythonManager(nativeFinder, api, outputChannel);
524530
sysPythonManager.resolve(sysMgr);
531+
// Each manager registers independently — one failure must not block the others.
525532
await Promise.all([
526-
registerSystemPythonFeatures(nativeFinder, context.subscriptions, outputChannel, sysMgr),
527-
registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager),
528-
registerPyenvFeatures(nativeFinder, context.subscriptions, projectManager),
529-
registerPipenvFeatures(nativeFinder, context.subscriptions, projectManager),
530-
registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager),
531-
shellStartupVarsMgr.initialize(),
533+
safeRegister(
534+
'system',
535+
registerSystemPythonFeatures(nativeFinder, context.subscriptions, outputChannel, sysMgr),
536+
),
537+
safeRegister(
538+
'conda',
539+
registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager),
540+
),
541+
safeRegister('pyenv', registerPyenvFeatures(nativeFinder, context.subscriptions, projectManager)),
542+
safeRegister('pipenv', registerPipenvFeatures(nativeFinder, context.subscriptions, projectManager)),
543+
safeRegister(
544+
'poetry',
545+
registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager),
546+
),
547+
safeRegister('shellStartupVars', shellStartupVarsMgr.initialize()),
532548
]);
533549

534550
await applyInitialEnvironmentSelection(envManagers, projectManager, nativeFinder, api);
@@ -545,6 +561,10 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
545561
await terminalManager.initialize(api);
546562
sendManagerSelectionTelemetry(projectManager);
547563
await sendProjectStructureTelemetry(projectManager, envManagers);
564+
await sendEnvironmentToolUsageTelemetry(projectManager, envManagers);
565+
566+
// Log discovery summary to help users troubleshoot environment detection issues
567+
await logDiscoverySummary(envManagers);
548568
} catch (error) {
549569
traceError('Failed to initialize environment managers:', error);
550570
// Show a user-friendly error message

src/features/common/managerReady.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import { installExtension } from '../../common/workbenchCommands';
1010
import { EnvironmentManagers, PythonProjectManager } from '../../internal.api';
1111
import { getDefaultEnvManagerSetting, getDefaultPkgManagerSetting } from '../settings/settingHelpers';
1212

13+
<<<<<<< smart-donkey
1314
export const MANAGER_READY_TIMEOUT_MS = 30_000;
15+
=======
16+
const MANAGER_READY_TIMEOUT_MS = 30_000;
17+
>>>>>>> main
1418

1519
interface ManagerReady extends Disposable {
1620
waitForEnvManager(uris?: Uri[]): Promise<void>;
@@ -153,6 +157,44 @@ class ManagerReadyImpl implements ManagerReady {
153157
}
154158
}
155159

160+
/**
161+
* Wraps a deferred with a timeout so a missing/dead manager cannot block the API forever.
162+
* On timeout the deferred is resolved (not rejected) so callers proceed with degraded results
163+
* instead of hanging.
164+
*/
165+
private _withTimeout(deferred: Deferred<void>, managerId: string, kind: string): Promise<void> {
166+
if (deferred.completed) {
167+
return deferred.promise;
168+
}
169+
return new Promise<void>((resolve) => {
170+
const timer = setTimeout(() => {
171+
if (!deferred.completed) {
172+
traceWarn(
173+
`Timed out after ${MANAGER_READY_TIMEOUT_MS / 1000}s waiting for ${kind} manager "${managerId}" to register. ` +
174+
`The manager may not be installed or its extension failed to activate. Proceeding without it. ` +
175+
`To prevent this, check your "python-envs.defaultEnvManager" and "python-envs.pythonProjects" settings.`,
176+
);
177+
sendTelemetryEvent(EventNames.MANAGER_READY_TIMEOUT, undefined, {
178+
managerId,
179+
managerKind: kind as 'environment' | 'package',
180+
});
181+
deferred.resolve();
182+
}
183+
}, MANAGER_READY_TIMEOUT_MS);
184+
185+
deferred.promise.then(
186+
() => {
187+
clearTimeout(timer);
188+
resolve();
189+
},
190+
() => {
191+
clearTimeout(timer);
192+
resolve();
193+
},
194+
);
195+
});
196+
}
197+
156198
public dispose(): void {
157199
this.disposables.forEach((d) => d.dispose());
158200
this.envManagers.clear();
@@ -165,7 +207,11 @@ class ManagerReadyImpl implements ManagerReady {
165207
}
166208
const deferred = createDeferred<void>();
167209
this.envManagers.set(managerId, deferred);
210+
<<<<<<< smart-donkey
168211
return withManagerTimeout(deferred, managerId, 'environment');
212+
=======
213+
return this._withTimeout(deferred, managerId, 'environment');
214+
>>>>>>> main
169215
}
170216

171217
public async waitForEnvManager(uris?: Uri[]): Promise<void> {
@@ -214,7 +260,11 @@ class ManagerReadyImpl implements ManagerReady {
214260
}
215261
const deferred = createDeferred<void>();
216262
this.pkgManagers.set(managerId, deferred);
263+
<<<<<<< smart-donkey
217264
return withManagerTimeout(deferred, managerId, 'package');
265+
=======
266+
return this._withTimeout(deferred, managerId, 'package');
267+
>>>>>>> main
218268
}
219269

220270
public async waitForPkgManager(uris?: Uri[]): Promise<void> {

0 commit comments

Comments
 (0)