Skip to content

Commit 1893737

Browse files
authored
Merge branch 'main' into youngest-aardwolf
2 parents 6a12891 + e192ffe commit 1893737

File tree

17 files changed

+629
-42
lines changed

17 files changed

+629
-42
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"git.branchProtectionPrompt": "alwaysCommitToNewBranch",
3232
"chat.tools.terminal.autoApprove": {
3333
"npx tsc": true,
34-
"mkdir": true
34+
"mkdir": true,
35+
"npx mocha": true
3536
}
3637
}

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: 28 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:
@@ -41,6 +49,7 @@ export enum EventNames {
4149
* - errorType: string (error class name, on failure only)
4250
*/
4351
ENVIRONMENT_DISCOVERY = 'ENVIRONMENT_DISCOVERY',
52+
MANAGER_READY_TIMEOUT = 'MANAGER_READY.TIMEOUT',
4453
}
4554

4655
// Map all events to their properties
@@ -195,6 +204,14 @@ export interface IEventNamePropertyMapping {
195204
projectUnderRoot: number;
196205
};
197206

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+
};
198215
/* __GDPR__
199216
"environment_discovery": {
200217
"managerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
@@ -210,4 +227,15 @@ export interface IEventNamePropertyMapping {
210227
envCount?: number;
211228
errorType?: string;
212229
};
230+
231+
/* __GDPR__
232+
"manager_ready.timeout": {
233+
"managerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
234+
"managerKind": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
235+
}
236+
*/
237+
[EventNames.MANAGER_READY_TIMEOUT]: {
238+
managerId: string;
239+
managerKind: 'environment' | 'package';
240+
};
213241
}

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`,
@@ -76,7 +75,6 @@ export async function handlePythonPath(
7675
}
7776
}
7877

79-
traceError(`Unable to handle ${interpreterUri.fsPath}`);
80-
showErrorMessage(`Unable to handle ${interpreterUri.fsPath}`);
78+
traceWarn(`Unable to handle ${interpreterUri.fsPath}`);
8179
return undefined;
8280
}

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);
@@ -546,6 +562,10 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
546562
await sendProjectStructureTelemetry(projectManager, envManagers);
547563

548564
sendTelemetryEvent(EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION, start.elapsedTime);
565+
await sendEnvironmentToolUsageTelemetry(projectManager, envManagers);
566+
567+
// Log discovery summary to help users troubleshoot environment detection issues
568+
await logDiscoverySummary(envManagers);
549569
} catch (error) {
550570
traceError('Failed to initialize environment managers:', error);
551571
sendTelemetryEvent(

0 commit comments

Comments
 (0)