Skip to content
Open
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
5 changes: 5 additions & 0 deletions .github/instructions/generic.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ Provide project context and coding guidelines that AI should follow when generat
- Use the extension’s logging utilities (`traceLog`, `traceVerbose`) for internal logs.
- Do not use `console.log` or `console.warn` for logging.

## Telemetry

- Always pass environment-manager and package-manager IDs through `getTelemetryManagerId()` from `src/common/telemetry/helpers.ts` when adding them to telemetry properties, including properties named `manager` or `managerId`.
- Hash IDs only at the telemetry boundary. Preserve exact manager IDs for registration, routing, settings, environment identity, logs, and user-facing output.

## Settings Precedence

- Always consider VS Code settings precedence:
Expand Down
5 changes: 4 additions & 1 deletion .github/prompts/add-telemetry.prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ When adding telemetry:

- If the user wants to record when an action is started (such as a command invocation), place the telemetry call at the start of the handler or function.
- If the user wants to record successful completions or outcomes, place the telemetry call at the end of the action, after the operation has succeeded (and optionally, record errors or failures as well).
- Always pass environment-manager and package-manager IDs through `getTelemetryManagerId()` from `src/common/telemetry/helpers.ts` before assigning them to telemetry properties such as `manager` or `managerId`. Do not hash IDs used for registration, routing, settings, environment identity, logs, or user-facing output.

Instructions to add a new telemetry event:

1. Add a new event name to the `EventNames` enum in `src/common/telemetry/constants.ts`.
2. Add a corresponding entry to the `IEventNamePropertyMapping` interface in the same file, including a GDPR comment and the expected properties.
3. In the relevant code location, call `sendTelemetryEvent` with the new event name and required properties. Example:
```typescript
sendTelemetryEvent(EventNames.YOUR_EVENT_NAME, undefined, { property: value });
sendTelemetryEvent(EventNames.YOUR_EVENT_NAME, undefined, {
managerId: getTelemetryManagerId(manager.id),
});
```
4. If the event is triggered by a command, ensure the call is placed at the start of the command handler.

Expand Down
13 changes: 10 additions & 3 deletions src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,26 @@ import * as path from 'path';
export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs';
export const PYTHON_EXTENSION_ID = 'ms-python.python';
export const PYLANCE_EXTENSION_ID = 'ms-python.vscode-pylance';

// Manager IDs
export const CONDA_MANAGER_ID = `${PYTHON_EXTENSION_ID}:conda`;
export const INLINE_SCRIPT_MANAGER_ID = `${PYTHON_EXTENSION_ID}:inline-script`;
export const INLINE_SCRIPT_ENVS_KEY = `${ENVS_EXTENSION_ID}:inline-script:SCRIPT_ENVIRONMENTS`;
export const PYENV_MANAGER_ID = `${PYTHON_EXTENSION_ID}:pyenv`;
export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter';
export const EXTENSION_ROOT_DIR = path.dirname(__dirname);
export const ISSUES_URL = 'https://github.com/microsoft/vscode-python-environments/issues';
export const PIPENV_MANAGER_ID = `${PYTHON_EXTENSION_ID}:pipenv`;
export const POETRY_MANAGER_ID = `${PYTHON_EXTENSION_ID}:poetry`;
export const PIXI_MANAGER_ID = `${PYTHON_EXTENSION_ID}:pixi`;
export const HATCH_MANAGER_ID = `${PYTHON_EXTENSION_ID}:hatch`;

export const DEFAULT_PACKAGE_MANAGER_ID = 'ms-python.python:pip';
export const DEFAULT_ENV_MANAGER_ID = 'ms-python.python:venv';
export const VENV_MANAGER_ID = 'ms-python.python:venv';
export const SYSTEM_MANAGER_ID = 'ms-python.python:system';

export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter';
export const EXTENSION_ROOT_DIR = path.dirname(__dirname);
export const ISSUES_URL = 'https://github.com/microsoft/vscode-python-environments/issues';

