Skip to content

Commit 0047da9

Browse files
authored
Merge branch 'main' into confused-mule
2 parents f7fe643 + f2a697b commit 0047da9

19 files changed

+1479
-141
lines changed

examples/sample1/package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

Lines changed: 434 additions & 88 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "vscode-python-envs",
33
"displayName": "Python Environments",
44
"description": "Provides a unified python environment experience",
5-
"version": "1.23.0",
5+
"version": "1.25.0",
66
"publisher": "ms-python",
77
"preview": true,
88
"engines": {
@@ -699,7 +699,7 @@
699699
"@types/which": "^3.0.4",
700700
"@typescript-eslint/eslint-plugin": "^8.16.0",
701701
"@typescript-eslint/parser": "^8.16.0",
702-
"@vscode/test-electron": "^2.3.2",
702+
"@vscode/test-electron": "^2.5.2",
703703
"@vscode/vsce": "^2.24.0",
704704
"eslint": "^9.15.0",
705705
"glob": "^8.1.0",

src/common/telemetry/constants.ts

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,38 @@ export enum EventNames {
5050
*/
5151
ENVIRONMENT_DISCOVERY = 'ENVIRONMENT_DISCOVERY',
5252
MANAGER_READY_TIMEOUT = 'MANAGER_READY.TIMEOUT',
53+
/**
54+
* Telemetry event for individual manager registration failure.
55+
* Fires once per manager that fails during registration (inside safeRegister).
56+
* Properties:
57+
* - managerName: string (e.g. 'system', 'conda', 'pyenv', 'pipenv', 'poetry', 'shellStartupVars')
58+
* - errorType: string (classified error category from classifyError)
59+
*/
60+
MANAGER_REGISTRATION_FAILED = 'MANAGER_REGISTRATION.FAILED',
61+
/**
62+
* Telemetry event fired when the setup block appears to be hung.
63+
* A watchdog timer fires after a deadline; if the setup completes normally,
64+
* the timer is cancelled and this event never fires.
65+
* Properties:
66+
* - failureStage: string (which phase was in progress when the watchdog fired)
67+
*/
68+
SETUP_HANG_DETECTED = 'SETUP.HANG_DETECTED',
69+
/**
70+
* Telemetry event for when a manager skips registration because its tool was not found.
71+
* This is an expected outcome (not an error) and is distinct from MANAGER_REGISTRATION_FAILED.
72+
* Properties:
73+
* - managerName: string (e.g. 'conda', 'pyenv', 'pipenv', 'poetry')
74+
* - reason: string ('tool_not_found')
75+
*/
76+
MANAGER_REGISTRATION_SKIPPED = 'MANAGER_REGISTRATION.SKIPPED',
77+
/**
78+
* Telemetry event for PET (Python Environment Tools) initialization timing.
79+
* Tracks how long it takes to create and start the native Python finder.
80+
* Properties:
81+
* - result: 'success' | 'error' | 'timeout'
82+
* - errorType: string (classified error category, on failure only)
83+
*/
84+
PET_INIT_DURATION = 'PET.INIT_DURATION',
5385
}
5486

5587
// Map all events to their properties
@@ -62,10 +94,17 @@ export interface IEventNamePropertyMapping {
6294
[EventNames.EXTENSION_ACTIVATION_DURATION]: never | undefined;
6395
/* __GDPR__
6496
"extension.manager_registration_duration": {
65-
"duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
97+
"duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
98+
"result" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" },
99+
"failureStage" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" },
100+
"errorType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" }
66101
}
67102
*/
68-
[EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION]: never | undefined;
103+
[EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION]: {
104+
result: 'success' | 'error';
105+
failureStage?: string;
106+
errorType?: string;
107+
};
69108

70109
/* __GDPR__
71110
"environment_manager.registered": {
@@ -239,4 +278,48 @@ export interface IEventNamePropertyMapping {
239278
managerId: string;
240279
managerKind: 'environment' | 'package';
241280
};
281+
282+
/* __GDPR__
283+
"manager_registration.failed": {
284+
"managerName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" },
285+
"errorType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" }
286+
}
287+
*/
288+
[EventNames.MANAGER_REGISTRATION_FAILED]: {
289+
managerName: string;
290+
errorType: string;
291+
};
292+
293+
/* __GDPR__
294+
"setup.hang_detected": {
295+
"failureStage": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" },
296+
"<duration>": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "StellaHuang95" }
297+
}
298+
*/
299+
[EventNames.SETUP_HANG_DETECTED]: {
300+
failureStage: string;
301+
};
302+
303+
/* __GDPR__
304+
"manager_registration.skipped": {
305+
"managerName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" },
306+
"reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" }
307+
}
308+
*/
309+
[EventNames.MANAGER_REGISTRATION_SKIPPED]: {
310+
managerName: string;
311+
reason: 'tool_not_found';
312+
};
313+
314+
/* __GDPR__
315+
"pet.init_duration": {
316+
"result": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
317+
"errorType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
318+
"<duration>": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" }
319+
}
320+
*/
321+
[EventNames.PET_INIT_DURATION]: {
322+
result: 'success' | 'error' | 'timeout';
323+
errorType?: string;
324+
};
242325
}

src/common/utils/asyncUtils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { traceError } from '../logging';
2+
import { EventNames } from '../telemetry/constants';
3+
import { classifyError } from '../telemetry/errorClassifier';
4+
import { sendTelemetryEvent } from '../telemetry/sender';
25

36
export async function timeout(milliseconds: number): Promise<void> {
47
return new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
@@ -13,5 +16,9 @@ export async function safeRegister(name: string, task: Promise<void>): Promise<v
1316
await task;
1417
} catch (error) {
1518
traceError(`Failed to register ${name} features:`, error);
19+
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_FAILED, undefined, {
20+
managerName: name,
21+
errorType: classifyError(error),
22+
});
1623
}
1724
}

