Skip to content

Commit 724167e

Browse files
Merge branch 'main' into telemetry
2 parents 23c96ed + 6703525 commit 724167e

5 files changed

Lines changed: 110 additions & 11 deletions

File tree

analysis/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ requires = ["uv_build>=0.10.2,<0.11.0"]
33
build-backend = "uv_build"
44

55
[tool.uv.build-backend]
6-
module-root = ""
6+
module-root = ".."
77

88
[project]
99
name = "analysis"

src/common/telemetry/constants.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ export enum EventNames {
4040
* - toolName: string (the tool being used: venv, conda, poetry, etc.)
4141
*/
4242
ENVIRONMENT_TOOL_USAGE = 'ENVIRONMENT_TOOL_USAGE',
43+
* Telemetry event for environment discovery per manager.
44+
* Properties:
45+
* - managerId: string (the id of the environment manager)
46+
* - result: 'success' | 'error' | 'timeout'
47+
* - envCount: number (environments found, on success only)
48+
* - errorType: string (error class name, on failure only)
49+
*/
50+
ENVIRONMENT_DISCOVERY = 'ENVIRONMENT_DISCOVERY',
4351
}
4452

4553
// Map all events to their properties
@@ -142,12 +150,17 @@ export interface IEventNamePropertyMapping {
142150
/* __GDPR__
143151
"package_management": {
144152
"managerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
145-
"result": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
153+
"result": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
154+
"errorType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
155+
"triggerSource": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
156+
"<duration>": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" }
146157
}
147158
*/
148159
[EventNames.PACKAGE_MANAGEMENT]: {
149160
managerId: string;
150161
result: 'success' | 'error' | 'cancelled';
162+
errorType?: string;
163+
triggerSource: 'ui' | 'requirements' | 'package' | 'uninstall';
151164
};
152165

153166
/* __GDPR__
@@ -196,5 +209,18 @@ export interface IEventNamePropertyMapping {
196209
*/
197210
[EventNames.ENVIRONMENT_TOOL_USAGE]: {
198211
toolName: string;
212+
"environment_discovery": {
213+
"managerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
214+
"result": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
215+
"envCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" },
216+
"errorType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
217+
"<duration>": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" }
218+
}
219+
*/
220+
[EventNames.ENVIRONMENT_DISCOVERY]: {
221+
managerId: string;
222+
result: 'success' | 'error' | 'timeout';
223+
envCount?: number;
224+
errorType?: string;
199225
};
200226
}

src/internal.api.ts

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
SetEnvironmentScope,
2929
} from './api';
3030
import { CreateEnvironmentNotSupported, RemoveEnvironmentNotSupported } from './common/errors/NotSupportedError';
31+
import { StopWatch } from './common/stopWatch';
3132
import { EventNames } from './common/telemetry/constants';
3233
import { sendTelemetryEvent } from './common/telemetry/sender';
3334

@@ -201,8 +202,30 @@ export class InternalEnvironmentManager implements EnvironmentManager {
201202
: Promise.reject(new RemoveEnvironmentNotSupported(`Remove Environment not supported by: ${this.id}`));
202203
}
203204

