Skip to content

Commit 19b1ee3

Browse files
authored
Merge branch 'main' into gothic-vicuna
2 parents ab57b73 + 0c7429b commit 19b1ee3

File tree

11 files changed

+395
-26
lines changed

11 files changed

+395
-26
lines changed

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: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
import { getDefaultEnvManagerSetting, getDefaultPkgManagerSetting } from '../../features/settings/settingHelpers';
22
import { EnvironmentManagers, PythonProjectManager } from '../../internal.api';
3+
import { getUvEnvironments } from '../../managers/builtin/uvEnvironments';
4+
import { traceVerbose } from '../logging';
35
import { getWorkspaceFolders } from '../workspace.apis';
46
import { EventNames } from './constants';
57
import { sendTelemetryEvent } from './sender';
68

9+
/**
10+
* Extracts the base tool name from a manager ID.
11+
* Example: 'ms-python.python:venv' -> 'venv'
12+
* Example: 'ms-python.python:conda' -> 'conda'
13+
*/
14+
function extractToolName(managerId: string): string {
15+
// Manager IDs follow the pattern 'extensionId:toolName'
16+
const parts = managerId.split(':');
17+
return parts.length > 1 ? parts[1].toLowerCase() : managerId.toLowerCase();
18+
}
19+
720
export function sendManagerSelectionTelemetry(pm: PythonProjectManager) {
821
const ems: Set<string> = new Set();
922
const ps: Set<string> = new Set();
@@ -58,7 +71,7 @@ export async function sendProjectStructureTelemetry(
5871
for (const wsFolder of workspaceFolders) {
5972
const workspacePath = wsFolder.uri.fsPath;
6073
const projectPath = project.uri.fsPath;
61-
74+
6275
// Check if project is a subdirectory of workspace folder:
6376
// - Path must start with workspace path
6477
// - Path must not be equal to workspace path
@@ -80,3 +93,64 @@ export async function sendProjectStructureTelemetry(
8093
projectUnderRoot,
8194
});
8295
}
96+
97+
/**
98+
* Sends telemetry about which environment tools are actively used across all projects.
99+
* This tracks ACTUAL USAGE (which environments are set for projects), not just what's installed.
100+
*
101+
* Fires one event per tool that has at least one project using it.
102+
* This allows simple deduplication: dcount(machineId) by toolName gives unique users per tool.
103+
*
104+
* Called once at extension activation to understand user's environment tool usage patterns.
105+
*/
106+
export async function sendEnvironmentToolUsageTelemetry(
107+
pm: PythonProjectManager,
108+
envManagers: EnvironmentManagers,
109+
): Promise<void> {
110+
try {
111+
const projects = pm.getProjects();
112+
113+
// Track which tools are used (Set ensures uniqueness)
114+
const toolsUsed = new Set<string>();
115+
116+
// Lazily loaded once when a venv environment is first encountered
117+
let uvEnvPaths: string[] | undefined;
118+
119+
// Check which environment manager is used for each project
120+
for (const project of projects) {
121+
try {
122+
const env = await envManagers.getEnvironment(project.uri);
123+
if (env?.envId?.managerId) {
124+
let toolName = extractToolName(env.envId.managerId);
125+
126+
// UV environments share the venv manager. Check the persistent UV env list instead
127+
if (toolName === 'venv' && env.environmentPath) {
128+
uvEnvPaths ??= await getUvEnvironments();
129+
if (uvEnvPaths.includes(env.environmentPath.fsPath)) {
130+
toolName = 'uv';
131+
}
132+
}
133+
134+
// Normalize 'global' to 'system' for consistency
135+
if (toolName === 'global') {
136+
toolName = 'system';
137+
}
138+
139+
toolsUsed.add(toolName);
140+
}
141+
} catch {
142+
// Ignore errors when getting environment for a project
143+
}
144+
}
145+
146+
// Fire one event per tool used
147+
toolsUsed.forEach((tool) => {
148+
sendTelemetryEvent(EventNames.ENVIRONMENT_TOOL_USAGE, undefined, {
149+
toolName: tool,
150+
});
151+
});
152+
} catch (error) {
153+
// Telemetry failures must never disrupt extension activation
154+
traceVerbose('Failed to send environment tool usage telemetry:', error);
155+
}
156+
}

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: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ 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+
sendEnvironmentToolUsageTelemetry,
22+
sendManagerSelectionTelemetry,
23+
sendProjectStructureTelemetry,
24+
} from './common/telemetry/helpers';
2125
import { sendTelemetryEvent } from './common/telemetry/sender';
2226
import { createDeferred } from './common/utils/deferred';
2327

@@ -542,11 +546,11 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
542546
);
543547

544548
sendTelemetryEvent(EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION, start.elapsedTime);
545-
546549
try {
547550
await terminalManager.initialize(api);
548551
sendManagerSelectionTelemetry(projectManager);
549552
await sendProjectStructureTelemetry(projectManager, envManagers);
553+
await sendEnvironmentToolUsageTelemetry(projectManager, envManagers);
550554
} catch (postInitError) {
551555
traceError('Post-initialization tasks failed:', postInitError);
552556
}

