Skip to content

Commit f549c7c

Browse files
committed
Merge upstream/main: resolve conflicts keeping both logDiscoverySummary and sendEnvironmentToolUsageTelemetry
2 parents d92b648 + 0c7429b commit f549c7c

File tree

11 files changed

+390
-24
lines changed

11 files changed

+390
-24
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:
@@ -195,6 +203,14 @@ export interface IEventNamePropertyMapping {
195203
projectUnderRoot: number;
196204
};
197205

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

src/common/telemetry/helpers.ts

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

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+
921
export function sendManagerSelectionTelemetry(pm: PythonProjectManager) {
1022
const ems: Set<string> = new Set();
1123
const ps: Set<string> = new Set();
@@ -83,6 +95,67 @@ export async function sendProjectStructureTelemetry(
8395
});
8496
}
8597

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+
86159
/**
87160
* Logs a summary of environment discovery results after startup.
88161
* If no environments are found, logs guidance to help users troubleshoot.

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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { StopWatch } from './common/stopWatch';
1919
import { EventNames } from './common/telemetry/constants';
2020
import {
2121
logDiscoverySummary,
22+
sendEnvironmentToolUsageTelemetry,
2223
sendManagerSelectionTelemetry,
2324
sendProjectStructureTelemetry,
2425
} from './common/telemetry/helpers';
@@ -549,6 +550,7 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
549550
await terminalManager.initialize(api);
550551
sendManagerSelectionTelemetry(projectManager);
551552
await sendProjectStructureTelemetry(projectManager, envManagers);
553+
await sendEnvironmentToolUsageTelemetry(projectManager, envManagers);
552554

553555
// Log discovery summary to help users troubleshoot environment detection issues
554556
await logDiscoverySummary(envManagers);

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)