Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
22 changes: 22 additions & 0 deletions .github/prompts/add-telemetry.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
mode: agent
---

If the user does not specify an event name or properties, pick an informative and descriptive name for the telemetry event based on the task or feature. Add properties as you see fit to collect the necessary information to achieve the telemetry goal, ensuring they are relevant and useful for diagnostics or analytics.

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).

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 });
```
4. If the event is triggered by a command, ensure the call is placed at the start of the command handler.

Expected output: The new event is tracked in telemetry and follows the GDPR and codebase conventions.
15 changes: 11 additions & 4 deletions src/common/pickers/environments.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Uri, ThemeIcon, QuickPickItem, QuickPickItemKind, ProgressLocation, QuickInputButtons } from 'vscode';
import { ProgressLocation, QuickInputButtons, QuickPickItem, QuickPickItemKind, ThemeIcon, Uri } from 'vscode';
import { IconPath, PythonEnvironment, PythonProject } from '../../api';
import { InternalEnvironmentManager } from '../../internal.api';
import { Common, Interpreter, Pickers } from '../localize';
import { showQuickPickWithButtons, showQuickPick, showOpenDialog, withProgress } from '../window.apis';
import { traceError } from '../logging';
import { pickEnvironmentManager } from './managers';
import { handlePythonPath } from '../utils/pythonPath';
import { EventNames } from '../telemetry/constants';
import { sendTelemetryEvent } from '../telemetry/sender';
import { isWindows } from '../utils/platformUtils';
import { handlePythonPath } from '../utils/pythonPath';
import { showOpenDialog, showQuickPick, showQuickPickWithButtons, withProgress } from '../window.apis';
import { pickEnvironmentManager } from './managers';

type QuickPickIcon =
| Uri
Expand Down Expand Up @@ -80,6 +82,7 @@ async function createEnvironment(
const manager = managers.find((m) => m.id === managerId);
if (manager) {
try {
// add telemetry here
const env = await manager.create(
options.projects.map((p) => p.uri),
undefined,
Expand Down Expand Up @@ -111,6 +114,10 @@ async function pickEnvironmentImpl(
if (selected.label === Interpreter.browsePath) {
return browseForPython(managers, projectEnvManagers);
} else if (selected.label === Interpreter.createVirtualEnvironment) {
sendTelemetryEvent(EventNames.CREATE_ENVIRONMENT, undefined, {
manager: 'none',
triggeredLocation: 'pickEnv',
});
return createEnvironment(managers, projectEnvManagers, options);
}
return (selected as { result: PythonEnvironment })?.result;
Expand Down
54 changes: 44 additions & 10 deletions src/common/telemetry/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,34 @@ export enum EventNames {
VENV_CREATION = 'VENV.CREATION',

PACKAGE_MANAGEMENT = 'PACKAGE_MANAGEMENT',
ADD_PROJECT = 'ADD_PROJECT',
/**
* Telemetry event for when a Python environment is created via command.
* Properties:
* - manager: string (the id of the environment manager used, or 'none')
* - triggeredLocation: string (where the create command is called from)
*/
CREATE_ENVIRONMENT = 'CREATE_ENVIRONMENT',
}

// Map all events to their properties
export interface IEventNamePropertyMapping {
/* __GDPR__
"extension.activation_duration": {
"duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }
"duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
}
*/
[EventNames.EXTENSION_ACTIVATION_DURATION]: never | undefined;
/* __GDPR__
"extension.manager_registration_duration": {
"duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }
"duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
}
*/
[EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION]: never | undefined;

/* __GDPR__
"environment_manager.registered": {
"managerId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }
"managerId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
}
*/
[EventNames.ENVIRONMENT_MANAGER_REGISTERED]: {
Expand All @@ -39,7 +47,7 @@ export interface IEventNamePropertyMapping {

/* __GDPR__
"package_manager.registered": {
"managerId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }
"managerId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
}
*/
[EventNames.PACKAGE_MANAGER_REGISTERED]: {
Expand All @@ -48,7 +56,7 @@ export interface IEventNamePropertyMapping {

/* __GDPR__
"environment_manager.selected": {
"managerId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }
"managerId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
}
*/
[EventNames.ENVIRONMENT_MANAGER_SELECTED]: {
Expand All @@ -57,19 +65,19 @@ export interface IEventNamePropertyMapping {

/* __GDPR__
"package_manager.selected": {
"managerId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }
"managerId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
}
*/
[EventNames.PACKAGE_MANAGER_SELECTED]: {
managerId: string;
};

/* __GDPR__
"venv.using_uv": {"owner": "karthiknadig" }
"venv.using_uv": {"owner": "eleanorjboyd" }
*/
[EventNames.VENV_USING_UV]: never | undefined /* __GDPR__
"venv.creation": {
"creationType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }
"creationType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
}
*/;
[EventNames.VENV_CREATION]: {
Expand All @@ -78,12 +86,38 @@ export interface IEventNamePropertyMapping {

/* __GDPR__
"package_management": {
"managerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" },
"result": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }
"managerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
"result": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
}
*/
[EventNames.PACKAGE_MANAGEMENT]: {
managerId: string;
result: 'success' | 'error' | 'cancelled';
};

/* __GDPR__
"add_project": {
"template": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
"quickCreate": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
"totalProjectCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
"triggeredLocation": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
}
*/
[EventNames.ADD_PROJECT]: {
template: string;
quickCreate: boolean;
totalProjectCount: number;
triggeredLocation: 'templateCreate' | 'add' | 'addGivenResource';
};

/* __GDPR__
"create_environment": {
"manager": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
"triggeredLocation": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
}
*/
[EventNames.CREATE_ENVIRONMENT]: {
manager: string;
triggeredLocation: string;
};
}
66 changes: 52 additions & 14 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { commands, extensions, ExtensionContext, LogOutputChannel, Terminal, Uri, window, workspace } from 'vscode';
import { commands, ExtensionContext, extensions, LogOutputChannel, Terminal, Uri, window, workspace } from 'vscode';
import { PythonEnvironment, PythonEnvironmentApi, PythonProjectCreator } from './api';
import { ensureCorrectVersion } from './common/extVersion';
import { registerLogger, traceError, traceInfo } from './common/logging';
Expand Down Expand Up @@ -75,27 +75,27 @@ import { registerPyenvFeatures } from './managers/pyenv/main';
async function collectEnvironmentInfo(
context: ExtensionContext,
envManagers: EnvironmentManagers,
projectManager: PythonProjectManager
projectManager: PythonProjectManager,
): Promise<string> {
const info: string[] = [];

try {
// Extension version
const extensionVersion = context.extension?.packageJSON?.version || 'unknown';
info.push(`Extension Version: ${extensionVersion}`);

// Python extension version
const pythonExtension = extensions.getExtension('ms-python.python');
const pythonVersion = pythonExtension?.packageJSON?.version || 'not installed';
info.push(`Python Extension Version: ${pythonVersion}`);

// Environment managers
const managers = envManagers.managers;
info.push(`\nRegistered Environment Managers (${managers.length}):`);
managers.forEach(manager => {
managers.forEach((manager) => {
info.push(` - ${manager.id} (${manager.displayName})`);
});

// Available environments
const allEnvironments: PythonEnvironment[] = [];
for (const manager of managers) {
Expand All @@ -106,7 +106,7 @@ async function collectEnvironmentInfo(
info.push(` Error getting environments from ${manager.id}: ${err}`);
}
}

info.push(`\nTotal Available Environments: ${allEnvironments.length}`);
if (allEnvironments.length > 0) {
info.push('Environment Details:');
Expand All @@ -117,7 +117,7 @@ async function collectEnvironmentInfo(
info.push(` ... and ${allEnvironments.length - 10} more environments`);
}
}

// Python projects
const projects = projectManager.getProjects();
info.push(`\nPython Projects (${projects.length}):`);
Expand All @@ -133,18 +133,17 @@ async function collectEnvironmentInfo(
info.push(` Error getting environment: ${err}`);
}
}

// Current settings (non-sensitive)
const config = workspace.getConfiguration('python-envs');
info.push('\nExtension Settings:');
info.push(` Default Environment Manager: ${config.get('defaultEnvManager')}`);
info.push(` Default Package Manager: ${config.get('defaultPackageManager')}`);
info.push(` Terminal Auto Activation: ${config.get('terminal.autoActivationType')}`);

} catch (err) {
info.push(`\nError collecting environment information: ${err}`);
}

return info.join('\n');
}

Expand Down Expand Up @@ -232,9 +231,23 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
await refreshPackagesCommand(item, envManagers);
}),
commands.registerCommand('python-envs.create', async (item) => {
// Telemetry: record environment creation attempt with selected manager
let managerId = 'unknown';
if (item && item.manager && item.manager.id) {
managerId = item.manager.id;
}
sendTelemetryEvent(EventNames.CREATE_ENVIRONMENT, undefined, {
manager: managerId,
triggeredLocation: 'createSpecifiedCommand',
});
return await createEnvironmentCommand(item, envManagers, projectManager);
}),
commands.registerCommand('python-envs.createAny', async (options) => {
// Telemetry: record environment creation attempt with no specific manager
sendTelemetryEvent(EventNames.CREATE_ENVIRONMENT, undefined, {
manager: 'none',
triggeredLocation: 'createAnyCommand',
});
return await createAnyEnvironmentCommand(
envManagers,
projectManager,
Expand Down Expand Up @@ -273,13 +286,28 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
}),
commands.registerCommand('python-envs.addPythonProject', async () => {
await addPythonProjectCommand(undefined, projectManager, envManagers, projectCreators);
const totalProjectCount = projectManager.getProjects().length + 1;
sendTelemetryEvent(EventNames.ADD_PROJECT, undefined, {
template: 'none',
quickCreate: false,
totalProjectCount,
triggeredLocation: 'add',
});
}),
commands.registerCommand('python-envs.addPythonProjectGivenResource', async (resource) => {
// Set context to show/hide menu item depending on whether the resource is already a Python project
if (resource instanceof Uri) {
commands.executeCommand('setContext', 'python-envs:isExistingProject', isExistingProject(resource));
}

await addPythonProjectCommand(resource, projectManager, envManagers, projectCreators);
const totalProjectCount = projectManager.getProjects().length + 1;
sendTelemetryEvent(EventNames.ADD_PROJECT, undefined, {
template: 'none',
quickCreate: false,
totalProjectCount,
triggeredLocation: 'addGivenResource',
});
}),
commands.registerCommand('python-envs.removePythonProject', async (item) => {
// Clear environment association before removing project
Expand Down Expand Up @@ -335,6 +363,9 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
commands.registerCommand(
'python-envs.createNewProjectFromTemplate',
async (projectType: string, quickCreate: boolean, newProjectName: string, newProjectPath: string) => {
let projectTemplateName = projectType || 'unknown';
let triggeredLocation: 'templateCreate' = 'templateCreate';
let totalProjectCount = projectManager.getProjects().length + 1;
if (quickCreate) {
if (!projectType || !newProjectName || !newProjectPath) {
throw new Error('Project type, name, and path are required for quick create.');
Expand All @@ -358,19 +389,26 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
} else {
const selected = await newProjectSelection(projectCreators.getProjectCreators());
if (selected) {
projectTemplateName = selected.name || 'unknown';
await selected.create();
}
}
sendTelemetryEvent(EventNames.ADD_PROJECT, undefined, {
template: projectTemplateName,
quickCreate: quickCreate,
totalProjectCount,
triggeredLocation,
});
},
),
commands.registerCommand('python-envs.reportIssue', async () => {
try {
const issueData = await collectEnvironmentInfo(context, envManagers, projectManager);

await commands.executeCommand('workbench.action.openIssueReporter', {
extensionId: 'ms-python.vscode-python-envs',
issueTitle: '[Python Environments] ',
issueBody: `<!-- Please describe the issue you're experiencing -->\n\n<!-- The following information was automatically generated -->\n\n<details>\n<summary>Environment Information</summary>\n\n\`\`\`\n${issueData}\n\`\`\`\n\n</details>`
issueBody: `<!-- Please describe the issue you're experiencing -->\n\n<!-- The following information was automatically generated -->\n\n<details>\n<summary>Environment Information</summary>\n\n\`\`\`\n${issueData}\n\`\`\`\n\n</details>`,
});
} catch (error) {
window.showErrorMessage(`Failed to open issue reporter: ${error}`);
Expand Down