Skip to content

Commit f4021ca

Browse files
Add telemetry for manager registration failures (#1365)
Adds telemetry to diagnose why environments sometimes fail to appear for users. Currently we do not know what went wrong or what's missing. ### New telemetry events - **`MANAGER_REGISTRATION.FAILED`** — Fires when a manager throws an unexpected error during registration. Tells us which managers fail and the classified error type. - **`MANAGER_REGISTRATION.SKIPPED`** — Fires when a manager's tool (conda/pyenv/pipenv/poetry) wasn't found on the system. Tells us which managers are commonly unavailable, helping us understand user environments. Without `SKIPPED`, we'd only see that a manager didn't register. We couldn't tell whether that's because the user doesn't use conda (fine, nothing to fix) or because conda is installed but our code crashed trying to register it (bug). - **`SETUP.HANG_DETECTED`** — Fires when the entire setup block hasn't completed within 120s. Tells us which stage was in progress when the hang occurred (nativeFinder, managerRegistration, envSelection, etc.). ### Enhanced existing event - **`EXTENSION_MANAGER_REGISTRATION_DURATION`** now includes `result` (`success`/`error`), `failureStage`, and `errorType` — previously it only reported duration. ### Non-telemetry changes - **Conda registration refactor:** Separated the "conda not found" path (try/catch on `getConda`) from the registration path, so `SKIPPED` vs `FAILED` telemetry is accurate. Post-getConda errors still call `notifyMissingManagerIfDefault` and re-throw for `safeRegister` to handle. - **`safeRegister` in asyncUtils.ts:** Now sends `MANAGER_REGISTRATION.FAILED` telemetry when a manager's promise rejects, so failures are captured even when individual managers don't add their own telemetry.
1 parent ecbcdac commit f4021ca

File tree

7 files changed

+157
-6
lines changed

7 files changed

+157
-6
lines changed

src/common/telemetry/constants.ts

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,30 @@ 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',
5377
}
5478

