Skip to content

Commit 0c7429b

Browse files
Add ENVIRONMENT_TOOL_USAGE telemetry to track actual environment tool usage (#1272)
This is a follow-up PR after discussion with Eleanor and Graham, the PR adds a new ENVIRONMENT_TOOL_USAGE telemetry event that fires once per distinct environment tool actively in use across a user's projects. It resolves each project's actual environment and emits `toolName` for each unique tool (e.g., venv, uv, conda, poetry, system). UV environments are detected separately since they share the `venv` manager. Will add a new chart to the dashboard once it's checked in.
1 parent b3a23df commit 0c7429b

File tree

3 files changed

+97
-2
lines changed

3 files changed

+97
-2
lines changed

src/common/telemetry/constants.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ export enum EventNames {
3232
* - projectUnderRoot: number (count of projects nested under workspace roots)
3333
*/
3434
PROJECT_STRUCTURE = 'PROJECT_STRUCTURE',
35+
/**
36+
* Telemetry event for environment tool usage at extension startup.
37+
* Fires once per tool that has at least one project using it.
38+
* Use dcount(machineId) by toolName to get unique users per tool.
39+
* Properties:
40+
* - toolName: string (the tool being used: venv, conda, poetry, etc.)
41+
*/
42+
ENVIRONMENT_TOOL_USAGE = 'ENVIRONMENT_TOOL_USAGE',
3543
/**
3644
* Telemetry event for environment discovery per manager.
3745
* Properties:
@@ -195,6 +203,14 @@ export interface IEventNamePropertyMapping {
195203
projectUnderRoot: number;
196204
};
197205

206+
/* __GDPR__
207+
"environment_tool_usage": {
208+
"toolName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "stellaHuang95" }
209+
}
210+
*/
211+
[EventNames.ENVIRONMENT_TOOL_USAGE]: {
212+
toolName: string;
213+
};
198214
/* __GDPR__
199215
"environment_discovery": {
200216
"managerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },

src/common/telemetry/helpers.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
import { getDefaultEnvManagerSetting, getDefaultPkgManagerSetting } from '../../features/settings/settingHelpers';
22
import { EnvironmentManagers, PythonProjectManager } from '../../internal.api';
3+
import { getUvEnvironments } from '../../managers/builtin/uvEnvironments';
4+
import { traceVerbose } from '../logging';
35
import { getWorkspaceFolders } from '../workspace.apis';
46
import { EventNames } from './constants';
57
import { sendTelemetryEvent } from './sender';
68

9+
/**
10+
* Extracts the base tool name from a manager ID.
11+
* Example: 'ms-python.python:venv' -> 'venv'
12+
* Example: 'ms-python.python:conda' -> 'conda'
13+
*/
14+
function extractToolName(managerId: string): string {
15+
// Manager IDs follow the pattern 'extensionId:toolName'
16+
const parts = managerId.split(':');
17+
return parts.length > 1 ? parts[1].toLowerCase() : managerId.toLowerCase();
18+
}
19+
720
export function sendManagerSelectionTelemetry(pm: PythonProjectManager) {
821
const ems: Set<string> = new Set();
922
const ps: Set<string> = new Set();
@@ -58,7 +71,7 @@ export async function sendProjectStructureTelemetry(
5871
for (const wsFolder of workspaceFolders) {
5972
const workspacePath = wsFolder.uri.fsPath;
6073
const projectPath = project.uri.fsPath;
61-
74+
6275
// Check if project is a subdirectory of workspace folder:
6376
// - Path must start with workspace path
6477
// - Path must not be equal to workspace path
@@ -80,3 +93,64 @@ export async function sendProjectStructureTelemetry(
8093
projectUnderRoot,
8194
});
8295
}
96+
97+
/**
98+
* Sends telemetry about which environment tools are actively used across all projects.
99+
* This tracks ACTUAL USAGE (which environments are set for projects), not just what's installed.
100+
*
101+
* Fires one event per tool that has at least one project using it.
102+
* This allows simple deduplication: dcount(machineId) by toolName gives unique users per tool.
103+
*
104+
* Called once at extension activation to understand user's environment tool usage patterns.
105+
*/
106+
export async function sendEnvironmentToolUsageTelemetry(
107+
pm: PythonProjectManager,
108+
envManagers: EnvironmentManagers,
109+
): Promise<void> {
110+
try {
111+
const projects = pm.getProjects();
112+
113+
// Track which tools are used (Set ensures uniqueness)
114+
const toolsUsed = new Set<string>();
115+
116+
// Lazily loaded once when a venv environment is first encountered
117+
let uvEnvPaths: string[] | undefined;
118+
119+
// Check which environment manager is used for each project
120+
for (const project of projects) {
121+
try {
122+
const env = await envManagers.getEnvironment(project.uri);
123+
if (env?.envId?.managerId) {
124+
let toolName = extractToolName(env.envId.managerId);
125+
126+
// UV environments share the venv manager. Check the persistent UV env list instead
127+
if (toolName === 'venv' && env.environmentPath) {
128+
uvEnvPaths ??= await getUvEnvironments();
129+
if (uvEnvPaths.includes(env.environmentPath.fsPath)) {
130+
toolName = 'uv';
131+
}
132+
}
133+
134+
// Normalize 'global' to 'system' for consistency
135+
if (toolName === 'global') {
136+
toolName = 'system';
137+
}
138+
139+
toolsUsed.add(toolName);
140+
}
141+
} catch {
142+
// Ignore errors when getting environment for a project
143+
}
144+
}
145+
146+
// Fire one event per tool used
147+
toolsUsed.forEach((tool) => {
148+
sendTelemetryEvent(EventNames.ENVIRONMENT_TOOL_USAGE, undefined, {
149+
toolName: tool,
150+
});
151+
});
152+
} catch (error) {
153+
// Telemetry failures must never disrupt extension activation
154+
traceVerbose('Failed to send environment tool usage telemetry:', error);
155+
}
156+
}

src/extension.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import { clearPersistentState, setPersistentState } from './common/persistentSta
1717
import { newProjectSelection } from './common/pickers/managers';
1818
import { StopWatch } from './common/stopWatch';
1919
import { EventNames } from './common/telemetry/constants';
20-
import { sendManagerSelectionTelemetry, sendProjectStructureTelemetry } from './common/telemetry/helpers';
20+
import {
21+
sendEnvironmentToolUsageTelemetry,
22+
sendManagerSelectionTelemetry,
23+
sendProjectStructureTelemetry,
24+
} from './common/telemetry/helpers';
2125
import { sendTelemetryEvent } from './common/telemetry/sender';
2226
import { createDeferred } from './common/utils/deferred';
2327

@@ -545,6 +549,7 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
545549
await terminalManager.initialize(api);
546550
sendManagerSelectionTelemetry(projectManager);
547551
await sendProjectStructureTelemetry(projectManager, envManagers);
552+
await sendEnvironmentToolUsageTelemetry(projectManager, envManagers);
548553
} catch (error) {
549554
traceError('Failed to initialize environment managers:', error);
550555
// Show a user-friendly error message

0 commit comments

Comments
 (0)