Skip to content

Commit 5cc5162

Browse files
eleanorjboydCopilot
andcommitted
feat: add triggerReason to pet.process_restart and globalScopeDeferred to setup.hang_detected
- pet.process_restart now emits triggerReason (process_exit:<code>:<signal>, process_error, rpc_connection_error, rpc_refresh_timeout, rpc_resolve_timeout, rpc_configure_timeout, start_failed, unknown) so we can distinguish crash restarts from timeout restarts in Kusto - setup.hang_detected now emits globalScopeDeferred (true/false/undefined) so we can tell whether a watchdog trip was a real foreground hang or a background global-scope scan running past 120s after the user already had an env selected - Updated GDPR declarations and TypeScript types in constants.ts for both events - Thread globalScopeDeferredRef through applyInitialEnvironmentSelection so the hang watchdog can read the deferred flag before envSelection returns Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7c06c11 commit 5cc5162

4 files changed

Lines changed: 45 additions & 4 deletions

File tree

src/common/telemetry/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,12 +391,15 @@ export interface IEventNamePropertyMapping {
391391
/* __GDPR__
392392
"setup.hang_detected": {
393393
"failureStage": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" },
394+
"globalScopeDeferred": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
394395
"<duration>": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" },
395396
"<stageDuration>": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" }
396397
}
397398
*/
398399
[EventNames.SETUP_HANG_DETECTED]: {
399400
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;
400403
};
401404

402405
/* __GDPR__
@@ -549,13 +552,16 @@ export interface IEventNamePropertyMapping {
549552
"attempt": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" },
550553
"result": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
551554
"errorType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
555+
"triggerReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
552556
"<duration>": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" }
553557
}
554558
*/
555559
[EventNames.PET_PROCESS_RESTART]: {
556560
attempt: number;
557561
result: 'success' | 'error';
558562
errorType?: string;
563+
/** Why the PET process needed restarting: process_exit:<code>:<signal>, process_error, rpc_connection_error, rpc_refresh_timeout, rpc_resolve_timeout, rpc_configure_timeout, start_failed, or unknown */
564+
triggerReason: string;
559565
};
560566

561567
/* __GDPR__

src/extension.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,9 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
553553
setImmediate(async () => {
554554
let failureStage = 'nativeFinder';
555555
const stageWatch = new StopWatch();
556+
// Mutable ref so the hang watchdog can report whether global scope was deferred
557+
// even if it fires mid-envSelection before applyInitialEnvironmentSelection returns.
558+
const globalScopeDeferredRef: { value: boolean | undefined } = { value: undefined };
556559
// Watchdog: fires if setup hasn't completed within 120s, indicating a likely hang
557560
const SETUP_HANG_TIMEOUT_MS = 120_000;
558561
let hangWatchdogActive = true;
@@ -572,7 +575,7 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
572575
sendTelemetryEvent(
573576
EventNames.SETUP_HANG_DETECTED,
574577
{ duration: start.elapsedTime, stageDuration: stageWatch.elapsedTime },
575-
{ failureStage },
578+
{ failureStage, globalScopeDeferred: globalScopeDeferredRef.value },
576579
);
577580
}, SETUP_HANG_TIMEOUT_MS);
578581
context.subscriptions.push({ dispose: clearHangWatchdog });
@@ -618,7 +621,14 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
618621

619622
failureStage = 'envSelection';
620623
stageWatch.reset();
621-
await applyInitialEnvironmentSelection(envManagers, projectManager, nativeFinder, api, start.elapsedTime);
624+
await applyInitialEnvironmentSelection(
625+
envManagers,
626+
projectManager,
627+
nativeFinder,
628+
api,
629+
start.elapsedTime,
630+
globalScopeDeferredRef,
631+
);
622632

623633
// Register manager-agnostic terminal watcher for package-modifying commands
624634
failureStage = 'terminalWatcher';

src/features/interpreterSelection.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ export async function applyInitialEnvironmentSelection(
284284
nativeFinder: NativePythonFinder,
285285
api: PythonEnvironmentApi,
286286
activationToReadyDurationMs?: number,
287+
globalScopeDeferredRef?: { value: boolean | undefined },
287288
): Promise<void> {
288289
const folders = getWorkspaceFolders() ?? [];
289290
traceInfo(
@@ -377,6 +378,9 @@ export async function applyInitialEnvironmentSelection(
377378
if (workspaceFolderResolved) {
378379
// Defer global scope so it doesn't block post-selection startup.
379380
traceInfo('[interpreterSelection] Workspace env resolved, deferring global scope to background');
381+
if (globalScopeDeferredRef) {
382+
globalScopeDeferredRef.value = true;
383+
}
380384
resolveGlobalScope()
381385
.then(async (globalErrors) => {
382386
if (globalErrors.length > 0) {
@@ -386,6 +390,9 @@ export async function applyInitialEnvironmentSelection(
386390
.catch((err) => traceError(`[interpreterSelection] Background global scope resolution failed: ${err}`));
387391
} else {
388392
// No workspace folder resolved — global scope is the primary fallback, must await.
393+
if (globalScopeDeferredRef) {
394+
globalScopeDeferredRef.value = false;
395+
}
389396
const globalErrors = await resolveGlobalScope();
390397
allErrors.push(...globalErrors);
391398
}

src/managers/common/nativePythonFinder.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ class NativePythonFinderImpl implements NativePythonFinder {
237237
private startFailed: boolean = false;
238238
private restartAttempts: number = 0;
239239
private isRestarting: boolean = false;
240+
private processExitReason: string | undefined = undefined;
240241
private readonly configureRetry = new ConfigureRetryState();
241242

242243
constructor(
@@ -279,6 +280,8 @@ class NativePythonFinderImpl implements NativePythonFinder {
279280
this.outputChannel.warn(`[pet] Resolve request ${reason}, killing process for restart`);
280281
this.killProcess();
281282
this.processExited = true;
283+
this.processExitReason =
284+
ex instanceof rpc.ConnectionError ? 'rpc_connection_error' : 'rpc_resolve_timeout';
282285
}
283286
throw ex;
284287
}
@@ -339,6 +342,7 @@ class NativePythonFinderImpl implements NativePythonFinder {
339342
this.isRestarting = true;
340343
this.restartAttempts++;
341344
const attempt = this.restartAttempts;
345+
const triggerReason = this.processExitReason ?? (this.startFailed ? 'start_failed' : 'unknown');
342346

343347
const backoffMs = RESTART_BACKOFF_BASE_MS * Math.pow(2, this.restartAttempts - 1);
344348
this.outputChannel.warn(
@@ -361,22 +365,27 @@ class NativePythonFinderImpl implements NativePythonFinder {
361365
// Reset state flags
362366
this.processExited = false;
363367
this.startFailed = false;
368+
this.processExitReason = undefined;
364369
this.lastConfiguration = undefined; // Force reconfiguration
365370
this.configureRetry.reset();
366371

367372
// Start fresh
368373
this.connection = this.start();
369374

370375
this.outputChannel.info('[pet] Python Environment Tools restarted successfully');
371-
sendTelemetryEvent(EventNames.PET_PROCESS_RESTART, sw.elapsedTime, { attempt, result: 'success' });
376+
sendTelemetryEvent(EventNames.PET_PROCESS_RESTART, sw.elapsedTime, {
377+
attempt,
378+
result: 'success',
379+
triggerReason,
380+
});
372381

373382
// Reset restart attempts on successful start (process didn't immediately fail)
374383
// We'll reset this only after a successful request completes
375384
} catch (ex) {
376385
sendTelemetryEvent(
377386
EventNames.PET_PROCESS_RESTART,
378387
sw.elapsedTime,
379-
{ attempt, result: 'error', errorType: classifyError(ex) },
388+
{ attempt, result: 'error', errorType: classifyError(ex), triggerReason },
380389
ex instanceof Error ? ex : undefined,
381390
);
382391
this.outputChannel.error('[pet] Failed to restart Python Environment Tools:', ex);
@@ -520,15 +529,19 @@ class NativePythonFinderImpl implements NativePythonFinder {
520529
this.proc.on('exit', (code, signal) => {
521530
this.processExited = true;
522531
if (code !== 0) {
532+
this.processExitReason = `process_exit:${code ?? 'null'}:${signal ?? 'none'}`;
523533
this.outputChannel.error(
524534
`[pet] Python Environment Tools exited unexpectedly with code ${code}, signal ${signal}`,
525535
);
536+
} else {
537+
this.processExitReason = 'process_exit:0';
526538
}
527539
});
528540

529541
// Handle process errors (e.g., ENOENT if executable not found)
530542
this.proc.on('error', (err) => {
531543
this.processExited = true;
544+
this.processExitReason = 'process_error';
532545
this.outputChannel.error('[pet] Process error:', err);
533546
});
534547

@@ -626,6 +639,8 @@ class NativePythonFinderImpl implements NativePythonFinder {
626639
// Kill and restart for retry
627640
this.killProcess();
628641
this.processExited = true;
642+
this.processExitReason =
643+
ex instanceof rpc.ConnectionError ? 'rpc_connection_error' : 'rpc_refresh_timeout';
629644
continue;
630645
}
631646
// Final attempt failed
@@ -737,6 +752,8 @@ class NativePythonFinderImpl implements NativePythonFinder {
737752
this.outputChannel.warn(`[pet] PET process ${reason}, killing for restart`);
738753
this.killProcess();
739754
this.processExited = true;
755+
this.processExitReason =
756+
ex instanceof rpc.ConnectionError ? 'rpc_connection_error' : 'rpc_refresh_timeout';
740757
}
741758
this.outputChannel.error('[pet] Error refreshing', ex);
742759
throw ex;
@@ -806,6 +823,7 @@ class NativePythonFinderImpl implements NativePythonFinder {
806823
);
807824
this.killProcess();
808825
this.processExited = true;
826+
this.processExitReason = 'rpc_configure_timeout';
809827
} else {
810828
this.outputChannel.warn(
811829
`[pet] Configure request timed out (attempt ${this.configureRetry.timeoutCount}/${MAX_CONFIGURE_TIMEOUTS_BEFORE_KILL}), ` +

0 commit comments

Comments
 (0)