5579
// Map all events to their properties
@@ -62,10 +86,17 @@ export interface IEventNamePropertyMapping {
6286
[EventNames.EXTENSION_ACTIVATION_DURATION]: never | undefined;
6387
/* __GDPR__
6488
"extension.manager_registration_duration": {
65-
"duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
89+
"duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
90+
"result" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" },
91+
"failureStage" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" },
92+
"errorType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" }
6693
}
6794
*/
68-
[EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION]: never | undefined;
95+
[EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION]: {
96+
result: 'success' | 'error';
97+
failureStage?: string;
98+
errorType?: string;
99+
};
69100

70101
/* __GDPR__
71102
"environment_manager.registered": {
@@ -239,4 +270,36 @@ export interface IEventNamePropertyMapping {
239270
managerId: string;
240271
managerKind: 'environment' | 'package';
241272
};
273+
274+
/* __GDPR__
275+
"manager_registration.failed": {
276+
"managerName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" },
277+
"errorType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" }
278+
}
279+
*/
280+
[EventNames.MANAGER_REGISTRATION_FAILED]: {
281+
managerName: string;
282+
errorType: string;
283+
};
284+
285+
/* __GDPR__
286+
"setup.hang_detected": {
287+
"failureStage": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" },
288+
"<duration>": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "StellaHuang95" }
289+
}
290+
*/
291+
[EventNames.SETUP_HANG_DETECTED]: {
292+
failureStage: string;
293+
};
294+
295+
/* __GDPR__
296+
"manager_registration.skipped": {
297+
"managerName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" },
298+
"reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" }
299+
}
300+
*/
301+
[EventNames.MANAGER_REGISTRATION_SKIPPED]: {
302+
managerName: string;
303+
reason: 'tool_not_found';
304+
};
242305
}

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: 35 additions & 2 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,34 @@ 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
527548
const nativeFinder: NativePythonFinder = await createNativePythonFinder(outputChannel, api, context);
528549
context.subscriptions.push(nativeFinder);
529550
const sysMgr = new SysPythonManager(nativeFinder, api, outputChannel);
530551
sysPythonManager.resolve(sysMgr);
531552
// Each manager registers independently — one failure must not block the others.
553+
failureStage = 'managerRegistration';
532554
await Promise.all([
533555
safeRegister(
534556
'system',
@@ -547,17 +569,23 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
547569
safeRegister('shellStartupVars', shellStartupVarsMgr.initialize()),
548570
]);
549571

572+
failureStage = 'envSelection';
550573
await applyInitialEnvironmentSelection(envManagers, projectManager, nativeFinder, api);
551574

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

555579
// Register listener for interpreter settings changes for interpreter re-selection
580+
failureStage = 'settingsListener';
556581
context.subscriptions.push(
557582
registerInterpreterSettingsChangeListener(envManagers, projectManager, nativeFinder, api),
558583
);
559584

560-
sendTelemetryEvent(EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION, start.elapsedTime);
585+
sendTelemetryEvent(EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION, start.elapsedTime, {
586+
result: 'success',
587+
});
588+
clearHangWatchdog();
561589
try {
562590
await terminalManager.initialize(api);
563591
sendManagerSelectionTelemetry(projectManager);
@@ -570,11 +598,16 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
570598
traceError('Post-initialization tasks failed:', postInitError);
571599
}
572600
} catch (error) {
601+
clearHangWatchdog();
573602
traceError('Failed to initialize environment managers:', error);
574603
sendTelemetryEvent(
575604
EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION,
576605
start.elapsedTime,
577-
undefined,
606+
{
607+
result: 'error',
608+
failureStage,
609+
errorType: classifyError(error),
610+
},
578611
error instanceof Error ? error : undefined,
579612
);
580613
// Show a user-friendly error message

src/managers/conda/main.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Disposable, LogOutputChannel } from 'vscode';
22
import { PythonEnvironmentApi } from '../../api';
33
import { traceInfo } from '../../common/logging';
4+
import { EventNames } from '../../common/telemetry/constants';
5+
import { sendTelemetryEvent } from '../../common/telemetry/sender';
46
import { getPythonApi } from '../../features/pythonApi';
57
import { PythonProjectManager } from '../../internal.api';
68
import { NativePythonFinder } from '../common/nativePythonFinder';
@@ -18,9 +20,22 @@ export async function registerCondaFeatures(
1820
): Promise<void> {
1921
const api: PythonEnvironmentApi = await getPythonApi();
2022

23+
let condaPath: string | undefined;
2124
try {
2225
// get Conda will return only ONE conda manager, that correlates to a single conda install
23-
const condaPath: string = await getConda(nativeFinder);
26+
condaPath = await getConda(nativeFinder);
27+
} catch (ex) {
28+
traceInfo('Conda not found, turning off conda features.', ex);
29+
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_SKIPPED, undefined, {
30+
managerName: 'conda',
31+
reason: 'tool_not_found',
32+
});
33+
await notifyMissingManagerIfDefault('ms-python.python:conda', projectManager, api);
34+
return;
35+
}
36+
37+
// Conda was found — errors below are real registration failures (let safeRegister handle telemetry)
38+
try {
2439
const sourcingStatus: CondaSourcingStatus = await constructCondaSourcingStatus(condaPath);
2540
traceInfo(sourcingStatus.toString());
2641

@@ -36,7 +51,7 @@ export async function registerCondaFeatures(
3651
api.registerPackageManager(packageManager),
3752
);
3853
} catch (ex) {
39-
traceInfo('Conda not found, turning off conda features.', ex);
4054
await notifyMissingManagerIfDefault('ms-python.python:conda', projectManager, api);
55+
throw ex;
4156
}
4257
}