src/extension.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ 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 { classifyError } from './common/telemetry/errorClassifier';
2021
import {
2122
logDiscoverySummary,
2223
sendEnvironmentToolUsageTelemetry,
@@ -522,13 +523,47 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
522523
* Below are all the contributed features using the APIs.
523524
*/
524525
setImmediate(async () => {
526+
let failureStage = 'nativeFinder';
527+
// Watchdog: fires if setup hasn't completed within 120s, indicating a likely hang
528+
const SETUP_HANG_TIMEOUT_MS = 120_000;
529+
let hangWatchdogActive = true;
530+
const clearHangWatchdog = () => {
531+
if (!hangWatchdogActive) {
532+
return;
533+
}
534+
hangWatchdogActive = false;
535+
clearTimeout(hangWatchdog);
536+
};
537+
const hangWatchdog = setTimeout(() => {
538+
if (!hangWatchdogActive) {
539+
return;
540+
}
541+
hangWatchdogActive = false;
542+
traceError(`Setup appears hung during stage: ${failureStage}`);
543+
sendTelemetryEvent(EventNames.SETUP_HANG_DETECTED, start.elapsedTime, { failureStage });
544+
}, SETUP_HANG_TIMEOUT_MS);
545+
context.subscriptions.push({ dispose: clearHangWatchdog });
525546
try {
526547
// This is the finder that is used by all the built in environment managers
527-
const nativeFinder: NativePythonFinder = await createNativePythonFinder(outputChannel, api, context);
548+
const petStart = new StopWatch();
549+
let nativeFinder: NativePythonFinder;
550+
try {
551+
nativeFinder = await createNativePythonFinder(outputChannel, api, context);
552+
sendTelemetryEvent(EventNames.PET_INIT_DURATION, petStart.elapsedTime, { result: 'success' });
553+
} catch (petError) {
554+
sendTelemetryEvent(
555+
EventNames.PET_INIT_DURATION,
556+
petStart.elapsedTime,
557+
{ result: 'error', errorType: classifyError(petError) },
558+
petError instanceof Error ? petError : undefined,
559+
);
560+
throw petError;
561+
}
528562
context.subscriptions.push(nativeFinder);
529563
const sysMgr = new SysPythonManager(nativeFinder, api, outputChannel);
530564
sysPythonManager.resolve(sysMgr);
531565
// Each manager registers independently — one failure must not block the others.
566+
failureStage = 'managerRegistration';
532567
await Promise.all([
533568
safeRegister(
534569
'system',
@@ -547,17 +582,23 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
547582
safeRegister('shellStartupVars', shellStartupVarsMgr.initialize()),
548583
]);
549584

585+
failureStage = 'envSelection';
550586
await applyInitialEnvironmentSelection(envManagers, projectManager, nativeFinder, api);
551587

552588
// Register manager-agnostic terminal watcher for package-modifying commands
589+
failureStage = 'terminalWatcher';
553590
registerTerminalPackageWatcher(api, terminalActivation, outputChannel, context.subscriptions);
554591

555592
// Register listener for interpreter settings changes for interpreter re-selection
593+
failureStage = 'settingsListener';
556594
context.subscriptions.push(
557595
registerInterpreterSettingsChangeListener(envManagers, projectManager, nativeFinder, api),
558596
);
559597

560-
sendTelemetryEvent(EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION, start.elapsedTime);
598+
sendTelemetryEvent(EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION, start.elapsedTime, {
599+
result: 'success',
600+
});
601+
clearHangWatchdog();
561602
try {
562603
await terminalManager.initialize(api);
563604
sendManagerSelectionTelemetry(projectManager);
@@ -570,11 +611,16 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
570611
traceError('Post-initialization tasks failed:', postInitError);
571612
}
572613
} catch (error) {
614+
clearHangWatchdog();
573615
traceError('Failed to initialize environment managers:', error);
574616
sendTelemetryEvent(
575617
EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION,
576618
start.elapsedTime,
577-
undefined,
619+
{
620+
result: 'error',
621+
failureStage,
622+
errorType: classifyError(error),
623+
},
578624
error instanceof Error ? error : undefined,
579625
);
580626
// Show a user-friendly error message

src/features/interpreterSelection.ts

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -108,30 +108,53 @@ async function resolvePriorityChainCore(
108108
if (userInterpreterPath) {
109109
const expandedInterpreterPath = resolveVariables(userInterpreterPath, scope);
110110
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);
111+
if (scope) {
112+
// Workspace scope: unresolved variables are a genuine configuration error
113+
traceWarn(
114+
`${logPrefix} defaultInterpreterPath '${userInterpreterPath}' contains unresolved variables, falling back to auto-discovery`,
115+
);
116+
const error: SettingResolutionError = {
117+
setting: 'defaultInterpreterPath',
118+
configuredValue: userInterpreterPath,
119+
reason: l10n.t('Path contains unresolved variables'),
120+
};
121+
errors.push(error);
122+
} else {
123+
// Global scope: workspace-specific variables like ${workspaceFolder} can't resolve here.
124+
// This is expected when a workspace-level setting uses workspace variables —
125+
// the per-folder chain handles them correctly. Silently skip to auto-discovery.
126+
traceVerbose(
127+
`${logPrefix} defaultInterpreterPath '${userInterpreterPath}' contains workspace-specific variables, skipping for global scope`,
128+
);
129+
}
120130
} 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 };
131+
const expandedInterpreterPath = resolveVariables(userInterpreterPath, scope);
132+
if (expandedInterpreterPath.includes('${')) {
133+
traceWarn(
134+
`${logPrefix} defaultInterpreterPath '${userInterpreterPath}' contains unresolved variables, falling back to auto-discovery`,
135+
);
136+
const error: SettingResolutionError = {
137+
setting: 'defaultInterpreterPath',
138+
configuredValue: userInterpreterPath,
139+
reason: l10n.t('Path contains unresolved variables'),
140+
};
141+
errors.push(error);
142+
} else {
143+
const resolved = await tryResolveInterpreterPath(nativeFinder, api, expandedInterpreterPath, envManagers);
144+
if (resolved) {
145+
traceVerbose(`${logPrefix} Priority 3: Using defaultInterpreterPath: ${userInterpreterPath}`);
146+
return { result: resolved, errors };
147+
}
148+
const error: SettingResolutionError = {
149+
setting: 'defaultInterpreterPath',
150+
configuredValue: userInterpreterPath,
151+
reason: `Could not resolve interpreter path '${userInterpreterPath}'`,
152+
};
153+
errors.push(error);
154+
traceWarn(
155+
`${logPrefix} defaultInterpreterPath '${userInterpreterPath}' unresolvable, falling back to auto-discovery`,
156+
);
125157
}
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-
);
135158
}
136159
}
137160

src/managers/builtin/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export async function refreshPythons(
125125
NativePythonEnvironmentKind.macXCode,
126126
NativePythonEnvironmentKind.windowsRegistry,
127127
NativePythonEnvironmentKind.windowsStore,
128+
NativePythonEnvironmentKind.winpython,
128129
].includes(e.kind)),
129130
);
130131
envs.forEach((env) => {

0 commit comments

Comments
 (0)