Skip to content

Commit f8d3243

Browse files
Add failureStage to MANAGER_REGISTRATION_FAILED telemetry for granular error diagnosis (#1405)
## Problem When `MANAGER_REGISTRATION.FAILED` fires, we only know which manager failed and the classified error type. We do not know where in the registration flow the error occurred, such as settings lookup, persistent state access, path validation, native finder refresh, or manager registration. That makes telemetry harder to use for diagnosis and prioritization. ## Changes This PR adds a `failureStage` property to `MANAGER_REGISTRATION.FAILED` telemetry so we can identify the exact stage where the error occurred. - Added `failureStage` to the telemetry event definition and GDPR mapping. - Updated `safeRegister()` to read `failureStage` from caught errors and fall back to `unknown`. - Added stage tracking in `getPipenv()`, `hasPipenvEnvironments()`, `getPyenv()`, and `getPoetry()`. - Added stage tracking in the pipenv, pyenv, poetry, and conda registration flows. - Propagated detailed stage information by attaching `failureStage` to rethrown errors. ## Why this is needed Manager registration failures currently lack enough detail to determine which step is failing. Adding stage-level telemetry makes it much easier to identify whether failures are happening during discovery, state lookup, file validation, native finder refresh, sourcing setup, or registration with the API. This should improve investigation speed and help target fixes more accurately. ## Example values - `getPipenv:nativeFinderRefresh` - `getPoetry:validateCachePath` - `constructCondaSourcingStatus` - `registerManager` - `unknown` ## Verification - Ran `npx tsc --noEmit` - No compile errors - Changes are telemetry-only and do not change feature behavior Co-authored-by: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com>
1 parent 8488dd6 commit f8d3243

File tree

9 files changed

+266
-163
lines changed

9 files changed

+266
-163
lines changed

src/common/telemetry/constants.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export enum EventNames {
5656
* Properties:
5757
* - managerName: string (e.g. 'system', 'conda', 'pyenv', 'pipenv', 'poetry', 'shellStartupVars')
5858
* - errorType: string (classified error category from classifyError)
59+
* - failureStage: string (hierarchical stage indicator, e.g. 'getPipenv:nativeFinderRefresh')
5960
*/
6061
MANAGER_REGISTRATION_FAILED = 'MANAGER_REGISTRATION.FAILED',
6162
/**
@@ -300,12 +301,14 @@ export interface IEventNamePropertyMapping {
300301
/* __GDPR__
301302
"manager_registration.failed": {
302303
"managerName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" },
303-
"errorType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" }
304+
"errorType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" },
305+
"failureStage": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" }
304306
}
305307
*/
306308
[EventNames.MANAGER_REGISTRATION_FAILED]: {
307309
managerName: string;
308310
errorType: string;
311+
failureStage: string;
309312
};
310313

311314
/* __GDPR__

src/common/utils/asyncUtils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@ export async function safeRegister(name: string, task: Promise<void>): Promise<v
1616
await task;
1717
} catch (error) {
1818
traceError(`Failed to register ${name} features:`, error);
19+
const failureStage =
20+
error instanceof Error
21+
? ((error as Error & { failureStage?: string }).failureStage ?? 'unknown')
22+
: 'unknown';
1923
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_FAILED, undefined, {
2024
managerName: name,
2125
errorType: classifyError(error),
26+
failureStage,
2227
});
2328
}
2429
}

src/managers/conda/main.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ export async function registerCondaFeatures(
1818
log: LogOutputChannel,
1919
projectManager: PythonProjectManager,
2020
): Promise<void> {
21+
let stage = 'getPythonApi';
2122
const api: PythonEnvironmentApi = await getPythonApi();
2223

2324
let condaPath: string | undefined;
2425
try {
2526
// get Conda will return only ONE conda manager, that correlates to a single conda install
27+
stage = 'getConda';
2628
condaPath = await getConda(nativeFinder);
2729
} catch (ex) {
2830
traceInfo('Conda not found, turning off conda features.', ex);
@@ -36,14 +38,18 @@ export async function registerCondaFeatures(
3638

3739
// Conda was found — errors below are real registration failures (let safeRegister handle telemetry)
3840
try {
41+
stage = 'constructCondaSourcingStatus';
3942
const sourcingStatus: CondaSourcingStatus = await constructCondaSourcingStatus(condaPath);
4043
traceInfo(sourcingStatus.toString());
4144

45+
stage = 'createEnvManager';
4246
const envManager = new CondaEnvManager(nativeFinder, api, log);
47+
stage = 'createPkgManager';
4348
const packageManager = new CondaPackageManager(api, log);
4449

4550
envManager.sourcingInformation = sourcingStatus;
4651

52+
stage = 'registerManagers';
4753
disposables.push(
4854
envManager,
4955
packageManager,
@@ -52,6 +58,8 @@ export async function registerCondaFeatures(
5258
);
5359
} catch (ex) {
5460
await notifyMissingManagerIfDefault('ms-python.python:conda', projectManager, api);
55-
throw ex;
61+
const err = ex instanceof Error ? ex : new Error(String(ex));
62+
(err as Error & { failureStage?: string }).failureStage = stage;
63+
throw err;
5664
}
5765
}

src/managers/pipenv/main.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,22 @@ export async function registerPipenvFeatures(
1717
disposables: Disposable[],
1818
projectManager: PythonProjectManager,
1919
): Promise<void> {
20+
let stage = 'getPythonApi';
2021
const api: PythonEnvironmentApi = await getPythonApi();
2122

2223
try {
24+
stage = 'getPipenv';
2325
const pipenv = await getPipenv(nativeFinder);
2426

2527
// Register the manager if the CLI is found, or if there are existing pipenv environments.
2628
// This allows users with existing pipenv environments to still see and use them.
29+
stage = 'hasPipenvEnvironments';
2730
const hasPipenvEnvs = !pipenv && (await hasPipenvEnvironments(nativeFinder));
2831

2932
if (pipenv || hasPipenvEnvs) {
33+
stage = 'createManager';
3034
const mgr = new PipenvManager(nativeFinder, api);
35+
stage = 'registerManager';
3136
disposables.push(mgr, api.registerEnvironmentManager(mgr));
3237
if (!pipenv) {
3338
traceInfo(
@@ -45,13 +50,15 @@ export async function registerPipenvFeatures(
4550
await notifyMissingManagerIfDefault('ms-python.python:pipenv', projectManager, api);
4651
}
4752
} catch (ex) {
53+
const failureStage = (ex as Error & { failureStage?: string })?.failureStage ?? stage;
4854
traceInfo(
4955
'Pipenv not found, turning off pipenv features. If you have pipenv installed in a non-standard location, set the "python.pipenvPath" setting.',
5056
ex,
5157
);
5258
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_FAILED, undefined, {
5359
managerName: 'pipenv',
5460
errorType: classifyError(ex),
61+
failureStage,
5562
});
5663
await notifyMissingManagerIfDefault('ms-python.python:pipenv', projectManager, api);
5764
}

src/managers/pipenv/pipenvUtils.ts

Lines changed: 75 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,18 @@ export async function clearPipenvCache(): Promise<void> {
5656
* This allows the manager to be registered even if the CLI is not found.
5757
*/
5858
export async function hasPipenvEnvironments(nativeFinder: NativePythonFinder): Promise<boolean> {
59-
const data = await nativeFinder.refresh(false);
60-
return data
61-
.filter((e) => isNativeEnvInfo(e))
62-
.some((e) => (e as NativeEnvInfo).kind === NativePythonEnvironmentKind.pipenv);
59+
let stage = 'nativeFinderRefresh';
60+
try {
61+
const data = await nativeFinder.refresh(false);
62+
stage = 'filterPipenvEnvs';
63+
return data
64+
.filter((e) => isNativeEnvInfo(e))
65+
.some((e) => (e as NativeEnvInfo).kind === NativePythonEnvironmentKind.pipenv);
66+
} catch (ex) {
67+
const err = ex instanceof Error ? ex : new Error(String(ex));
68+
(err as Error & { failureStage?: string }).failureStage = `hasPipenvEnvironments:${stage}`;
69+
throw err;
70+
}
6371
}
6472

6573
function getPipenvPathFromSettings(): string | undefined {
@@ -68,61 +76,78 @@ function getPipenvPathFromSettings(): string | undefined {
6876
}
6977

7078
export async function getPipenv(native?: NativePythonFinder): Promise<string | undefined> {
71-
// Priority 1: Settings (if explicitly set and valid)
72-
const settingPath = getPipenvPathFromSettings();
73-
if (settingPath) {
74-
if (await fs.exists(untildify(settingPath))) {
75-
traceInfo(`Using pipenv from settings: ${settingPath}`);
76-
return untildify(settingPath);
79+
let stage = 'checkSettings';
80+
try {
81+
// Priority 1: Settings (if explicitly set and valid)
82+
const settingPath = getPipenvPathFromSettings();
83+
if (settingPath) {
84+
stage = 'validateSettingsPath';
85+
if (await fs.exists(untildify(settingPath))) {
86+
traceInfo(`Using pipenv from settings: ${settingPath}`);
87+
return untildify(settingPath);
88+
}
89+
traceInfo(`Pipenv path from settings does not exist: ${settingPath}`);
7790
}
78-
traceInfo(`Pipenv path from settings does not exist: ${settingPath}`);
79-
}
8091

81-
// Priority 2: In-memory cache
82-
if (pipenvPath) {
83-
if (await fs.exists(untildify(pipenvPath))) {
84-
return untildify(pipenvPath);
92+
// Priority 2: In-memory cache
93+
stage = 'checkCache';
94+
if (pipenvPath) {
95+
stage = 'validateCachePath';
96+
if (await fs.exists(untildify(pipenvPath))) {
97+
return untildify(pipenvPath);
98+
}
99+
pipenvPath = undefined;
85100
}
86-
pipenvPath = undefined;
87-
}
88101

89-
// Priority 3: Persistent state
90-
const state = await getWorkspacePersistentState();
91-
const storedPath = await state.get<string>(PIPENV_PATH_KEY);
92-
if (storedPath) {
93-
if (await fs.exists(untildify(storedPath))) {
94-
pipenvPath = storedPath;
95-
traceInfo(`Using pipenv from persistent state: ${pipenvPath}`);
96-
return untildify(pipenvPath);
102+
// Priority 3: Persistent state
103+
stage = 'getPersistentState';
104+
const state = await getWorkspacePersistentState();
105+
stage = 'checkPersistentState';
106+
const storedPath = await state.get<string>(PIPENV_PATH_KEY);
107+
if (storedPath) {
108+
stage = 'validatePersistentStatePath';
109+
if (await fs.exists(untildify(storedPath))) {
110+
pipenvPath = storedPath;
111+
traceInfo(`Using pipenv from persistent state: ${pipenvPath}`);
112+
return untildify(pipenvPath);
113+
}
114+
await state.set(PIPENV_PATH_KEY, undefined);
97115
}
98-
await state.set(PIPENV_PATH_KEY, undefined);
99-
}
100116

101-
// Priority 4: PATH lookup
102-
const foundPipenv = await findPipenv();
103-
if (foundPipenv) {
104-
pipenvPath = foundPipenv;
105-
traceInfo(`Found pipenv in PATH: ${foundPipenv}`);
106-
return foundPipenv;
107-
}
117+
// Priority 4: PATH lookup
118+
stage = 'pathLookup';
119+
const foundPipenv = await findPipenv();
120+
if (foundPipenv) {
121+
pipenvPath = foundPipenv;
122+
traceInfo(`Found pipenv in PATH: ${foundPipenv}`);
123+
return foundPipenv;
124+
}
108125

109-
// Priority 5: Native finder as fallback
110-
if (native) {
111-
const data = await native.refresh(false);
112-
const managers = data
113-
.filter((e) => !isNativeEnvInfo(e))
114-
.map((e) => e as NativeEnvManagerInfo)
115-
.filter((e) => e.tool.toLowerCase() === 'pipenv');
116-
if (managers.length > 0) {
117-
pipenvPath = managers[0].executable;
118-
traceInfo(`Using pipenv from native finder: ${pipenvPath}`);
119-
await state.set(PIPENV_PATH_KEY, pipenvPath);
120-
return pipenvPath;
126+
// Priority 5: Native finder as fallback
127+
stage = 'nativeFinderRefresh';
128+
if (native) {
129+
const data = await native.refresh(false);
130+
stage = 'filterNativeFinderResults';
131+
const managers = data
132+
.filter((e) => !isNativeEnvInfo(e))
133+
.map((e) => e as NativeEnvManagerInfo)
134+
.filter((e) => e.tool.toLowerCase() === 'pipenv');
135+
if (managers.length > 0) {
136+
pipenvPath = managers[0].executable;
137+
traceInfo(`Using pipenv from native finder: ${pipenvPath}`);
138+
stage = 'persistNativeFinderResult';
139+
await state.set(PIPENV_PATH_KEY, pipenvPath);
140+
return pipenvPath;
141+
}
121142
}
122-
}
123143

124-
traceInfo('Pipenv not found');
125-
return undefined;
144+
traceInfo('Pipenv not found');
145+
return undefined;
146+
} catch (ex) {
147+
const err = ex instanceof Error ? ex : new Error(String(ex));
148+
(err as Error & { failureStage?: string }).failureStage = `getPipenv:${stage}`;
149+
throw err;
150+
}
126151
}
127152

128153
async function nativeToPythonEnv(

src/managers/poetry/main.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,25 @@ export async function registerPoetryFeatures(
1818
outputChannel: LogOutputChannel,
1919
projectManager: PythonProjectManager,
2020
): Promise<void> {
21+
let stage = 'getPythonApi';
2122
const api: PythonEnvironmentApi = await getPythonApi();
2223

2324
try {
25+
stage = 'getPoetry';
2426
const poetryPath = await getPoetry(nativeFinder);
2527
if (poetryPath) {
2628
traceInfo(
2729
'The `shell` command is not available by default in Poetry versions 2.0.0 and above. Therefore all shell activation will be handled by calling `source <path-to-activate>`. If you face any problems with shell activation, please file an issue at https://github.com/microsoft/vscode-python-environments/issues to help us improve this implementation.',
2830
);
31+
stage = 'getPoetryVersion';
2932
const version = await getPoetryVersion(poetryPath);
3033
traceInfo(`Poetry found at ${poetryPath}, version: ${version}`);
34+
stage = 'createEnvManager';
3135
const envManager = new PoetryManager(nativeFinder, api);
36+
stage = 'createPkgManager';
3237
const pkgManager = new PoetryPackageManager(api, outputChannel, envManager);
3338

39+
stage = 'registerManagers';
3440
disposables.push(
3541
envManager,
3642
pkgManager,
@@ -46,10 +52,12 @@ export async function registerPoetryFeatures(
4652
await notifyMissingManagerIfDefault('ms-python.python:poetry', projectManager, api);
4753
}
4854
} catch (ex) {
55+
const failureStage = (ex as Error & { failureStage?: string })?.failureStage ?? stage;
4956
traceInfo('Poetry not found, turning off poetry features.', ex);
5057
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_FAILED, undefined, {
5158
managerName: 'poetry',
5259
errorType: classifyError(ex),
60+
failureStage,
5361
});
5462
await notifyMissingManagerIfDefault('ms-python.python:poetry', projectManager, api);
5563
}

0 commit comments

Comments
 (0)