src/managers/pipenv/main.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { Disposable } from 'vscode';
22
import { PythonEnvironmentApi } from '../../api';
33
import { traceInfo } from '../../common/logging';
4+
import { EventNames } from '../../common/telemetry/constants';
5+
import { classifyError } from '../../common/telemetry/errorClassifier';
6+
import { sendTelemetryEvent } from '../../common/telemetry/sender';
47
import { getPythonApi } from '../../features/pythonApi';
58
import { PythonProjectManager } from '../../internal.api';
69
import { NativePythonFinder } from '../common/nativePythonFinder';
@@ -35,13 +38,21 @@ export async function registerPipenvFeatures(
3538
traceInfo(
3639
'Pipenv not found, turning off pipenv features. If you have pipenv installed in a non-standard location, set the "python.pipenvPath" setting.',
3740
);
41+
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_SKIPPED, undefined, {
42+
managerName: 'pipenv',
43+
reason: 'tool_not_found',
44+
});
3845
await notifyMissingManagerIfDefault('ms-python.python:pipenv', projectManager, api);
3946
}
4047
} catch (ex) {
4148
traceInfo(
4249
'Pipenv not found, turning off pipenv features. If you have pipenv installed in a non-standard location, set the "python.pipenvPath" setting.',
4350
ex,
4451
);
52+
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_FAILED, undefined, {
53+
managerName: 'pipenv',
54+
errorType: classifyError(ex),
55+
});
4556
await notifyMissingManagerIfDefault('ms-python.python:pipenv', projectManager, api);
4657
}
4758
}

src/managers/poetry/main.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { Disposable, LogOutputChannel } from 'vscode';
22
import { PythonEnvironmentApi } from '../../api';
33
import { traceInfo } from '../../common/logging';
4+
import { EventNames } from '../../common/telemetry/constants';
5+
import { classifyError } from '../../common/telemetry/errorClassifier';
6+
import { sendTelemetryEvent } from '../../common/telemetry/sender';
47
import { getPythonApi } from '../../features/pythonApi';
58
import { PythonProjectManager } from '../../internal.api';
69
import { NativePythonFinder } from '../common/nativePythonFinder';
@@ -36,10 +39,18 @@ export async function registerPoetryFeatures(
3639
);
3740
} else {
3841
traceInfo('Poetry not found, turning off poetry features.');
42+
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_SKIPPED, undefined, {
43+
managerName: 'poetry',
44+
reason: 'tool_not_found',
45+
});
3946
await notifyMissingManagerIfDefault('ms-python.python:poetry', projectManager, api);
4047
}
4148
} catch (ex) {
4249
traceInfo('Poetry not found, turning off poetry features.', ex);
50+
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_FAILED, undefined, {
51+
managerName: 'poetry',
52+
errorType: classifyError(ex),
53+
});
4354
await notifyMissingManagerIfDefault('ms-python.python:poetry', projectManager, api);
4455
}
4556
}

src/managers/pyenv/main.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { Disposable } from 'vscode';
22
import { PythonEnvironmentApi } from '../../api';
33
import { traceInfo } from '../../common/logging';
4+
import { EventNames } from '../../common/telemetry/constants';
5+
import { classifyError } from '../../common/telemetry/errorClassifier';
6+
import { sendTelemetryEvent } from '../../common/telemetry/sender';
47
import { getPythonApi } from '../../features/pythonApi';
58
import { PythonProjectManager } from '../../internal.api';
69
import { NativePythonFinder } from '../common/nativePythonFinder';
@@ -23,10 +26,18 @@ export async function registerPyenvFeatures(
2326
disposables.push(mgr, api.registerEnvironmentManager(mgr));
2427
} else {
2528
traceInfo('Pyenv not found, turning off pyenv features.');
29+
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_SKIPPED, undefined, {
30+
managerName: 'pyenv',
31+
reason: 'tool_not_found',
32+
});
2633
await notifyMissingManagerIfDefault('ms-python.python:pyenv', projectManager, api);
2734
}
2835
} catch (ex) {
2936
traceInfo('Pyenv not found, turning off pyenv features.', ex);
37+
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_FAILED, undefined, {
38+
managerName: 'pyenv',
39+
errorType: classifyError(ex),
40+
});
3041
await notifyMissingManagerIfDefault('ms-python.python:pyenv', projectManager, api);
3142
}
3243
}

0 commit comments

Comments
 (0)