Skip to content

Commit d5311d0

Browse files
committed
feat: enhance telemetry for global scope handling and process restart reasons
1 parent ca51c2d commit d5311d0

4 files changed

Lines changed: 118 additions & 13 deletions

File tree

src/common/telemetry/constants.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,15 @@ export enum EventNames {
8686
* - errorType: string (classified error category, on failure only)
8787
*/
8888
PET_INIT_DURATION = 'PET.INIT_DURATION',
89+
/**
90+
* Telemetry event fired once per activation reporting the version of the bundled
91+
* PET (Python Environment Tools) binary in use. Used to slice other PET telemetry
92+
* by binary version when investigating regressions/improvements.
93+
* Properties:
94+
* - version: string (e.g. '0.1.0' from `pet --version`; 'unknown' if the lookup failed)
95+
* - source: 'envs_extension' | 'python_extension' (which extension shipped the binary)
96+
*/
97+
PET_VERSION = 'PET.VERSION',
8998
/**
9099
* Telemetry event fired when applyInitialEnvironmentSelection begins.
91100
* Signals that all managers are registered and env selection is starting.
@@ -398,8 +407,14 @@ export interface IEventNamePropertyMapping {
398407
*/
399408
[EventNames.SETUP_HANG_DETECTED]: {
400409
failureStage: string;
401-
/** Whether the global Python scope search was deferred to background at time of hang. undefined = hang fired before env-selection stage. */
402-
globalScopeDeferred: boolean | undefined;
410+
/**
411+
* State of the global Python scope search at the time of hang:
412+
* - 'deferred': workspace env resolved and global scope was pushed to background.
413+
* - 'not_deferred': no workspace env resolved, global scope was awaited as primary fallback.
414+
* - 'unknown': hang fired before the env-selection stage reached the global-scope decision
415+
* (i.e. before/during env selection, prior to the workspace-resolution branch).
416+
*/
417+
globalScopeDeferred: 'deferred' | 'not_deferred' | 'unknown';
403418
};
404419

405420
/* __GDPR__
@@ -425,6 +440,19 @@ export interface IEventNamePropertyMapping {
425440
errorType?: string;
426441
};
427442

443+
/* __GDPR__
444+
"pet.version": {
445+
"version": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
446+
"source": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
447+
}
448+
*/
449+
[EventNames.PET_VERSION]: {
450+
/** Version string reported by `pet --version` (e.g. '0.1.0'), or 'unknown' if the lookup failed. */
451+
version: string;
452+
/** Which extension shipped the PET binary that's being used. */
453+
source: 'envs_extension' | 'python_extension';
454+
};
455+
428456
/* __GDPR__
429457
"env_selection.started": {
430458
"registeredManagerCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" },

src/extension.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,12 @@ import { collectEnvironmentInfo, getEnvManagerAndPackageManagerConfigLevels, run
9696
import { EnvironmentManagers, ProjectCreators, PythonProjectManager } from './internal.api';
9797
import { registerSystemPythonFeatures } from './managers/builtin/main';
9898
import { SysPythonManager } from './managers/builtin/sysPythonManager';
99-
import { createNativePythonFinder, NativePythonFinder } from './managers/common/nativePythonFinder';
99+
import {
100+
createNativePythonFinder,
101+
getNativePythonToolsPathAndSource,
102+
getNativePythonToolsVersion,
103+
NativePythonFinder,
104+
} from './managers/common/nativePythonFinder';
100105
import { IDisposable } from './managers/common/types';
101106
import { registerCondaFeatures } from './managers/conda/main';
102107
import { registerPipenvFeatures } from './managers/pipenv/main';
@@ -555,7 +560,7 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
555560
const stageWatch = new StopWatch();
556561
// Mutable ref so the hang watchdog can report whether global scope was deferred
557562
// even if it fires mid-envSelection before applyInitialEnvironmentSelection returns.
558-
const globalScopeDeferredRef: { value: boolean | undefined } = { value: undefined };
563+
const globalScopeDeferredRef: { value: 'deferred' | 'not_deferred' | 'unknown' } = { value: 'unknown' };
559564
// Watchdog: fires if setup hasn't completed within 120s, indicating a likely hang
560565
const SETUP_HANG_TIMEOUT_MS = 120_000;
561566
let hangWatchdogActive = true;
@@ -586,6 +591,19 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
586591
try {
587592
nativeFinder = await createNativePythonFinder(outputChannel, api, context);
588593
sendTelemetryEvent(EventNames.PET_INIT_DURATION, petStart.elapsedTime, { result: 'success' });
594+
// Fire-and-forget: report the bundled PET binary version so other PET telemetry
595+
// can be sliced by version. Don't block activation on this.
596+
void getNativePythonToolsPathAndSource()
597+
.then(async ({ toolPath, source }) => {
598+
const version = await getNativePythonToolsVersion(toolPath);
599+
sendTelemetryEvent(EventNames.PET_VERSION, undefined, { version, source });
600+
})
601+
.catch(() => {
602+
sendTelemetryEvent(EventNames.PET_VERSION, undefined, {
603+
version: 'unknown',
604+
source: 'python_extension',
605+
});
606+
});
589607
} catch (petError) {
590608
sendTelemetryEvent(
591609
EventNames.PET_INIT_DURATION,

src/features/interpreterSelection.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ export async function applyInitialEnvironmentSelection(
284284
nativeFinder: NativePythonFinder,
285285
api: PythonEnvironmentApi,
286286
activationToReadyDurationMs?: number,
287-
globalScopeDeferredRef?: { value: boolean | undefined },
287+
globalScopeDeferredRef?: { value: 'deferred' | 'not_deferred' | 'unknown' },
288288
): Promise<void> {
289289
const folders = getWorkspaceFolders() ?? [];
290290
traceInfo(
@@ -379,7 +379,7 @@ export async function applyInitialEnvironmentSelection(
379379
// Defer global scope so it doesn't block post-selection startup.
380380
traceInfo('[interpreterSelection] Workspace env resolved, deferring global scope to background');
381381
if (globalScopeDeferredRef) {
382-
globalScopeDeferredRef.value = true;
382+
globalScopeDeferredRef.value = 'deferred';
383383
}
384384
resolveGlobalScope()
385385
.then(async (globalErrors) => {
@@ -391,7 +391,7 @@ export async function applyInitialEnvironmentSelection(
391391
} else {
392392
// No workspace folder resolved — global scope is the primary fallback, must await.
393393
if (globalScopeDeferredRef) {
394-
globalScopeDeferredRef.value = false;
394+
globalScopeDeferredRef.value = 'not_deferred';
395395
}
396396
const globalErrors = await resolveGlobalScope();
397397
allErrors.push(...globalErrors);

src/managers/common/nativePythonFinder.ts

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,21 @@ export class ConfigureRetryState {
8585
}
8686
}
8787

88+
export type NativePythonToolsSource = 'envs_extension' | 'python_extension';
89+
8890
export async function getNativePythonToolsPath(): Promise<string> {
91+
return (await getNativePythonToolsPathAndSource()).toolPath;
92+
}
93+
94+
export async function getNativePythonToolsPathAndSource(): Promise<{
95+
toolPath: string;
96+
source: NativePythonToolsSource;
97+
}> {
8998
const envsExt = getExtension(ENVS_EXTENSION_ID);
9099
if (envsExt) {
91100
const petPath = path.join(envsExt.extensionPath, 'python-env-tools', 'bin', isWindows() ? 'pet.exe' : 'pet');
92101
if (await fs.pathExists(petPath)) {
93-
return petPath;
102+
return { toolPath: petPath, source: 'envs_extension' };
94103
}
95104
}
96105

@@ -99,7 +108,54 @@ export async function getNativePythonToolsPath(): Promise<string> {
99108
throw new Error('Python extension not found');
100109
}
101110

102-
return path.join(python.extensionPath, 'python-env-tools', 'bin', isWindows() ? 'pet.exe' : 'pet');
111+
return {
112+
toolPath: path.join(python.extensionPath, 'python-env-tools', 'bin', isWindows() ? 'pet.exe' : 'pet'),
113+
source: 'python_extension',
114+
};
115+
}
116+
117+
/**
118+
* Runs `pet --version` and returns the parsed version string (e.g. '0.1.0').
119+
* Returns 'unknown' if the command fails, times out, or the output can't be parsed.
120+
*/
121+
export async function getNativePythonToolsVersion(toolPath: string, timeoutMs: number = 5_000): Promise<string> {
122+
return new Promise<string>((resolve) => {
123+
let settled = false;
124+
const settle = (value: string) => {
125+
if (settled) {
126+
return;
127+
}
128+
settled = true;
129+
resolve(value);
130+
};
131+
try {
132+
const proc = spawnProcess(toolPath, ['--version'], { stdio: 'pipe' });
133+
let stdout = '';
134+
const timer = setTimeout(() => {
135+
try {
136+
proc.kill('SIGTERM');
137+
} catch {
138+
// ignore
139+
}
140+
settle('unknown');
141+
}, timeoutMs);
142+
proc.stdout?.on('data', (data: Buffer) => {
143+
stdout += data.toString();
144+
});
145+
proc.on('error', () => {
146+
clearTimeout(timer);
147+
settle('unknown');
148+
});
149+
proc.on('close', () => {
150+
clearTimeout(timer);
151+
// Output looks like "pet 0.1.0\n" — extract the version token.
152+
const match = stdout.match(/\b\d+\.\d+\.\d+\S*/);
153+
settle(match ? match[0] : 'unknown');
154+
});
155+
} catch {
156+
settle('unknown');
157+
}
158+
});
103159
}
104160

105161
export interface NativeEnvInfo {
@@ -545,20 +601,23 @@ class NativePythonFinderImpl implements NativePythonFinder {
545601
// Handle process exit - mark as exited so pending requests fail fast
546602
this.proc.on('exit', (code, signal) => {
547603
this.processExited = true;
548-
if (code !== 0) {
604+
// Preserve a more-specific reason (e.g. rpc_*) if one was already recorded before the kill.
605+
if (this.processExitReason === undefined) {
549606
this.processExitReason = `process_exit:${code ?? 'null'}:${signal ?? 'none'}`;
607+
}
608+
if (code !== 0) {
550609
this.outputChannel.error(
551610
`[pet] Python Environment Tools exited unexpectedly with code ${code}, signal ${signal}`,
552611
);
553-
} else {
554-
this.processExitReason = 'process_exit:0';
555612
}
556613
});
557614

558615
// Handle process errors (e.g., ENOENT if executable not found)
559616
this.proc.on('error', (err) => {
560617
this.processExited = true;
561-
this.processExitReason = 'process_error';
618+
if (this.processExitReason === undefined) {
619+
this.processExitReason = 'process_error';
620+
}
562621
this.outputChannel.error('[pet] Process error:', err);
563622
});
564623

0 commit comments

Comments
 (0)