From 804716f8e70108d46c628d59c824be9354c39bfd Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 19 Jun 2025 11:23:38 -0700 Subject: [PATCH 1/3] add telemetry for addProject actions --- .github/prompts/add-telemetry.prompt.md | 22 +++++++++++ src/common/telemetry/constants.ts | 16 ++++++++ src/extension.ts | 52 ++++++++++++++++++------- 3 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 .github/prompts/add-telemetry.prompt.md diff --git a/.github/prompts/add-telemetry.prompt.md b/.github/prompts/add-telemetry.prompt.md new file mode 100644 index 00000000..18c643d8 --- /dev/null +++ b/.github/prompts/add-telemetry.prompt.md @@ -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. diff --git a/src/common/telemetry/constants.ts b/src/common/telemetry/constants.ts index 14a653da..8621818b 100644 --- a/src/common/telemetry/constants.ts +++ b/src/common/telemetry/constants.ts @@ -11,6 +11,7 @@ export enum EventNames { VENV_CREATION = 'VENV.CREATION', PACKAGE_MANAGEMENT = 'PACKAGE_MANAGEMENT', + ADD_PROJECT = 'ADD_PROJECT', } // Map all events to their properties @@ -86,4 +87,19 @@ export interface IEventNamePropertyMapping { 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'; + }; } diff --git a/src/extension.ts b/src/extension.ts index 7386d97e..9aa445c8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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'; @@ -75,27 +75,27 @@ import { registerPyenvFeatures } from './managers/pyenv/main'; async function collectEnvironmentInfo( context: ExtensionContext, envManagers: EnvironmentManagers, - projectManager: PythonProjectManager + projectManager: PythonProjectManager, ): Promise { 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) { @@ -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:'); @@ -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}):`); @@ -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'); } @@ -273,13 +272,28 @@ export async function activate(context: ExtensionContext): Promise { 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 @@ -335,6 +349,9 @@ export async function activate(context: ExtensionContext): Promise { + 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.'); @@ -358,19 +375,26 @@ export async function activate(context: ExtensionContext): Promise { try { const issueData = await collectEnvironmentInfo(context, envManagers, projectManager); - + await commands.executeCommand('workbench.action.openIssueReporter', { extensionId: 'ms-python.vscode-python-envs', issueTitle: '[Python Environments] ', - issueBody: `\n\n\n\n
\nEnvironment Information\n\n\`\`\`\n${issueData}\n\`\`\`\n\n
` + issueBody: `\n\n\n\n
\nEnvironment Information\n\n\`\`\`\n${issueData}\n\`\`\`\n\n
`, }); } catch (error) { window.showErrorMessage(`Failed to open issue reporter: ${error}`); From 9783834e3f03b8b86e71dad1f34991142f75d26e Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 19 Jun 2025 13:59:51 -0700 Subject: [PATCH 2/3] add create env trigger telemetry --- src/common/pickers/environments.ts | 15 +++++++++++---- src/common/telemetry/constants.ts | 18 ++++++++++++++++++ src/extension.ts | 14 ++++++++++++++ 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/common/pickers/environments.ts b/src/common/pickers/environments.ts index 058e1ad7..7238efbe 100644 --- a/src/common/pickers/environments.ts +++ b/src/common/pickers/environments.ts @@ -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 @@ -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, @@ -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; diff --git a/src/common/telemetry/constants.ts b/src/common/telemetry/constants.ts index 8621818b..42739c04 100644 --- a/src/common/telemetry/constants.ts +++ b/src/common/telemetry/constants.ts @@ -12,6 +12,13 @@ export enum EventNames { 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 @@ -102,4 +109,15 @@ export interface IEventNamePropertyMapping { 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; + }; } diff --git a/src/extension.ts b/src/extension.ts index 9aa445c8..762a474b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -231,9 +231,23 @@ export async function activate(context: ExtensionContext): Promise { + // 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, From 2d4dedf7672effc11599b7704288b8c478d0afd2 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 23 Jun 2025 13:23:36 -0700 Subject: [PATCH 3/3] update telemetry owner --- src/common/telemetry/constants.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/common/telemetry/constants.ts b/src/common/telemetry/constants.ts index 42739c04..d9ae5d9f 100644 --- a/src/common/telemetry/constants.ts +++ b/src/common/telemetry/constants.ts @@ -25,20 +25,20 @@ export enum EventNames { 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]: { @@ -47,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]: { @@ -56,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]: { @@ -65,7 +65,7 @@ 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]: { @@ -73,11 +73,11 @@ export interface IEventNamePropertyMapping { }; /* __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]: { @@ -86,8 +86,8 @@ 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]: {