Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 65 additions & 2 deletions src/common/telemetry/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,30 @@ export enum EventNames {
*/
ENVIRONMENT_DISCOVERY = 'ENVIRONMENT_DISCOVERY',
MANAGER_READY_TIMEOUT = 'MANAGER_READY.TIMEOUT',
/**
* Telemetry event for individual manager registration failure.
* Fires once per manager that fails during registration (inside safeRegister).
* Properties:
* - managerName: string (e.g. 'system', 'conda', 'pyenv', 'pipenv', 'poetry', 'shellStartupVars')
* - errorType: string (classified error category from classifyError)
*/
MANAGER_REGISTRATION_FAILED = 'MANAGER_REGISTRATION.FAILED',
/**
* Telemetry event fired when the setup block appears to be hung.
* A watchdog timer fires after a deadline; if the setup completes normally,
* the timer is cancelled and this event never fires.
* Properties:
* - failureStage: string (which phase was in progress when the watchdog fired)
*/
SETUP_HANG_DETECTED = 'SETUP.HANG_DETECTED',
/**
* Telemetry event for when a manager skips registration because its tool was not found.
* This is an expected outcome (not an error) and is distinct from MANAGER_REGISTRATION_FAILED.
* Properties:
* - managerName: string (e.g. 'conda', 'pyenv', 'pipenv', 'poetry')
* - reason: string ('tool_not_found')
*/
MANAGER_REGISTRATION_SKIPPED = 'MANAGER_REGISTRATION.SKIPPED',
}

// Map all events to their properties
Expand All @@ -62,10 +86,17 @@ export interface IEventNamePropertyMapping {
[EventNames.EXTENSION_ACTIVATION_DURATION]: never | undefined;
/* __GDPR__
"extension.manager_registration_duration": {
"duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
"duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
"result" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" },
"failureStage" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" },
"errorType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" }
}
*/
[EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION]: never | undefined;
[EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION]: {
result: 'success' | 'error';
failureStage?: string;
errorType?: string;
};

/* __GDPR__
"environment_manager.registered": {
Expand Down Expand Up @@ -239,4 +270,36 @@ export interface IEventNamePropertyMapping {
managerId: string;
managerKind: 'environment' | 'package';
};

/* __GDPR__
"manager_registration.failed": {
"managerName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" },
"errorType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" }
}
*/
[EventNames.MANAGER_REGISTRATION_FAILED]: {
managerName: string;
errorType: string;
};

/* __GDPR__
"setup.hang_detected": {
"failureStage": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" },
"<duration>": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "StellaHuang95" }
}
*/
[EventNames.SETUP_HANG_DETECTED]: {
failureStage: string;
};

/* __GDPR__
"manager_registration.skipped": {
"managerName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" },
"reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" }
}
*/
[EventNames.MANAGER_REGISTRATION_SKIPPED]: {
managerName: string;
reason: 'tool_not_found';
};
}
7 changes: 7 additions & 0 deletions src/common/utils/asyncUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { traceError } from '../logging';
import { EventNames } from '../telemetry/constants';
import { classifyError } from '../telemetry/errorClassifier';
import { sendTelemetryEvent } from '../telemetry/sender';