204-
refresh(options: RefreshEnvironmentsScope): Promise<void> {
205-
return this.manager.refresh(options);
205+
async refresh(options: RefreshEnvironmentsScope): Promise<void> {
206+
const sw = new StopWatch();
207+
try {
208+
await this.manager.refresh(options);
209+
const envs = await this.manager.getEnvironments('all').catch(() => []);
210+
sendTelemetryEvent(EventNames.ENVIRONMENT_DISCOVERY, sw.elapsedTime, {
211+
managerId: this.id,
212+
result: 'success',
213+
envCount: envs.length,
214+
});
215+
} catch (ex) {
216+
const isTimeout = ex instanceof Error && ex.message.includes('timed out');
217+
sendTelemetryEvent(
218+
EventNames.ENVIRONMENT_DISCOVERY,
219+
sw.elapsedTime,
220+
{
221+
managerId: this.id,
222+
result: isTimeout ? 'timeout' : 'error',
223+
errorType: ex instanceof Error ? ex.name : 'unknown',
224+
},
225+
ex instanceof Error ? ex : undefined,
226+
);
227+
throw ex;
228+
}
206229
}
207230

208231
getEnvironments(options: GetEnvironmentsScope): Promise<PythonEnvironment[]> {
@@ -245,6 +268,23 @@ export class InternalEnvironmentManager implements EnvironmentManager {
245268
}
246269
}
247270

271+
function inferPackageManagementTrigger(
272+
options: PackageManagementOptions,
273+
): 'ui' | 'requirements' | 'package' | 'uninstall' {
274+
const hasInstall = options.install && options.install.length > 0;
275+
const hasUninstall = options.uninstall && options.uninstall.length > 0;
276+
if (!hasInstall && hasUninstall) {
277+
return 'uninstall';
278+
}
279+
if (!hasInstall) {
280+
return 'ui'; // empty install list opens the package picker UI
281+
}
282+
if (options.install?.some((arg) => arg === '-r' || arg === '--requirement')) {
283+
return 'requirements';
284+
}
285+
return 'package';
286+
}
287+
248288
export class InternalPackageManager implements PackageManager {
249289
public constructor(
250290
public readonly id: string,
@@ -271,18 +311,30 @@ export class InternalPackageManager implements PackageManager {
271311
}
272312

273313
async manage(environment: PythonEnvironment, options: PackageManagementOptions): Promise<void> {
314+
const stopWatch = new StopWatch();
315+
const triggerSource = inferPackageManagementTrigger(options);
274316
try {
275317
await this.manager.manage(environment, options);
276-
sendTelemetryEvent(EventNames.PACKAGE_MANAGEMENT, undefined, { managerId: this.id, result: 'success' });
318+
sendTelemetryEvent(EventNames.PACKAGE_MANAGEMENT, stopWatch.elapsedTime, {
319+
managerId: this.id,
320+
result: 'success',
321+
triggerSource,
322+
});
277323
} catch (error) {
278324
if (error instanceof CancellationError) {
279-
sendTelemetryEvent(EventNames.PACKAGE_MANAGEMENT, undefined, {
325+
sendTelemetryEvent(EventNames.PACKAGE_MANAGEMENT, stopWatch.elapsedTime, {
280326
managerId: this.id,
281327
result: 'cancelled',
328+
triggerSource,
282329
});
283330
throw error;
284331
}
285-
sendTelemetryEvent(EventNames.PACKAGE_MANAGEMENT, undefined, { managerId: this.id, result: 'error' });
332+
sendTelemetryEvent(EventNames.PACKAGE_MANAGEMENT, stopWatch.elapsedTime, {
333+
managerId: this.id,
334+
result: 'error',
335+
errorType: error instanceof Error ? error.name : 'unknown',
336+
triggerSource,
337+
});
286338
throw error;
287339
}
288340
}

src/managers/builtin/helpers.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,15 @@ export async function runUV(
6464
cwd?: string,
6565
log?: LogOutputChannel,
6666
token?: CancellationToken,
67+
timeout?: number,
6768
): Promise<string> {
6869
log?.info(`Running: uv ${args.join(' ')}`);
6970
return new Promise<string>((resolve, reject) => {
70-
const proc = spawnProcess('uv', args, { cwd: cwd });
71+
const spawnOptions: { cwd?: string; timeout?: number } = { cwd };
72+
if (timeout !== undefined) {
73+
spawnOptions.timeout = timeout;
74+
}
75+
const proc = spawnProcess('uv', args, spawnOptions);
7176
token?.onCancellationRequested(() => {
7277
proc.kill();
7378
reject(new CancellationError());
@@ -104,10 +109,11 @@ export async function runPython(
104109
cwd?: string,
105110
log?: LogOutputChannel,
106111
token?: CancellationToken,
112+
timeout?: number,
107113
): Promise<string> {
108114
log?.info(`Running: ${python} ${args.join(' ')}`);
109115
return new Promise<string>((resolve, reject) => {
110-
const proc = spawnProcess(python, args, { cwd: cwd });
116+
const proc = spawnProcess(python, args, { cwd: cwd, timeout });
111117
token?.onCancellationRequested(() => {
112118
proc.kill();
113119
reject(new CancellationError());

src/managers/builtin/utils.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,29 @@ export async function refreshPythons(
139139
return sortEnvironments(collection);
140140
}
141141

142+
const PIP_LIST_TIMEOUT_MS = 30_000;
143+
142144
async function refreshPipPackagesRaw(environment: PythonEnvironment, log?: LogOutputChannel): Promise<string> {
143145
// Use environmentPath directly for consistency with UV environment tracking
144146
const useUv = await shouldUseUv(log, environment.environmentPath.fsPath);
145147
if (useUv) {
146-
return await runUV(['pip', 'list', '--python', environment.execInfo.run.executable], undefined, log);
148+
return await runUV(
149+
['pip', 'list', '--python', environment.execInfo.run.executable],
150+
undefined,
151+
log,
152+
undefined,
153+
PIP_LIST_TIMEOUT_MS,
154+
);
147155
}
148156
try {
149-
return await runPython(environment.execInfo.run.executable, ['-m', 'pip', 'list'], undefined, log);
157+
return await runPython(
158+
environment.execInfo.run.executable,
159+
['-m', 'pip', 'list'],
160+
undefined,
161+
log,
162+
undefined,
163+
PIP_LIST_TIMEOUT_MS,
164+
);
150165
} catch (ex) {
151166
log?.error('Error running pip list', ex);
152167
log?.info(

0 commit comments

Comments
 (0)