@@ -24,6 +24,7 @@ const CONFIGURE_TIMEOUT_MS = 30_000; // 30 seconds for configuration
2424const MAX_CONFIGURE_TIMEOUT_MS = 60_000 ; // Max configure timeout after retries (60s)
2525const REFRESH_TIMEOUT_MS = 30_000 ; // 30 seconds for full refresh (with 1 retry = 60s max)
2626const RESOLVE_TIMEOUT_MS = 30_000 ; // 30 seconds for single resolve
27+ const INFO_TIMEOUT_MS = 2_000 ; // `info` is a const lookup on PET; 2s is generous
2728
2829// CLI fallback timeout: generous budget since it's a full process spawn doing a full scan
2930const CLI_FALLBACK_TIMEOUT_MS = 120_000 ; // 2 minutes
@@ -265,6 +266,17 @@ interface PetTelemetryNotification {
265266 } ;
266267}
267268
269+ /**
270+ * Response shape of the PET `info` JSON-RPC request.
271+ * `buildId` / `commitSha` are populated only when the PET binary was built by CI
272+ * with the appropriate env vars set; local dev builds omit them.
273+ */
274+ interface NativePetInfo {
275+ petVersion : string ;
276+ buildId ?: string ;
277+ commitSha ?: string ;
278+ }
279+
268280/**
269281 * Error thrown when a JSON-RPC request times out.
270282 */
@@ -322,6 +334,13 @@ class NativePythonFinderImpl implements NativePythonFinder {
322334 private isRestarting : boolean = false ;
323335 private processExitReason : string | undefined = undefined ;
324336 private readonly configureRetry = new ConfigureRetryState ( ) ;
337+ /**
338+ * Cached PET `info` response for the current connection. Reset to undefined on every
339+ * `start()` and re-populated asynchronously by `kickoffInfoFetch()`. Telemetry callers
340+ * read this via `getPetInfoProperties()`; if the fetch hasn't finished yet (or the PET
341+ * binary is too old to implement `info`), telemetry reports 'unknown'.
342+ */
343+ private petInfo : NativePetInfo | undefined ;
325344
326345 constructor (
327346 private readonly outputChannel : LogOutputChannel ,
@@ -353,7 +372,10 @@ class NativePythonFinderImpl implements NativePythonFinder {
353372 this . outputChannel . info ( `Resolved Python Environment ${ environment . executable } ` ) ;
354373 // Reset restart attempts on successful request
355374 this . restartAttempts = 0 ;
356- sendTelemetryEvent ( EventNames . PET_RESOLVE , sw . elapsedTime , { result : 'success' } ) ;
375+ sendTelemetryEvent ( EventNames . PET_RESOLVE , sw . elapsedTime , {
376+ result : 'success' ,
377+ ...this . getPetInfoProperties ( ) ,
378+ } ) ;
357379 return environment ;
358380 } catch ( ex ) {
359381 // On resolve timeout or connection error (not configure — configure handles its own timeout),
@@ -376,6 +398,7 @@ class NativePythonFinderImpl implements NativePythonFinder {
376398 {
377399 result : errorType === 'spawn_timeout' ? 'timeout' : 'error' ,
378400 errorType,
401+ ...this . getPetInfoProperties ( ) ,
379402 } ,
380403 ex instanceof Error ? ex : undefined ,
381404 ) ;
@@ -460,6 +483,7 @@ class NativePythonFinderImpl implements NativePythonFinder {
460483 attempt,
461484 result : 'success' ,
462485 triggerReason,
486+ ...this . getPetInfoProperties ( ) ,
463487 } ) ;
464488
465489 // Reset restart attempts on successful start (process didn't immediately fail)
@@ -468,7 +492,13 @@ class NativePythonFinderImpl implements NativePythonFinder {
468492 sendTelemetryEvent (
469493 EventNames . PET_PROCESS_RESTART ,
470494 sw . elapsedTime ,
471- { attempt, result : 'error' , errorType : classifyError ( ex ) , triggerReason } ,
495+ {
496+ attempt,
497+ result : 'error' ,
498+ errorType : classifyError ( ex ) ,
499+ triggerReason,
500+ ...this . getPetInfoProperties ( ) ,
501+ } ,
472502 ex instanceof Error ? ex : undefined ,
473503 ) ;
474504 this . outputChannel . error ( '[pet] Failed to restart Python Environment Tools:' , ex ) ;
@@ -701,9 +731,54 @@ class NativePythonFinderImpl implements NativePythonFinder {
701731 ) ;
702732
703733 connection . listen ( ) ;
734+
735+ // Stamp PET telemetry with version/buildId/commitSha. Fire-and-forget — must not block refresh.
736+ this . petInfo = undefined ;
737+ this . kickoffInfoFetch ( connection ) ;
738+
704739 return connection ;
705740 }
706741
742+ /**
743+ * Asks the PET server for its build metadata (version + optional buildId + optional commitSha)
744+ * and caches it in `this.petInfo` for downstream telemetry. Runs once per `start()` call.
745+ *
746+ * Fire-and-forget by design — the response is not awaited so refresh/resolve callers are
747+ * never blocked. The 2 s timeout caps the worst case if PET is misbehaving. If a newer
748+ * connection has replaced `this.connection` by the time the response arrives, the response
749+ * is dropped to avoid clobbering the cache for the newer process.
750+ */
751+ private kickoffInfoFetch ( connection : rpc . MessageConnection ) : void {
752+ sendRequestWithTimeout < NativePetInfo > ( connection , 'info' , { } , INFO_TIMEOUT_MS )
753+ . then ( ( result ) => {
754+ if ( connection !== this . connection ) {
755+ return ;
756+ }
757+ this . petInfo = result ;
758+ this . outputChannel . debug ( '[pet] info:' , result ) ;
759+ } )
760+ . catch ( ( ex ) => {
761+ if ( connection !== this . connection ) {
762+ return ;
763+ }
764+ // Older PET binaries don't implement `info`; leave petInfo undefined so telemetry reports 'unknown'.
765+ this . outputChannel . debug ( '[pet] info request failed:' , ex ) ;
766+ } ) ;
767+ }
768+
769+ /**
770+ * Builds the petVersion/petBuildId/petCommitSha properties for PET telemetry events.
771+ * Always returns concrete strings (defaulting to 'unknown') so Kusto can group by them
772+ * without dealing with nulls.
773+ */
774+ private getPetInfoProperties ( ) : { petVersion : string ; petBuildId : string ; petCommitSha : string } {
775+ return {
776+ petVersion : this . petInfo ?. petVersion ?? 'unknown' ,
777+ petBuildId : this . petInfo ?. buildId ?? 'unknown' ,
778+ petCommitSha : this . petInfo ?. commitSha ?? 'unknown' ,
779+ } ;
780+ }
781+
707782 private async doRefresh ( options ?: NativePythonEnvironmentKind | Uri [ ] ) : Promise < NativeInfo [ ] > {
708783 let lastError : unknown ;
709784
@@ -840,6 +915,7 @@ class NativePythonFinderImpl implements NativePythonFinder {
840915 searchPathCount,
841916 attempt,
842917 locatorsJson : refreshPerf ? JSON . stringify ( refreshPerf . locators ) : undefined ,
918+ ...this . getPetInfoProperties ( ) ,
843919 } ,
844920 ) ;
845921 } catch ( ex ) {
@@ -853,6 +929,7 @@ class NativePythonFinderImpl implements NativePythonFinder {
853929 unresolvedCount,
854930 attempt,
855931 errorType,
932+ ...this . getPetInfoProperties ( ) ,
856933 } ,
857934 ex instanceof Error ? ex : undefined ,
858935 ) ;
0 commit comments