export const KNOWN_FILES = [
'requirements.txt',
'requirements.in',
Expand Down
1 change: 1 addition & 0 deletions src/common/telemetry/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ export interface IEventNamePropertyMapping {
"isLegacy": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eduardovil" }
}
*/
/** Manager IDs are reported as full SHA-256 digests; missing IDs are reported as "undefined". */
[EventNames.MANAGER_CAPABILITY_QUERY]: {
managerId: string;
managerKind: 'environment' | 'package';
Expand Down
18 changes: 16 additions & 2 deletions src/common/telemetry/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createHash } from 'crypto';
import { getDefaultEnvManagerSetting, getDefaultPkgManagerSetting } from '../../features/settings/settingHelpers';
import type { EnvironmentManagers } from '../../features/envManagers';
import type { PythonProjectManager } from '../../features/projectManager';
Expand All @@ -8,6 +9,15 @@ import { getWorkspaceFolders } from '../workspace.apis';
import { EventNames } from './constants';
import { sendTelemetryEvent } from './sender';

/**
* Returns a deterministic SHA-256 manager ID for telemetry, or the "undefined" sentinel.
* @param managerId Manager ID to hash.
* @returns The full lowercase hexadecimal SHA-256 digest, or "undefined" when no ID is available.
*/
export function getTelemetryManagerId(managerId: string | undefined): string {
return managerId === undefined ? 'undefined' : createHash('sha256').update(managerId, 'utf8').digest('hex');
}

/**
* Extracts the base tool name from a manager ID.
* Example: 'ms-python.python:venv' -> 'venv'
Expand Down Expand Up @@ -35,11 +45,15 @@ export function sendManagerSelectionTelemetry(pm: PythonProjectManager) {
});

ems.forEach((em) => {
sendTelemetryEvent(EventNames.ENVIRONMENT_MANAGER_SELECTED, undefined, { managerId: em });
sendTelemetryEvent(EventNames.ENVIRONMENT_MANAGER_SELECTED, undefined, {
managerId: getTelemetryManagerId(em),
});
});

ps.forEach((pkg) => {
sendTelemetryEvent(EventNames.PACKAGE_MANAGER_SELECTED, undefined, { managerId: pkg });
sendTelemetryEvent(EventNames.PACKAGE_MANAGER_SELECTED, undefined, {
managerId: getTelemetryManagerId(pkg),
});
});
}

Expand Down
28 changes: 19 additions & 9 deletions src/common/utils/pythonPath.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import { CancellationToken, Progress, Uri } from 'vscode';
import { PythonEnvironment } from '../../api';
import type { InternalEnvironmentManager } from '../../managers/common/registeredManagers';
import { PYTHON_EXTENSION_ID } from '../constants';
import {
CONDA_MANAGER_ID,
HATCH_MANAGER_ID,
PIPENV_MANAGER_ID,
PIXI_MANAGER_ID,
POETRY_MANAGER_ID,
PYENV_MANAGER_ID,
PYTHON_EXTENSION_ID,
SYSTEM_MANAGER_ID,
VENV_MANAGER_ID,
} from '../constants';
import { traceVerbose, traceWarn } from '../logging';

const priorityOrder = [
`${PYTHON_EXTENSION_ID}:pyenv`,
`${PYTHON_EXTENSION_ID}:pixi`,
`${PYTHON_EXTENSION_ID}:conda`,
`${PYTHON_EXTENSION_ID}:pipenv`,
`${PYTHON_EXTENSION_ID}:poetry`,
PYENV_MANAGER_ID,
PIXI_MANAGER_ID,
CONDA_MANAGER_ID,
PIPENV_MANAGER_ID,
POETRY_MANAGER_ID,
`${PYTHON_EXTENSION_ID}:activestate`,
`${PYTHON_EXTENSION_ID}:hatch`,
`${PYTHON_EXTENSION_ID}:venv`,
`${PYTHON_EXTENSION_ID}:system`,
HATCH_MANAGER_ID,
VENV_MANAGER_ID,
SYSTEM_MANAGER_ID,
];
function sortManagersByPriority(managers: InternalEnvironmentManager[]): InternalEnvironmentManager[] {
const systemId = priorityOrder[priorityOrder.length - 1];
Expand Down
3 changes: 2 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { EventNames } from './common/telemetry/constants';
import { classifyError } from './common/telemetry/errorClassifier';
import {
logDiscoverySummary,
getTelemetryManagerId,
sendEnvironmentToolUsageTelemetry,
sendManagerSelectionTelemetry,
sendProjectStructureTelemetry,
Expand Down Expand Up @@ -297,7 +298,7 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
managerId = item.manager.id;
}
sendTelemetryEvent(EventNames.CREATE_ENVIRONMENT, undefined, {
manager: managerId,
manager: getTelemetryManagerId(managerId),
triggeredLocation: 'createSpecifiedCommand',
});
return await withProgress(
Expand Down
3 changes: 2 additions & 1 deletion src/extensionApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { CapabilityContext, EnvironmentManagerCapability, PackageManagerCap
import { isKnownManagerCapability } from './capabilities';
import { explain as parsePep440Version } from '@renovatebot/pep440';
import { EventNames } from './common/telemetry/constants';
import { getTelemetryManagerId } from './common/telemetry/helpers';
import { sendTelemetryEvent } from './common/telemetry/sender';
import type {
CreateEnvironmentOptions,
Expand Down Expand Up @@ -247,7 +248,7 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
void this.getManagerVersionForTelemetryWithTimeout(query, CAPABILITY_TELEMETRY_VERSION_TIMEOUT_MS)
.then((managerVersion) => {
sendTelemetryEvent(EventNames.MANAGER_CAPABILITY_QUERY, undefined, {
managerId: manager?.id ?? 'none',
managerId: getTelemetryManagerId(manager?.id),
managerKind: query.kind,
managerVersion,
capability: isKnownManagerCapability(capability) ? capability : 'unknown',
Expand Down
3 changes: 2 additions & 1 deletion src/features/common/managerReady.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getExtension } from '../../common/extension.apis';
import { WorkbenchStrings } from '../../common/localize';
import { traceError, traceInfo, traceWarn } from '../../common/logging';
import { EventNames } from '../../common/telemetry/constants';
import { getTelemetryManagerId } from '../../common/telemetry/helpers';
import { sendTelemetryEvent } from '../../common/telemetry/sender';
import { createDeferred, Deferred } from '../../common/utils/deferred';
import { showErrorMessage } from '../../common/window.apis';
Expand Down Expand Up @@ -52,7 +53,7 @@ export function withManagerTimeout(
`To prevent this, check your "python-envs.defaultEnvManager" and "python-envs.pythonProjects" settings.`,
);
sendTelemetryEvent(EventNames.MANAGER_READY_TIMEOUT, undefined, {
managerId,
managerId: getTelemetryManagerId(managerId),
managerKind: kind,
});
deferred.resolve();
Expand Down
3 changes: 2 additions & 1 deletion src/features/creators/creationHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { traceError, traceVerbose } from '../../common/logging';
import { showQuickPickWithButtons } from '../../common/window.apis';
import type { EnvironmentManagers } from '../envManagers';
import type { InternalEnvironmentManager } from '../../managers/common/registeredManagers';
import { VENV_MANAGER_ID } from '../../common/constants';

/**
* Prompts the user to choose whether to create a new virtual environment (venv) for a project, with a clearer return and early exit.
Expand Down Expand Up @@ -74,7 +75,7 @@ export async function promptForCopilotInstructions(): Promise<boolean | undefine
export async function quickCreateNewVenv(envManagers: EnvironmentManagers, destFolder: string) {
// get the environment manager for venv, should always exist
const envManager: InternalEnvironmentManager | undefined = envManagers.managers.find(
(m) => m.id === 'ms-python.python:venv',
(m) => m.id === VENV_MANAGER_ID,
);
const destinationUri = Uri.parse(destFolder);
if (envManager?.supportsQuickCreate) {
Expand Down
4 changes: 2 additions & 2 deletions src/features/creators/newPackageProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as fs from 'fs-extra';
import * as path from 'path';
import { commands, l10n, MarkdownString, QuickInputButtons, Uri, window, workspace } from 'vscode';
import { PythonEnvironment, PythonProject, PythonProjectCreator, PythonProjectCreatorOptions } from '../../api';
import { NEW_PROJECT_TEMPLATES_FOLDER } from '../../common/constants';
import { NEW_PROJECT_TEMPLATES_FOLDER, VENV_MANAGER_ID } from '../../common/constants';
import { traceError } from '../../common/logging';
import { showInputBoxWithButtons } from '../../common/window.apis';
import type { EnvironmentManagers } from '../envManagers';
Expand Down Expand Up @@ -134,7 +134,7 @@ export class NewPackageProject implements PythonProjectCreator {
} else {
// get venv manager or any manager that supports quick creating environments
const venvManager = this.envManagers.managers.find(
(m) => m.id === 'ms-python.python:venv' || m.supportsQuickCreate,
(m) => m.id === VENV_MANAGER_ID || m.supportsQuickCreate,
);
if (venvManager) {
createdEnv = await venvManager.create(Uri.file(projectDestinationFolder), {
Expand Down
5 changes: 3 additions & 2 deletions src/features/envManagers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import { traceError, traceVerbose } from '../common/logging';
import { StopWatch } from '../common/stopWatch';
import { EventNames } from '../common/telemetry/constants';
import { getTelemetryManagerId } from '../common/telemetry/helpers';
import { sendTelemetryEvent } from '../common/telemetry/sender';
import { getCallingExtension } from '../common/utils/frameUtils';
import { normalizePath } from '../common/utils/pathUtils';
Expand Down Expand Up @@ -307,7 +308,7 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {

if (!managerId.toLowerCase().startsWith('undefined_publisher.')) {
sendTelemetryEvent(EventNames.ENVIRONMENT_MANAGER_REGISTERED, registrationStopWatch.elapsedTime, {
managerId,
managerId: getTelemetryManagerId(managerId),
});
}

Expand Down Expand Up @@ -336,7 +337,7 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {

if (!managerId.toLowerCase().startsWith('undefined_publisher.')) {
sendTelemetryEvent(EventNames.PACKAGE_MANAGER_REGISTERED, undefined, {
managerId,
managerId: getTelemetryManagerId(managerId),
});
}

Expand Down
5 changes: 3 additions & 2 deletions src/features/interpreterSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SYSTEM_MANAGER_ID, VENV_MANAGER_ID } from '../common/constants';
import { traceError, traceInfo, traceVerbose, traceWarn } from '../common/logging';
import { StopWatch } from '../common/stopWatch';
import { EventNames } from '../common/telemetry/constants';
import { getTelemetryManagerId } from '../common/telemetry/helpers';
import { sendTelemetryEvent } from '../common/telemetry/sender';
import { resolveVariables } from '../common/utils/internalVariables';
import { normalizePath } from '../common/utils/pathUtils';
Expand Down Expand Up @@ -318,7 +319,7 @@ export async function applyInitialEnvironmentSelection(
sendTelemetryEvent(EventNames.ENV_SELECTION_RESULT, scopeStopWatch.elapsedTime, {
scope: 'workspace',
prioritySource: result.source,
managerId: result.manager.id,
managerId: getTelemetryManagerId(result.manager.id),
resolutionPath: result.environment ? 'envPreResolved' : 'managerDiscovery',
hasPersistedSelection: env !== undefined,
});
Expand Down Expand Up @@ -357,7 +358,7 @@ export async function applyInitialEnvironmentSelection(
sendTelemetryEvent(EventNames.ENV_SELECTION_RESULT, globalStopWatch.elapsedTime, {
scope: 'global',
prioritySource: result.source,
managerId: result.manager.id,
managerId: getTelemetryManagerId(result.manager.id),
resolutionPath: result.environment ? 'envPreResolved' : 'managerDiscovery',
hasPersistedSelection: env !== undefined,
});
Expand Down
9 changes: 2 additions & 7 deletions src/managers/builtin/inlineScript/envManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,7 @@ import {
SetEnvironmentScope,
} from '../../../api';
import { Capabilities, EnvironmentManagerCapability, supportedCapability, EnvironmentCapability } from '../../../capabilities';
import {
CONDA_MANAGER_ID,
INLINE_SCRIPT_MANAGER_ID,
PYENV_MANAGER_ID,
SYSTEM_MANAGER_ID,
} from '../../../common/constants';
import { CONDA_MANAGER_ID, INLINE_SCRIPT_MANAGER_ID, PYENV_MANAGER_ID, SYSTEM_MANAGER_ID, DEFAULT_PACKAGE_MANAGER_ID } from '../../../common/constants';
import { getErrorMessage } from '../../../common/errors/utils';
import { computeCacheKey, normalizeDependency } from '../../../common/inlineScript/cacheKey';
import { InlineScriptEnvironmentModifiedError } from '../../../common/inlineScript/errors';
Expand Down Expand Up @@ -362,7 +357,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {

public readonly name = 'inline-script';
public readonly displayName = l10n.t('Inline scripts');
public readonly preferredPackageManagerId = 'ms-python.python:pip';
public readonly preferredPackageManagerId = DEFAULT_PACKAGE_MANAGER_ID;
public readonly description: string | undefined = undefined;
public readonly tooltip: string | MarkdownString = new MarkdownString(
l10n.t('Environments built from PEP 723 inline script metadata.'),
Expand Down
3 changes: 2 additions & 1 deletion src/managers/builtin/sysPythonManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
} from './cache';
import { getDefaultGlobalPython, refreshPythons, resolveSystemPythonEnvironmentPath } from './utils';
import { installPythonWithUv, promptInstallPythonViaUv, selectPythonVersionToInstall } from './uvPythonInstaller';
import { DEFAULT_PACKAGE_MANAGER_ID } from '../../common/constants';

export class SysPythonManager implements EnvironmentManager {
readonly capabilities: Capabilities<EnvironmentManagerCapability> = {
Expand Down Expand Up @@ -73,7 +74,7 @@ export class SysPythonManager implements EnvironmentManager {
) {
this.name = 'system';
this.displayName = 'Global';
this.preferredPackageManagerId = 'ms-python.python:pip';
this.preferredPackageManagerId = DEFAULT_PACKAGE_MANAGER_ID;
this.description = undefined;
this.tooltip = new MarkdownString(SysManagerStrings.sysManagerDescription, true);
this.iconPath = new ThemeIcon('globe');
Expand Down
6 changes: 3 additions & 3 deletions src/managers/builtin/venvManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
} from '../../api';
import { Capabilities, EnvironmentManagerCapability, supportedCapability, EnvironmentCapability } from '../../capabilities';
import { executeCommand } from '../../common/command.api';
import { PYTHON_EXTENSION_ID } from '../../common/constants';
import { DEFAULT_PACKAGE_MANAGER_ID, VENV_MANAGER_ID } from '../../common/constants';
import { VenvManagerStrings } from '../../common/localize';
import { traceError, traceWarn } from '../../common/logging';
import { createDeferred, Deferred } from '../../common/utils/deferred';
Expand Down Expand Up @@ -111,7 +111,7 @@ export class VenvManager implements EnvironmentManager {
// https://github.com/microsoft/vscode-python-environments/issues/167
this.description = undefined;
this.tooltip = new MarkdownString(VenvManagerStrings.venvManagerDescription, true);
this.preferredPackageManagerId = 'ms-python.python:pip';
this.preferredPackageManagerId = DEFAULT_PACKAGE_MANAGER_ID;
this.iconPath = new ThemeIcon('python');
}

Expand Down Expand Up @@ -675,7 +675,7 @@ export class VenvManager implements EnvironmentManager {
this.baseManager,
);
if (resolved) {
if (resolved.envId.managerId === `${PYTHON_EXTENSION_ID}:venv`) {
if (resolved.envId.managerId === VENV_MANAGER_ID) {
// We should only return the resolved env if it is a venv.
// Fall through an return undefined if it is not a venv
this.addEnvironment(resolved, true);
Expand Down
Loading
Loading