From d563d5bbb4872a7dddb5b520258fb05979417129 Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Mon, 23 Feb 2026 15:55:40 -0800 Subject: [PATCH 1/5] add telemetry --- src/common/telemetry/constants.ts | 17 +++++++++ src/common/telemetry/helpers.ts | 63 ++++++++++++++++++++++++++++++- src/extension.ts | 7 +++- 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/common/telemetry/constants.ts b/src/common/telemetry/constants.ts index 508cbaea..ebad1e6b 100644 --- a/src/common/telemetry/constants.ts +++ b/src/common/telemetry/constants.ts @@ -32,6 +32,14 @@ export enum EventNames { * - projectUnderRoot: number (count of projects nested under workspace roots) */ PROJECT_STRUCTURE = 'PROJECT_STRUCTURE', + /** + * Telemetry event for environment tool usage at extension startup. + * Fires once per tool that has at least one project using it. + * Use dcount(machineId) by toolName to get unique users per tool. + * Properties: + * - toolName: string (the tool being used: venv, conda, poetry, etc.) + */ + ENVIRONMENT_TOOL_USAGE = 'ENVIRONMENT_TOOL_USAGE', } // Map all events to their properties @@ -180,4 +188,13 @@ export interface IEventNamePropertyMapping { uniqueInterpreterCount: number; projectUnderRoot: number; }; + + /* __GDPR__ + "environment_tool_usage": { + "toolName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "stellaHuang95" } + } + */ + [EventNames.ENVIRONMENT_TOOL_USAGE]: { + toolName: string; + }; } diff --git a/src/common/telemetry/helpers.ts b/src/common/telemetry/helpers.ts index 8a226d0f..d9203c98 100644 --- a/src/common/telemetry/helpers.ts +++ b/src/common/telemetry/helpers.ts @@ -4,6 +4,17 @@ import { getWorkspaceFolders } from '../workspace.apis'; import { EventNames } from './constants'; import { sendTelemetryEvent } from './sender'; +/** + * Extracts the base tool name from a manager ID. + * Example: 'ms-python.python:venv' -> 'venv' + * Example: 'ms-python.python:conda' -> 'conda' + */ +function extractToolName(managerId: string): string { + // Manager IDs follow the pattern 'extensionId:toolName' + const parts = managerId.split(':'); + return parts.length > 1 ? parts[1].toLowerCase() : managerId.toLowerCase(); +} + export function sendManagerSelectionTelemetry(pm: PythonProjectManager) { const ems: Set = new Set(); const ps: Set = new Set(); @@ -58,7 +69,7 @@ export async function sendProjectStructureTelemetry( for (const wsFolder of workspaceFolders) { const workspacePath = wsFolder.uri.fsPath; const projectPath = project.uri.fsPath; - + // Check if project is a subdirectory of workspace folder: // - Path must start with workspace path // - Path must not be equal to workspace path @@ -80,3 +91,53 @@ export async function sendProjectStructureTelemetry( projectUnderRoot, }); } + +/** + * Sends telemetry about which environment tools are actively used across all projects. + * This tracks ACTUAL USAGE (which environments are set for projects), not just what's installed. + * + * Fires one event per tool that has at least one project using it. + * This allows simple deduplication: dcount(machineId) by toolName gives unique users per tool. + * + * Called once at extension activation to understand user's environment tool usage patterns. + */ +export async function sendEnvironmentToolUsageTelemetry( + pm: PythonProjectManager, + envManagers: EnvironmentManagers, +): Promise { + const projects = pm.getProjects(); + + // Track which tools are used (Set ensures uniqueness) + const toolsUsed = new Set(); + + // Check which environment manager is used for each project + for (const project of projects) { + try { + const env = await envManagers.getEnvironment(project.uri); + if (env?.envId?.managerId) { + const toolName = extractToolName(env.envId.managerId); + + // Check if this is a UV environment (UV uses venv manager but has 'uv' in description) + const isUv = env.description?.toLowerCase().includes('uv') ?? false; + + // Determine the tool name + if (isUv) { + toolsUsed.add('uv'); + } else { + // Normalize 'global' to 'system' for consistency + const normalizedTool = toolName === 'global' ? 'system' : toolName; + toolsUsed.add(normalizedTool); + } + } + } catch { + // Ignore errors when getting environment for a project + } + } + + // Fire one event per tool used + toolsUsed.forEach((tool) => { + sendTelemetryEvent(EventNames.ENVIRONMENT_TOOL_USAGE, undefined, { + toolName: tool, + }); + }); +} diff --git a/src/extension.ts b/src/extension.ts index b03c5132..c542fdbe 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -17,7 +17,11 @@ import { clearPersistentState, setPersistentState } from './common/persistentSta import { newProjectSelection } from './common/pickers/managers'; import { StopWatch } from './common/stopWatch'; import { EventNames } from './common/telemetry/constants'; -import { sendManagerSelectionTelemetry, sendProjectStructureTelemetry } from './common/telemetry/helpers'; +import { + sendEnvironmentToolUsageTelemetry, + sendManagerSelectionTelemetry, + sendProjectStructureTelemetry, +} from './common/telemetry/helpers'; import { sendTelemetryEvent } from './common/telemetry/sender'; import { createDeferred } from './common/utils/deferred'; @@ -545,6 +549,7 @@ export async function activate(context: ExtensionContext): Promise Date: Mon, 23 Feb 2026 16:13:13 -0800 Subject: [PATCH 2/5] check for uv --- src/common/telemetry/helpers.ts | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/common/telemetry/helpers.ts b/src/common/telemetry/helpers.ts index d9203c98..84946fbb 100644 --- a/src/common/telemetry/helpers.ts +++ b/src/common/telemetry/helpers.ts @@ -1,5 +1,6 @@ import { getDefaultEnvManagerSetting, getDefaultPkgManagerSetting } from '../../features/settings/settingHelpers'; import { EnvironmentManagers, PythonProjectManager } from '../../internal.api'; +import { getUvEnvironments } from '../../managers/builtin/uvEnvironments'; import { getWorkspaceFolders } from '../workspace.apis'; import { EventNames } from './constants'; import { sendTelemetryEvent } from './sender'; @@ -115,19 +116,23 @@ export async function sendEnvironmentToolUsageTelemetry( try { const env = await envManagers.getEnvironment(project.uri); if (env?.envId?.managerId) { - const toolName = extractToolName(env.envId.managerId); - - // Check if this is a UV environment (UV uses venv manager but has 'uv' in description) - const isUv = env.description?.toLowerCase().includes('uv') ?? false; - - // Determine the tool name - if (isUv) { - toolsUsed.add('uv'); - } else { - // Normalize 'global' to 'system' for consistency - const normalizedTool = toolName === 'global' ? 'system' : toolName; - toolsUsed.add(normalizedTool); + let toolName = extractToolName(env.envId.managerId); + + // UV environments share the venv manager. Check the persistent UV env list instead + if (toolName === 'venv' && env.environmentPath) { + // Lazily load UV environment paths only when a venv environment is encountered + const uvEnvPaths = await getUvEnvironments(); + if (uvEnvPaths.includes(env.environmentPath.fsPath)) { + toolName = 'uv'; + } } + + // Normalize 'global' to 'system' for consistency + if (toolName === 'global') { + toolName = 'system'; + } + + toolsUsed.add(toolName); } } catch { // Ignore errors when getting environment for a project From 23c96ed68e22d303cad05a81935ffea87fb92109 Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Mon, 23 Feb 2026 16:31:43 -0800 Subject: [PATCH 3/5] catch exception --- src/common/telemetry/helpers.ts | 70 ++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/src/common/telemetry/helpers.ts b/src/common/telemetry/helpers.ts index 84946fbb..88382b6a 100644 --- a/src/common/telemetry/helpers.ts +++ b/src/common/telemetry/helpers.ts @@ -1,6 +1,7 @@ import { getDefaultEnvManagerSetting, getDefaultPkgManagerSetting } from '../../features/settings/settingHelpers'; import { EnvironmentManagers, PythonProjectManager } from '../../internal.api'; import { getUvEnvironments } from '../../managers/builtin/uvEnvironments'; +import { traceVerbose } from '../logging'; import { getWorkspaceFolders } from '../workspace.apis'; import { EventNames } from './constants'; import { sendTelemetryEvent } from './sender'; @@ -106,43 +107,50 @@ export async function sendEnvironmentToolUsageTelemetry( pm: PythonProjectManager, envManagers: EnvironmentManagers, ): Promise { - const projects = pm.getProjects(); - - // Track which tools are used (Set ensures uniqueness) - const toolsUsed = new Set(); + try { + const projects = pm.getProjects(); + + // Track which tools are used (Set ensures uniqueness) + const toolsUsed = new Set(); + + // Lazily loaded once when a venv environment is first encountered + let uvEnvPaths: string[] | undefined; + + // Check which environment manager is used for each project + for (const project of projects) { + try { + const env = await envManagers.getEnvironment(project.uri); + if (env?.envId?.managerId) { + let toolName = extractToolName(env.envId.managerId); + + // UV environments share the venv manager. Check the persistent UV env list instead + if (toolName === 'venv' && env.environmentPath) { + uvEnvPaths ??= await getUvEnvironments(); + if (uvEnvPaths.includes(env.environmentPath.fsPath)) { + toolName = 'uv'; + } + } - // Check which environment manager is used for each project - for (const project of projects) { - try { - const env = await envManagers.getEnvironment(project.uri); - if (env?.envId?.managerId) { - let toolName = extractToolName(env.envId.managerId); - - // UV environments share the venv manager. Check the persistent UV env list instead - if (toolName === 'venv' && env.environmentPath) { - // Lazily load UV environment paths only when a venv environment is encountered - const uvEnvPaths = await getUvEnvironments(); - if (uvEnvPaths.includes(env.environmentPath.fsPath)) { - toolName = 'uv'; + // Normalize 'global' to 'system' for consistency + if (toolName === 'global') { + toolName = 'system'; } - } - // Normalize 'global' to 'system' for consistency - if (toolName === 'global') { - toolName = 'system'; + toolsUsed.add(toolName); } - - toolsUsed.add(toolName); + } catch { + // Ignore errors when getting environment for a project } - } catch { - // Ignore errors when getting environment for a project } - } - // Fire one event per tool used - toolsUsed.forEach((tool) => { - sendTelemetryEvent(EventNames.ENVIRONMENT_TOOL_USAGE, undefined, { - toolName: tool, + // Fire one event per tool used + toolsUsed.forEach((tool) => { + sendTelemetryEvent(EventNames.ENVIRONMENT_TOOL_USAGE, undefined, { + toolName: tool, + }); }); - }); + } catch (error) { + // Telemetry failures must never disrupt extension activation + traceVerbose('Failed to send environment tool usage telemetry:', error); + } } From 0a348884890ee97f1713242dc352bbfd0e31c13b Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Tue, 24 Feb 2026 14:58:04 -0800 Subject: [PATCH 4/5] clean up --- src/common/telemetry/constants.ts | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/common/telemetry/constants.ts b/src/common/telemetry/constants.ts index 7766750f..3664bfe0 100644 --- a/src/common/telemetry/constants.ts +++ b/src/common/telemetry/constants.ts @@ -40,6 +40,7 @@ export enum EventNames { * - toolName: string (the tool being used: venv, conda, poetry, etc.) */ ENVIRONMENT_TOOL_USAGE = 'ENVIRONMENT_TOOL_USAGE', + /** * Telemetry event for environment discovery per manager. * Properties: * - managerId: string (the id of the environment manager) @@ -209,14 +210,24 @@ export interface IEventNamePropertyMapping { */ [EventNames.ENVIRONMENT_TOOL_USAGE]: { toolName: string; - "environment_discovery": { - "managerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, - "result": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, - "envCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" }, - "errorType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, - "": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" } - } - */ + environment_discovery: { + managerId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'eleanorjboyd' }; + result: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'eleanorjboyd' }; + envCount: { + classification: 'SystemMetaData'; + purpose: 'FeatureInsight'; + isMeasurement: true; + owner: 'eleanorjboyd'; + }; + errorType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'eleanorjboyd' }; + '': { + classification: 'SystemMetaData'; + purpose: 'FeatureInsight'; + isMeasurement: true; + owner: 'eleanorjboyd'; + }; + }; + }; [EventNames.ENVIRONMENT_DISCOVERY]: { managerId: string; result: 'success' | 'error' | 'timeout'; From 97bd553751f514743ee63f62566424047a2547ba Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Tue, 24 Feb 2026 15:00:15 -0800 Subject: [PATCH 5/5] merge conflicts --- src/common/telemetry/constants.ts | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/common/telemetry/constants.ts b/src/common/telemetry/constants.ts index 3664bfe0..9bcd68ea 100644 --- a/src/common/telemetry/constants.ts +++ b/src/common/telemetry/constants.ts @@ -210,24 +210,16 @@ export interface IEventNamePropertyMapping { */ [EventNames.ENVIRONMENT_TOOL_USAGE]: { toolName: string; - environment_discovery: { - managerId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'eleanorjboyd' }; - result: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'eleanorjboyd' }; - envCount: { - classification: 'SystemMetaData'; - purpose: 'FeatureInsight'; - isMeasurement: true; - owner: 'eleanorjboyd'; - }; - errorType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'eleanorjboyd' }; - '': { - classification: 'SystemMetaData'; - purpose: 'FeatureInsight'; - isMeasurement: true; - owner: 'eleanorjboyd'; - }; - }; }; + /* __GDPR__ + "environment_discovery": { + "managerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "result": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "envCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" }, + "errorType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" } + } + */ [EventNames.ENVIRONMENT_DISCOVERY]: { managerId: string; result: 'success' | 'error' | 'timeout';