Skip to content

Commit dbb10ec

Browse files
Fix PET timeout telemetry attribution (#1695)
## Summary Correct PET/setup telemetry so timeout investigations retain numeric context and reliable binary attribution. - send refresh, configure, and restart counts through telemetry measurements - classify PET JSON-RPC timeouts by method and preserve refresh failure context - retain partial locator timing on failed refreshes - retry transient PET `info` timeouts while retaining metadata only for an unchanged binary - derive Conda counts from the caller-owned enum identity - regression-test setup stage duration, timeout categories, refresh measures, retry bounds, and binary attribution ## Validation - `npm run lint` - `npm run compile-tests` - `npm run unittest` (1504 passing, 5 pending) - `npm run compile` Fixes microsoft/python-environment-tools#478 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent def0c4e commit dbb10ec

8 files changed

Lines changed: 452 additions & 129 deletions

File tree

‎src/common/telemetry/constants.ts‎

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -601,27 +601,10 @@ export interface IEventNamePropertyMapping {
601601
"<duration>": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" }
602602
}
603603
*/
604+
// Numeric fields declared in the GDPR block are sent through the measurements payload.
604605
[EventNames.PET_REFRESH]: {
605606
result: 'success' | 'timeout' | 'error';
606-
envCount?: number;
607-
/** Number of discovered environments whose kind is Conda. Lets us slice refresh duration by conda footprint. */
608-
condaEnvCount?: number;
609-
/** Number of discovered environment managers (conda/pyenv/poetry/etc.). */
610-
managerCount?: number;
611-
unresolvedCount?: number;
612-
workspaceDirCount?: number;
613-
searchPathCount?: number;
614-
attempt: number;
615607
errorType?: string;
616-
// breakdown* fields go through the measures payload (numeric); listed here for GDPR only.
617-
/** ms in the Locators phase. */
618-
breakdownLocators?: number;
619-
/** ms walking PATH env var entries (not a file path). */
620-
breakdownPathEnv?: number;
621-
/** ms scanning global virtual-env dirs. */
622-
breakdownGlobalVirtualEnvs?: number;
623-
/** ms scanning workspace dirs. */
624-
breakdownWorkspaces?: number;
625608
/** JSON-serialized Record<locatorName, ms>. Parse with parse_json() in Kusto. */
626609
locatorsJson?: string;
627610
/** PET crate version reported by the `info` RPC. 'unknown' if the call failed or the PET binary doesn't implement it. */
@@ -638,14 +621,14 @@ export interface IEventNamePropertyMapping {
638621
"workspaceDirCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" },
639622
"envDirCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" },
640623
"retryCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" },
624+
"errorType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
641625
"<duration>": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" }
642626
}
643627
*/
628+
// Numeric fields declared in the GDPR block are sent through the measurements payload.
644629
[EventNames.PET_CONFIGURE]: {
645630
result: 'success' | 'timeout' | 'error' | 'skipped';
646-
workspaceDirCount?: number;
647-
envDirCount?: number;
648-
retryCount: number;
631+
errorType?: string;
649632
};
650633

651634
/* __GDPR__
@@ -660,8 +643,8 @@ export interface IEventNamePropertyMapping {
660643
"<duration>": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" }
661644
}
662645
*/
646+
// `attempt` is declared in the GDPR block and sent through the measurements payload.
663647
[EventNames.PET_PROCESS_RESTART]: {
664-
attempt: number;
665648
result: 'success' | 'error';
666649
errorType?: string;
667650
/**

‎src/common/telemetry/errorClassifier.ts‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,20 @@ export type DiscoveryErrorType =
1313
| 'command_failed'
1414
| 'connection_error'
1515
| 'rpc_error'
16+
| 'rpc_timeout'
17+
| 'rpc_configure_timeout'
18+
| 'rpc_refresh_timeout'
19+
| 'rpc_resolve_timeout'
1620
| 'process_crash'
1721
| 'already_registered'
1822
| 'unknown';
1923

24+
/** Returns true for spawn and JSON-RPC timeout telemetry categories. */
25+
export function isTimeoutErrorType(errorType: DiscoveryErrorType): boolean {
26+
return errorType === 'spawn_timeout' || errorType === 'rpc_timeout' ||
27+
(errorType.startsWith('rpc_') && errorType.endsWith('_timeout'));
28+
}
29+
2030
/**
2131
* Classifies an error into a telemetry-safe category for the `errorType` property.
2232
* Does NOT include raw error messages — only the category.
@@ -27,7 +37,16 @@ export function classifyError(ex: unknown): DiscoveryErrorType {
2737
}
2838

2939
if (ex instanceof RpcTimeoutError) {
30-
return 'spawn_timeout';
40+
switch (ex.method) {
41+
case 'configure':
42+
return 'rpc_configure_timeout';
43+
case 'refresh':
44+
return 'rpc_refresh_timeout';
45+
case 'resolve':
46+
return 'rpc_resolve_timeout';
47+
default:
48+
return 'rpc_timeout';
49+
}
3150
}
3251

3352
// JSON-RPC connection errors (e.g., PET process died mid-request, connection closed/disposed)

‎src/internal.api.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { CreateEnvironmentNotSupported, RemoveEnvironmentNotSupported } from './
3434
import { traceWarn } from './common/logging';
3535
import { StopWatch } from './common/stopWatch';
3636
import { EventNames } from './common/telemetry/constants';
37-
import { classifyError } from './common/telemetry/errorClassifier';
37+
import { classifyError, isTimeoutErrorType } from './common/telemetry/errorClassifier';
3838
import { sendTelemetryEvent } from './common/telemetry/sender';
3939

4040
export type EnvironmentManagerScope = undefined | string | Uri | PythonEnvironment;
@@ -242,7 +242,7 @@ export class InternalEnvironmentManager implements EnvironmentManager {
242242
duration,
243243
{
244244
managerId: this.id,
245-
result: errorType === 'canceled' || errorType === 'spawn_timeout' ? 'timeout' : 'error',
245+
result: errorType === 'canceled' || isTimeoutErrorType(errorType) ? 'timeout' : 'error',
246246
errorType,
247247
},
248248
ex instanceof Error ? ex : undefined,

0 commit comments

Comments
 (0)