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
23 changes: 23 additions & 0 deletions src/common/telemetry/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,18 @@ export enum EventNames {
* - errorType: string (classified error category, on failure only)
*/
PET_RESOLVE = 'PET.RESOLVE',
/**
* Telemetry event for the one-time migration that removes a stale
* `python-envs.defaultEnvManager: system` value from User (global) settings.
* Fires only on the activation when the migration actually runs (not on subsequent runs).
* Properties:
* - outcome: 'removed' (was set to system, all user-scope slots cleared)
* | 'partial' (cleared current context's slot but another user-scope slot still has it; will retry)
* | 'not_set' (no user-scope slot of system found, nothing to do)
* | 'failed' (attempted removal threw)
* - errorType: string (only when outcome === 'failed')
*/
MIGRATION_SYSTEM_ENV_MANAGER = 'MIGRATION.SYSTEM_ENV_MANAGER',
}

// Map all events to their properties
Expand Down Expand Up @@ -634,4 +646,15 @@ export interface IEventNamePropertyMapping {
result: 'success' | 'timeout' | 'error';
errorType?: string;
};

/* __GDPR__
"migration.system_env_manager": {
"outcome": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
"errorType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
}
*/
[EventNames.MIGRATION_SYSTEM_ENV_MANAGER]: {
outcome: 'removed' | 'partial' | 'not_set' | 'failed';
errorType?: string;
};
}
10 changes: 10 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import {
import { PythonProjectManagerImpl } from './features/projectManager';
import { getPythonApi, setPythonApi } from './features/pythonApi';
import { registerCompletionProvider } from './features/settings/settingCompletions';
import { migrateGlobalDefaultEnvManagerSetting } from './features/settings/settingHelpers';
import { setActivateMenuButtonContext } from './features/terminal/activateMenuButton';
import { normalizeShellPath } from './features/terminal/shells/common/shellUtils';
import {
Expand Down Expand Up @@ -162,6 +163,15 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
// Setup the persistent state for the extension.
setPersistentState(context);

// One-time migration: remove `system` defaultEnvManager from User settings if a previous
// version wrote it there (bug #1468). Awaited so the migration deterministically affects
// initial environment selection on the very first activation after upgrade.
try {
await migrateGlobalDefaultEnvManagerSetting();
} catch (err) {
traceError(`[migration] migrateGlobalDefaultEnvManagerSetting threw: ${err}`);
}

const statusBar = new PythonStatusBarImpl();
context.subscriptions.push(statusBar);

Expand Down
95 changes: 94 additions & 1 deletion src/features/settings/settingHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import * as path from 'path';
import { ConfigurationScope, ConfigurationTarget, Uri, WorkspaceConfiguration, WorkspaceFolder } from 'vscode';
import { PythonProject } from '../../api';
import { DEFAULT_ENV_MANAGER_ID, DEFAULT_PACKAGE_MANAGER_ID } from '../../common/constants';
import { DEFAULT_ENV_MANAGER_ID, DEFAULT_PACKAGE_MANAGER_ID, SYSTEM_MANAGER_ID } from '../../common/constants';
import { traceError, traceInfo, traceVerbose, traceWarn } from '../../common/logging';
import { getGlobalPersistentState } from '../../common/persistentState';
import { EventNames } from '../../common/telemetry/constants';
import { sendTelemetryEvent } from '../../common/telemetry/sender';
import * as workspaceApis from '../../common/workspace.apis';
import { PythonProjectManager, PythonProjectSettings } from '../../internal.api';

Expand Down Expand Up @@ -576,3 +579,93 @@ export function getSettingUserScope<T>(section: string, key: string): T | undefi
}
return undefined;
}

const MIGRATION_KEY = 'globalSettingsMigration.systemEnvManagerRemoved';

/**
* Returns true if any user-scope slot of the inspection result equals `value`.
* For window-scoped settings VS Code may populate `globalRemoteValue` and/or
* `globalLocalValue` in addition to `globalValue` depending on context.
*/
function userScopeHasValue(inspect: { globalValue?: string } | undefined, value: string): boolean {
if (!inspect) {
return false;
}
const record = inspect as Record<string, unknown>;
if (record.globalRemoteValue === value) {
return true;
}
if (record.globalLocalValue === value) {
return true;
}
if (inspect.globalValue === value) {
return true;
}
return false;
}

/**
* One-time migration: removes `defaultEnvManager` from User (global) settings if it was
* set to `system` by the extension. This was an unintentional side effect of a bug where
* the extension wrote to User scope when no workspace was open. Having `system` at the
* User level causes all workspaces to ignore local .venv environments.
*
* Because `python-envs.defaultEnvManager` is a window-scoped setting, the stale value can
* land in any of `globalValue`, `globalLocalValue`, or `globalRemoteValue` depending on
* which context (local vs remote) hit the bug. We check all three, attempt removal via
* `ConfigurationTarget.Global` (which clears the slot for the current context), then
* re-inspect. If any user-scope slot still holds the stale value we do NOT mark the
* migration complete, so a future activation in the other context can finish the job.
*
* See: https://github.com/microsoft/vscode-python-environments/issues/1468
*/
export async function migrateGlobalDefaultEnvManagerSetting(): Promise<void> {
const state = await getGlobalPersistentState();
const alreadyMigrated = await state.get<boolean>(MIGRATION_KEY);
if (alreadyMigrated) {
return;
}

const config = workspaceApis.getConfiguration('python-envs', undefined);
const inspect = config.inspect<string>('defaultEnvManager');

if (!userScopeHasValue(inspect, SYSTEM_MANAGER_ID)) {
sendTelemetryEvent(EventNames.MIGRATION_SYSTEM_ENV_MANAGER, undefined, { outcome: 'not_set' });
await state.set(MIGRATION_KEY, true);
return;
}

try {
await config.update('defaultEnvManager', undefined, ConfigurationTarget.Global);
} catch (err) {
// Don't mark migration done; we'll retry on a future activation.
traceWarn(
`[migration] Failed to remove 'python-envs.defaultEnvManager: ${SYSTEM_MANAGER_ID}' from User settings: ${err}`,
);
sendTelemetryEvent(EventNames.MIGRATION_SYSTEM_ENV_MANAGER, undefined, {
outcome: 'failed',
errorType: err instanceof Error ? err.name : typeof err,
});
return;
}

// Re-inspect: `update(Global)` only clears the current context's slot. If another
// context's slot (local vs remote) still holds the stale value, leave the flag unset
// so the next activation in that context can clear it.
const after = config.inspect<string>('defaultEnvManager');
if (userScopeHasValue(after, SYSTEM_MANAGER_ID)) {
traceInfo(
`[migration] Partially removed 'python-envs.defaultEnvManager: ${SYSTEM_MANAGER_ID}' from User settings; ` +
`another context still has it set. Will retry on next activation.`,
);
sendTelemetryEvent(EventNames.MIGRATION_SYSTEM_ENV_MANAGER, undefined, { outcome: 'partial' });
return;
}

traceInfo(
`[migration] Removed 'python-envs.defaultEnvManager: ${SYSTEM_MANAGER_ID}' from User settings ` +
`(was set unintentionally by a previous version). See https://github.com/microsoft/vscode-python-environments/issues/1468`,
);
sendTelemetryEvent(EventNames.MIGRATION_SYSTEM_ENV_MANAGER, undefined, { outcome: 'removed' });
await state.set(MIGRATION_KEY, true);
}
Loading
Loading