diff --git a/src/common/telemetry/constants.ts b/src/common/telemetry/constants.ts index 4cd75372..91de136a 100644 --- a/src/common/telemetry/constants.ts +++ b/src/common/telemetry/constants.ts @@ -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 @@ -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": { @@ -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" }, + "": { "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'; + }; } diff --git a/src/common/utils/asyncUtils.ts b/src/common/utils/asyncUtils.ts index b0de59e4..06380b29 100644 --- a/src/common/utils/asyncUtils.ts +++ b/src/common/utils/asyncUtils.ts @@ -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 { return new Promise((resolve) => setTimeout(resolve, milliseconds)); @@ -13,5 +16,9 @@ export async function safeRegister(name: string, task: Promise): Promise { + 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); @@ -529,6 +550,7 @@ export async function activate(context: ExtensionContext): Promise { 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()); @@ -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; } } diff --git a/src/managers/pipenv/main.ts b/src/managers/pipenv/main.ts index d87d0d9b..75f567a1 100644 --- a/src/managers/pipenv/main.ts +++ b/src/managers/pipenv/main.ts @@ -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'; @@ -35,6 +38,10 @@ 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) { @@ -42,6 +49,10 @@ export async function registerPipenvFeatures( '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); } } diff --git a/src/managers/poetry/main.ts b/src/managers/poetry/main.ts index 5f74fb46..bd28f5ec 100644 --- a/src/managers/poetry/main.ts +++ b/src/managers/poetry/main.ts @@ -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'; @@ -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); } } diff --git a/src/managers/pyenv/main.ts b/src/managers/pyenv/main.ts index 2ca6a96a..853c44d7 100644 --- a/src/managers/pyenv/main.ts +++ b/src/managers/pyenv/main.ts @@ -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'; @@ -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); } }