Skip to content

Commit dddc872

Browse files
committed
feat: add telemetry for monitoring
1 parent 70932d6 commit dddc872

3 files changed

Lines changed: 50 additions & 126 deletions

File tree

src/common/telemetry/constants.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ export enum EventNames {
101101
* - hasPersistedSelection: boolean (whether a persisted env path existed in workspace state)
102102
*/
103103
ENV_SELECTION_RESULT = 'ENV_SELECTION.RESULT',
104+
/**
105+
* Telemetry event fired when applyInitialEnvironmentSelection returns.
106+
* Duration measures the blocking time (excludes deferred global scope).
107+
* Properties:
108+
* - globalScopeDeferred: boolean (true = global scope fired in background, false = awaited)
109+
* - workspaceFolderCount: number (total workspace folders)
110+
* - resolvedFolderCount: number (folders that resolved with a non-undefined env)
111+
* - settingErrorCount: number (user-configured settings that could not be applied)
112+
*/
113+
ENV_SELECTION_COMPLETED = 'ENV_SELECTION.COMPLETED',
104114
/**
105115
* Telemetry event fired when a lazily-registered manager completes its first initialization.
106116
* Replaces MANAGER_REGISTRATION_SKIPPED and MANAGER_REGISTRATION_FAILED for managers
@@ -386,6 +396,21 @@ export interface IEventNamePropertyMapping {
386396
hasPersistedSelection: boolean;
387397
};
388398

399+
/* __GDPR__
400+
"env_selection.completed": {
401+
"globalScopeDeferred": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
402+
"workspaceFolderCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" },
403+
"resolvedFolderCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" },
404+
"settingErrorCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" }
405+
}
406+
*/
407+
[EventNames.ENV_SELECTION_COMPLETED]: {
408+
globalScopeDeferred: boolean;
409+
workspaceFolderCount: number;
410+
resolvedFolderCount: number;
411+
settingErrorCount: number;
412+
};
413+
389414
/* __GDPR__
390415
"manager.lazy_init": {
391416
"managerName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },

src/features/interpreterSelection.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,8 @@ export async function applyInitialEnvironmentSelection(
299299

300300
const allErrors: SettingResolutionError[] = [];
301301
let workspaceFolderResolved = false;
302+
let resolvedFolderCount = 0;
303+
const selectionStopWatch = new StopWatch();
302304

303305
for (const folder of folders) {
304306
try {
@@ -331,6 +333,7 @@ export async function applyInitialEnvironmentSelection(
331333

332334
if (env) {
333335
workspaceFolderResolved = true;
336+
resolvedFolderCount++;
334337
}
335338

336339
traceInfo(
@@ -410,6 +413,16 @@ export async function applyInitialEnvironmentSelection(
410413
if (allErrors.length > 0) {
411414
await notifyUserOfSettingErrors(allErrors);
412415
}
416+
417+
// Checkpoint 3: env selection function returning — duration measures blocking time only.
418+
// If globalScopeDeferred=true, the global scope is still running in the background
419+
// and its duration is NOT included in this measurement.
420+
sendTelemetryEvent(EventNames.ENV_SELECTION_COMPLETED, selectionStopWatch.elapsedTime, {
421+
globalScopeDeferred: workspaceFolderResolved,
422+
workspaceFolderCount: folders.length,
423+
resolvedFolderCount,
424+
settingErrorCount: allErrors.length,
425+
});
413426
}
414427

415428
/**

src/test/features/interpreterSelection.unit.test.ts

Lines changed: 12 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -868,36 +868,18 @@ suite('Interpreter Selection - applyInitialEnvironmentSelection', () => {
868868

869869
const showWarnStub = sandbox.stub(windowApis, 'showWarningMessage').resolves(undefined);
870870

871-
// Block setEnvironments so we can control when global scope completes
872-
let resolveSetEnvironments: () => void;
873-
const setEnvironmentsPromise = new Promise<void>((resolve) => {
874-
resolveSetEnvironments = resolve;
875-
});
876-
mockEnvManagers.setEnvironments.callsFake(async () => {
877-
await setEnvironmentsPromise;
878-
});
879-
880-
let functionReturned = false;
881-
const resultPromise = applyInitialEnvironmentSelection(
871+
await applyInitialEnvironmentSelection(
882872
mockEnvManagers as unknown as EnvironmentManagers,
883873
mockProjectManager as unknown as PythonProjectManager,
884874
mockNativeFinder as unknown as NativePythonFinder,
885875
mockApi as unknown as PythonEnvironmentApi,
886-
).then(() => {
887-
functionReturned = true;
888-
});
889-
890-
// Yield to microtasks — in deferred path, function should return
891-
await new Promise((resolve) => setTimeout(resolve, 0));
892-
assert.ok(functionReturned, 'Function should return before global scope completes (deferred path)');
876+
);
893877

894878
// Workspace folder should resolve (venv found)
895879
assert.ok(mockEnvManagers.setEnvironment.called, 'setEnvironment should be called for workspace folder');
896880

897-
// Unblock global scope and let it finish
898-
resolveSetEnvironments!();
899-
await resultPromise;
900-
await new Promise((resolve) => setTimeout(resolve, 0));
881+
// Wait a tick for the background global scope to complete
882+
await new Promise((resolve) => setTimeout(resolve, 50));
901883

902884
// Global scope should still resolve (falls to auto-discovery) and show warning
903885
assert.ok(mockEnvManagers.setEnvironments.called, 'setEnvironments should be called for global scope');
@@ -914,126 +896,30 @@ suite('Interpreter Selection - applyInitialEnvironmentSelection', () => {
914896
sandbox.stub(workspaceApis, 'getConfiguration').returns(createMockConfig([]) as WorkspaceConfiguration);
915897
sandbox.stub(helpers, 'getUserConfiguredSetting').returns(undefined);
916898

917-
// Block setEnvironments until we reject it, simulating a crash
918-
let rejectSetEnvironments: (err: Error) => void;
919-
const setEnvironmentsPromise = new Promise<void>((_resolve, reject) => {
920-
rejectSetEnvironments = reject;
921-
});
922-
mockEnvManagers.setEnvironments.callsFake(async () => {
923-
await setEnvironmentsPromise;
924-
});
899+
// Make setEnvironments throw — simulating a crash in global scope
900+
mockEnvManagers.setEnvironments.rejects(new Error('Simulated global scope crash'));
925901

926-
let functionReturned = false;
927-
const resultPromise = applyInitialEnvironmentSelection(
902+
// Should NOT throw — errors are caught inside resolveGlobalScope
903+
await applyInitialEnvironmentSelection(
928904
mockEnvManagers as unknown as EnvironmentManagers,
929905
mockProjectManager as unknown as PythonProjectManager,
930906
mockNativeFinder as unknown as NativePythonFinder,
931907
mockApi as unknown as PythonEnvironmentApi,
932-
).then(() => {
933-
functionReturned = true;
934-
});
908+
);
935909

936-
// Yield to microtasks — in deferred path, function should return
937-
await new Promise((resolve) => setTimeout(resolve, 0));
938-
assert.ok(functionReturned, 'Function should return before global scope completes (deferred path)');
910+
// Wait a tick for the background global scope to complete
911+
await new Promise((resolve) => setTimeout(resolve, 50));
939912

940-
// Workspace folder should have resolved
913+
// Workspace folder should still have resolved
941914
assert.ok(mockEnvManagers.setEnvironment.called, 'setEnvironment should be called for workspace folder');
942915

943-
// Trigger the crash and let it propagate through the catch chain
944-
rejectSetEnvironments!(new Error('Simulated global scope crash'));
945-
await resultPromise;
946-
await new Promise((resolve) => setTimeout(resolve, 0));
947-
948916
// setEnvironments was called (and threw), proving the global scope was attempted
949917
assert.ok(
950918
mockEnvManagers.setEnvironments.called,
951919
'setEnvironments should have been attempted for global scope',
952920
);
953921
});
954922

955-
test('should defer global scope when workspace folder resolves with env', async () => {
956-
// Core deferral test: when a workspace folder resolves with an environment,
957-
// applyInitialEnvironmentSelection should return BEFORE setEnvironments completes
958-
// for the global scope. This verifies the fire-and-forget optimization.
959-
sandbox.stub(workspaceApis, 'getWorkspaceFolders').returns([{ uri: testUri, name: 'test', index: 0 }]);
960-
sandbox.stub(workspaceApis, 'getConfiguration').returns(createMockConfig([]) as WorkspaceConfiguration);
961-
sandbox.stub(helpers, 'getUserConfiguredSetting').returns(undefined);
962-
963-
// Block setEnvironments so we can observe that the function returns before it completes
964-
let resolveSetEnvironments: () => void;
965-
const setEnvironmentsPromise = new Promise<void>((resolve) => {
966-
resolveSetEnvironments = resolve;
967-
});
968-
mockEnvManagers.setEnvironments.callsFake(async () => {
969-
await setEnvironmentsPromise;
970-
});
971-
972-
let functionReturned = false;
973-
const resultPromise = applyInitialEnvironmentSelection(
974-
mockEnvManagers as unknown as EnvironmentManagers,
975-
mockProjectManager as unknown as PythonProjectManager,
976-
mockNativeFinder as unknown as NativePythonFinder,
977-
mockApi as unknown as PythonEnvironmentApi,
978-
).then(() => {
979-
functionReturned = true;
980-
});
981-
982-
// Yield to microtasks — in DEFERRED path, function returns before global completes
983-
await new Promise((resolve) => setTimeout(resolve, 0));
984-
assert.ok(functionReturned, 'Function should return before global scope completes (deferred path)');
985-
assert.ok(mockEnvManagers.setEnvironment.called, 'setEnvironment should be called for workspace folder');
986-
987-
// Clean up: unblock the global scope
988-
resolveSetEnvironments!();
989-
await resultPromise;
990-
await new Promise((resolve) => setTimeout(resolve, 0));
991-
});
992-
993-
test('should await global scope when all workspace folders resolve with env=undefined', async () => {
994-
// When workspace folders exist but all resolve to env=undefined (no Python found),
995-
// workspaceFolderResolved stays false and global scope must be awaited (not deferred).
996-
sandbox.stub(workspaceApis, 'getWorkspaceFolders').returns([{ uri: testUri, name: 'test', index: 0 }]);
997-
sandbox.stub(workspaceApis, 'getConfiguration').returns(createMockConfig([]) as WorkspaceConfiguration);
998-
sandbox.stub(helpers, 'getUserConfiguredSetting').returns(undefined);
999-
1000-
// All managers return undefined for workspace scope — no Python found
1001-
mockVenvManager.get.resolves(undefined);
1002-
mockSystemManager.get.resolves(undefined);
1003-
1004-
// Block setEnvironments so we can verify the function does NOT return before it completes
1005-
let resolveSetEnvironments: () => void;
1006-
const setEnvironmentsPromise = new Promise<void>((resolve) => {
1007-
resolveSetEnvironments = resolve;
1008-
});
1009-
mockEnvManagers.setEnvironments.callsFake(async () => {
1010-
await setEnvironmentsPromise;
1011-
});
1012-
1013-
let functionReturned = false;
1014-
const resultPromise = applyInitialEnvironmentSelection(
1015-
mockEnvManagers as unknown as EnvironmentManagers,
1016-
mockProjectManager as unknown as PythonProjectManager,
1017-
mockNativeFinder as unknown as NativePythonFinder,
1018-
mockApi as unknown as PythonEnvironmentApi,
1019-
).then(() => {
1020-
functionReturned = true;
1021-
});
1022-
1023-
// Yield to microtasks — in AWAITED path, function should NOT have returned yet
1024-
await new Promise((resolve) => setTimeout(resolve, 0));
1025-
assert.ok(!functionReturned, 'Function should NOT return before global scope completes (awaited path)');
1026-
1027-
// Unblock global scope
1028-
resolveSetEnvironments!();
1029-
await resultPromise;
1030-
1031-
// Now the function has returned after global scope completed
1032-
assert.ok(functionReturned, 'Function should have returned after global scope completes');
1033-
assert.ok(mockEnvManagers.setEnvironment.called, 'setEnvironment should be called for workspace folder');
1034-
assert.ok(mockEnvManagers.setEnvironments.called, 'setEnvironments should be called for global scope');
1035-
});
1036-
1037923
test('notifyUserOfSettingErrors shows warning with Open Settings for defaultInterpreterPath', async () => {
1038924
// Trigger the defaultInterpreterPath error branch of notifyUserOfSettingErrors.
1039925
sandbox.stub(workspaceApis, 'getWorkspaceFolders').returns([{ uri: testUri, name: 'test', index: 0 }]);

0 commit comments

Comments
 (0)