src/features/envCommands.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import * as fs from 'fs-extra';
22
import * as path from 'path';
3-
import { ProgressLocation, QuickInputButtons, TaskExecution, TaskRevealKind, Terminal, Uri, workspace } from 'vscode';
3+
import {
4+
ProgressLocation,
5+
QuickInputButtons,
6+
TaskExecution,
7+
TaskRevealKind,
8+
Terminal,
9+
Uri,
10+
l10n,
11+
workspace,
12+
} from 'vscode';
413
import {
514
CreateEnvironmentOptions,
615
PythonEnvironment,
@@ -93,6 +102,10 @@ async function browseAndResolveInterpreter(
93102
},
94103
);
95104

105+
if (!environment) {
106+
showErrorMessage(l10n.t('Selected file is not a valid Python interpreter: {0}', interpreterUri.fsPath));
107+
}
108+
96109
return environment;
97110
}
98111

src/features/interpreterSelection.ts

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -107,20 +107,32 @@ async function resolvePriorityChainCore(
107107
const userInterpreterPath = getUserConfiguredSetting<string>('python', 'defaultInterpreterPath', scope);
108108
if (userInterpreterPath) {
109109
const expandedInterpreterPath = resolveVariables(userInterpreterPath, scope);
110-
const resolved = await tryResolveInterpreterPath(nativeFinder, api, expandedInterpreterPath, envManagers);
111-
if (resolved) {
112-
traceVerbose(`${logPrefix} Priority 3: Using defaultInterpreterPath: ${userInterpreterPath}`);
113-
return { result: resolved, errors };
110+
if (expandedInterpreterPath.includes('${')) {
111+
traceWarn(
112+
`${logPrefix} defaultInterpreterPath '${userInterpreterPath}' contains unresolved variables, falling back to auto-discovery`,
113+
);
114+
const error: SettingResolutionError = {
115+
setting: 'defaultInterpreterPath',
116+
configuredValue: userInterpreterPath,
117+
reason: l10n.t('Path contains unresolved variables'),
118+
};
119+
errors.push(error);
120+
} else {
121+
const resolved = await tryResolveInterpreterPath(nativeFinder, api, expandedInterpreterPath, envManagers);
122+
if (resolved) {
123+
traceVerbose(`${logPrefix} Priority 3: Using defaultInterpreterPath: ${userInterpreterPath}`);
124+
return { result: resolved, errors };
125+
}
126+
const error: SettingResolutionError = {
127+
setting: 'defaultInterpreterPath',
128+
configuredValue: userInterpreterPath,
129+
reason: `Could not resolve interpreter path '${userInterpreterPath}'`,
130+
};
131+
errors.push(error);
132+
traceWarn(
133+
`${logPrefix} defaultInterpreterPath '${userInterpreterPath}' unresolvable, falling back to auto-discovery`,
134+
);
114135
}
115-
const error: SettingResolutionError = {
116-
setting: 'defaultInterpreterPath',
117-
configuredValue: userInterpreterPath,
118-
reason: `Could not resolve interpreter path '${userInterpreterPath}'`,
119-
};
120-
errors.push(error);
121-
traceWarn(
122-
`${logPrefix} defaultInterpreterPath '${userInterpreterPath}' unresolvable, falling back to auto-discovery`,
123-
);
124136
}
125137

126138
// PRIORITY 4: Auto-discovery (no user-configured settings matched)

src/managers/common/nativePythonFinder.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,15 @@ function getGlobalSearchPaths(): string[] {
842842
}
843843
}
844844

845+
let workspaceSearchPathsGlobalWarningShown = false;
846+
847+
/**
848+
* @internal Test-only helper to reset the workspaceSearchPaths global-level warning flag.
849+
*/
850+
export function resetWorkspaceSearchPathsGlobalWarningFlag(): void {
851+
workspaceSearchPathsGlobalWarningShown = false;
852+
}
853+
845854
/**
846855
* Gets the most specific workspace-level setting available for workspaceSearchPaths.
847856
* Supports glob patterns which are expanded by PET.
@@ -851,7 +860,8 @@ function getWorkspaceSearchPaths(): string[] {
851860
const envConfig = getConfiguration('python-envs');
852861
const inspection = envConfig.inspect<string[]>('workspaceSearchPaths');
853862

854-
if (inspection?.globalValue) {
863+
if (inspection?.globalValue && !workspaceSearchPathsGlobalWarningShown) {
864+
workspaceSearchPathsGlobalWarningShown = true;
855865
traceError(
856866
'python-envs.workspaceSearchPaths is set at the user/global level, but this setting can only be set at the workspace or workspace folder level.',
857867
);

0 commit comments

Comments
 (0)