export async function timeout(milliseconds: number): Promise<void> {
return new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
Expand All @@ -13,5 +16,9 @@ export async function safeRegister(name: string, task: Promise<void>): Promise<v
await task;
} catch (error) {
traceError(`Failed to register ${name} features:`, error);
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_FAILED, undefined, {
managerName: name,
errorType: classifyError(error),
});
}
}
37 changes: 35 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { clearPersistentState, setPersistentState } from './common/persistentSta
import { newProjectSelection } from './common/pickers/managers';
import { StopWatch } from './common/stopWatch';
import { EventNames } from './common/telemetry/constants';
import { classifyError } from './common/telemetry/errorClassifier';
import {
logDiscoverySummary,
sendEnvironmentToolUsageTelemetry,
Expand Down Expand Up @@ -522,13 +523,34 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
* Below are all the contributed features using the APIs.
*/
setImmediate(async () => {
let failureStage = 'nativeFinder';
// Watchdog: fires if setup hasn't completed within 120s, indicating a likely hang
const SETUP_HANG_TIMEOUT_MS = 120_000;
let hangWatchdogActive = true;
const clearHangWatchdog = () => {
if (!hangWatchdogActive) {
return;
}
hangWatchdogActive = false;
clearTimeout(hangWatchdog);
};
const hangWatchdog = setTimeout(() => {
if (!hangWatchdogActive) {
return;
}
hangWatchdogActive = false;
traceError(`Setup appears hung during stage: ${failureStage}`);
sendTelemetryEvent(EventNames.SETUP_HANG_DETECTED, start.elapsedTime, { failureStage });
}, SETUP_HANG_TIMEOUT_MS);
context.subscriptions.push({ dispose: clearHangWatchdog });
try {
// This is the finder that is used by all the built in environment managers
const nativeFinder: NativePythonFinder = await createNativePythonFinder(outputChannel, api, context);
context.subscriptions.push(nativeFinder);
const sysMgr = new SysPythonManager(nativeFinder, api, outputChannel);
sysPythonManager.resolve(sysMgr);
// Each manager registers independently — one failure must not block the others.
failureStage = 'managerRegistration';
await Promise.all([
safeRegister(
'system',
Expand All @@ -547,17 +569,23 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
safeRegister('shellStartupVars', shellStartupVarsMgr.initialize()),
]);

failureStage = 'envSelection';
await applyInitialEnvironmentSelection(envManagers, projectManager, nativeFinder, api);

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

// Register listener for interpreter settings changes for interpreter re-selection
failureStage = 'settingsListener';
context.subscriptions.push(
registerInterpreterSettingsChangeListener(envManagers, projectManager, nativeFinder, api),
);

sendTelemetryEvent(EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION, start.elapsedTime);
sendTelemetryEvent(EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION, start.elapsedTime, {
result: 'success',
});
clearHangWatchdog();
try {
await terminalManager.initialize(api);
sendManagerSelectionTelemetry(projectManager);
Expand All @@ -570,11 +598,16 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
traceError('Post-initialization tasks failed:', postInitError);
}
} catch (error) {
clearHangWatchdog();
traceError('Failed to initialize environment managers:', error);
sendTelemetryEvent(
EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION,
start.elapsedTime,
undefined,
{
result: 'error',
failureStage,
errorType: classifyError(error),
},
error instanceof Error ? error : undefined,
);
// Show a user-friendly error message
Expand Down
19 changes: 17 additions & 2 deletions src/managers/conda/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Disposable, LogOutputChannel } from 'vscode';
import { PythonEnvironmentApi } from '../../api';
import { traceInfo } from '../../common/logging';
import { EventNames } from '../../common/telemetry/constants';
import { sendTelemetryEvent } from '../../common/telemetry/sender';
import { getPythonApi } from '../../features/pythonApi';
import { PythonProjectManager } from '../../internal.api';
import { NativePythonFinder } from '../common/nativePythonFinder';
Expand All @@ -18,9 +20,22 @@ export async function registerCondaFeatures(
): Promise<void> {
const api: PythonEnvironmentApi = await getPythonApi();

let condaPath: string | undefined;
try {
// get Conda will return only ONE conda manager, that correlates to a single conda install
const condaPath: string = await getConda(nativeFinder);
condaPath = await getConda(nativeFinder);
} catch (ex) {
traceInfo('Conda not found, turning off conda features.', ex);
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_SKIPPED, undefined, {
managerName: 'conda',
reason: 'tool_not_found',
});
await notifyMissingManagerIfDefault('ms-python.python:conda', projectManager, api);
return;
}

// Conda was found — errors below are real registration failures (let safeRegister handle telemetry)
try {
const sourcingStatus: CondaSourcingStatus = await constructCondaSourcingStatus(condaPath);
traceInfo(sourcingStatus.toString());

Expand All @@ -36,7 +51,7 @@ export async function registerCondaFeatures(
api.registerPackageManager(packageManager),
);
} catch (ex) {
traceInfo('Conda not found, turning off conda features.', ex);
await notifyMissingManagerIfDefault('ms-python.python:conda', projectManager, api);
throw ex;
}
}
11 changes: 11 additions & 0 deletions src/managers/pipenv/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Disposable } from 'vscode';
import { PythonEnvironmentApi } from '../../api';
import { traceInfo } from '../../common/logging';
import { EventNames } from '../../common/telemetry/constants';
import { classifyError } from '../../common/telemetry/errorClassifier';
import { sendTelemetryEvent } from '../../common/telemetry/sender';
import { getPythonApi } from '../../features/pythonApi';
import { PythonProjectManager } from '../../internal.api';
import { NativePythonFinder } from '../common/nativePythonFinder';
Expand Down Expand Up @@ -35,13 +38,21 @@ export async function registerPipenvFeatures(
traceInfo(
'Pipenv not found, turning off pipenv features. If you have pipenv installed in a non-standard location, set the "python.pipenvPath" setting.',
);
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_SKIPPED, undefined, {
managerName: 'pipenv',
reason: 'tool_not_found',
});
await notifyMissingManagerIfDefault('ms-python.python:pipenv', projectManager, api);
}
} catch (ex) {
traceInfo(
'Pipenv not found, turning off pipenv features. If you have pipenv installed in a non-standard location, set the "python.pipenvPath" setting.',
ex,
);
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_FAILED, undefined, {
managerName: 'pipenv',
errorType: classifyError(ex),
});
await notifyMissingManagerIfDefault('ms-python.python:pipenv', projectManager, api);
}
}
11 changes: 11 additions & 0 deletions src/managers/poetry/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Disposable, LogOutputChannel } from 'vscode';
import { PythonEnvironmentApi } from '../../api';
import { traceInfo } from '../../common/logging';
import { EventNames } from '../../common/telemetry/constants';
import { classifyError } from '../../common/telemetry/errorClassifier';
import { sendTelemetryEvent } from '../../common/telemetry/sender';
import { getPythonApi } from '../../features/pythonApi';
import { PythonProjectManager } from '../../internal.api';
import { NativePythonFinder } from '../common/nativePythonFinder';
Expand Down Expand Up @@ -36,10 +39,18 @@ export async function registerPoetryFeatures(
);
} else {
traceInfo('Poetry not found, turning off poetry features.');
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_SKIPPED, undefined, {
managerName: 'poetry',
reason: 'tool_not_found',
});
await notifyMissingManagerIfDefault('ms-python.python:poetry', projectManager, api);
}
} catch (ex) {
traceInfo('Poetry not found, turning off poetry features.', ex);
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_FAILED, undefined, {
managerName: 'poetry',
errorType: classifyError(ex),
});
await notifyMissingManagerIfDefault('ms-python.python:poetry', projectManager, api);
}
}
11 changes: 11 additions & 0 deletions src/managers/pyenv/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Disposable } from 'vscode';
import { PythonEnvironmentApi } from '../../api';
import { traceInfo } from '../../common/logging';
import { EventNames } from '../../common/telemetry/constants';
import { classifyError } from '../../common/telemetry/errorClassifier';
import { sendTelemetryEvent } from '../../common/telemetry/sender';
import { getPythonApi } from '../../features/pythonApi';
import { PythonProjectManager } from '../../internal.api';
import { NativePythonFinder } from '../common/nativePythonFinder';
Expand All @@ -23,10 +26,18 @@ export async function registerPyenvFeatures(
disposables.push(mgr, api.registerEnvironmentManager(mgr));
} else {
traceInfo('Pyenv not found, turning off pyenv features.');
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_SKIPPED, undefined, {
managerName: 'pyenv',
reason: 'tool_not_found',
});
await notifyMissingManagerIfDefault('ms-python.python:pyenv', projectManager, api);
}
} catch (ex) {
traceInfo('Pyenv not found, turning off pyenv features.', ex);
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_FAILED, undefined, {
managerName: 'pyenv',
errorType: classifyError(ex),
});
await notifyMissingManagerIfDefault('ms-python.python:pyenv', projectManager, api);
}
}
Loading