feat: include git commit SHA in PET info response for telemetry regression tracking#473
Conversation
| println!("cargo:rustc-env=PET_BUILD_ID={build_id}"); | ||
| } | ||
|
|
||
| // BUILD_SOURCEVERSION is set by Azure Pipelines; GITHUB_SHA by GitHub Actions. |
There was a problem hiding this comment.
Copilot generated:
The Skeptic identified (structurally confirmed) that an explicitly-set-but-empty PET_COMMIT_SHA silently suppresses the BUILD_SOURCEVERSION/GITHUB_SHA fallbacks: or_else only fires on None, so Some("") short-circuits the chain and the trailing .filter() then drops it to None, losing the SHA entirely. This is concretely triggerable (a GitHub Actions YAML referencing an unset context yields ""). While the Advocate notes this mirrors the existing build_id logic and may be unreachable in practice, the fix is cheap — filter each candidate before falling back: .ok().filter(|v| !v.is_empty()).or_else(|| std::env::var("BUILD_SOURCEVERSION").ok().filter(|v| !v.is_empty())).... Apply the same hardening to the build_id chain above.
[verified]
There was a problem hiding this comment.
Good catch — fixed in 356f4af. Moved the empty-string .filter() inside each candidate so a Some("") falls through to the next env var instead of being swallowed by the trailing filter. Applied the same hardening to the �uild_id chain.
There was a problem hiding this comment.
Good catch — fixed in 356f4af. Moved the empty-string .filter() inside each candidate so a Some(\"\") falls through to the next env var instead of being swallowed by the trailing filter. Applied the same hardening to the build_id chain.
| println!("cargo:rustc-env=PET_BUILD_ID={build_id}"); | ||
| } | ||
|
|
||
| // BUILD_SOURCEVERSION is set by Azure Pipelines; GITHUB_SHA by GitHub Actions. |
There was a problem hiding this comment.
Copilot generated:
Operational caveat from the Skeptic (Low): Azure Pipelines' BUILD_SOURCEVERSION is the ephemeral merge commit for PR-triggered builds, not the PR head commit. Since the stated goal is correlating regressions to a specific PET commit, verify the CI trigger config resolves to the intended (head) commit — this is a config check, not a code defect.
[verified]
There was a problem hiding this comment.
Acknowledged — treating this as a config check rather than a code change. Shipped PET binaries are built from main / release pipeline runs (not PR-triggered validation), where BUILD_SOURCEVERSION resolves to the real source commit. If we ever change the release pipeline to build from a PR context, I'll set PET_COMMIT_SHA explicitly to the head SHA so this stays correct.
There was a problem hiding this comment.
Acknowledged — treating this as a config check rather than a code change. Shipped PET binaries are built from main / release pipeline runs (not PR-triggered validation), where BUILD_SOURCEVERSION resolves to the real source commit. If we ever change the release pipeline to build from a PR context, I'll set PET_COMMIT_SHA explicitly to the head SHA so this stays correct.
| let info = InfoResponse::current(); | ||
|
|
||
| assert_eq!(info.pet_version, env!("CARGO_PKG_VERSION")); | ||
| // build_id / commit_sha are populated from env vars set at compile time by CI. |
There was a problem hiding this comment.
Copilot generated:
The Skeptic, Advocate, and Architect all noted these assertions are vacuously true: commit_sha.as_deref().is_none_or(|s| !s.is_empty()) passes whenever commit_sha is None, which is every local dev run — and build.rs's own .filter() already guarantees it can never be Some(""). As a result no test positively exercises the build.rs → binary → JSON path the PR claims, so a regression (the empty-string short-circuit, a serde-rename typo, a dropped option_env!) would ship green. Consider a test that, when option_env!("PET_COMMIT_SHA") is Some, asserts the serialized JSON contains a non-empty commitSha key.
[verified]
There was a problem hiding this comment.
Good point. Added test_info_response_serializes_camel_case_and_omits_none in 356f4af that constructs InfoResponse directly and asserts the JSON wire format — both the camelCase rename and the skip_serializing_if behavior. That catches serde-rename typos, dropped option_env!, or accidentally serializing None without requiring CI env vars at compile time. Kept the existing assertion (now renamed to ..._optional_build_metadata) as a light sanity check on the option_env! path; the new test provides the real wire-format protection.
|
GitHub cannot anchor PR review comments to unchanged lines in the diff. Falling back to a general PR comment for crates/pet/src/jsonrpc.rs:L1545. Copilot generated: [verified] |
| let info = client.info().expect("info request failed"); | ||
|
|
||
| assert_eq!(info.pet_version, env!("CARGO_PKG_VERSION")); | ||
| // build_id / commit_sha are populated from env vars set at compile time by CI. |
There was a problem hiding this comment.
Copilot generated:
Same vacuous-assertion concern as the unit test: commit_sha/build_id are None in local and most CI runs, so this assertion can essentially never fail and provides little real coverage of the end-to-end wire format. A round-trip test that sets the env var and asserts the commitSha JSON key would give genuine protection.
[verified]
There was a problem hiding this comment.
Same fix path as the unit-test thread: the new test_info_response_serializes_camel_case_and_omits_none in 356f4af covers the wire format at the serialization layer, which is where a serde-rename / skip_serializing_if regression would actually break. Setting compile-time env vars from an integration test would require rebuilding the binary, so I kept this assertion (renamed to info_reports_pet_version_and_optional_build_metadata) as a smoke check that deserialization still works when a SHA is present in a CI build.
brettcannon
left a comment
There was a problem hiding this comment.
The Copilot comments look reasonable.
…re-format test, rename tests - build.rs: push empty-string filter into each candidate of build_id/commit_sha chains so an explicitly-set-but-empty primary var (e.g. PET_COMMIT_SHA from an unset GitHub Actions context) doesn't short-circuit fallbacks. - jsonrpc.rs: add test_info_response_serializes_camel_case_and_omits_none covering camelCase rename + skip_serializing_if without requiring CI env vars at compile time. - rename existing tests to ..._optional_build_metadata for accurate scope.
|
Addressed Copilot feedback in 356f4af:
|
## Context PET (Python Environment Tools) is the Rust JSON-RPC service this extension uses to discover Python environments. PET ships independently of the extension and is bundled as a binary inside the VSIX, so the PET version a user is running can drift from the extension version. When we see a performance regression or a failure in PET telemetry today, we have no way to map it back to the **exact PET source code** the user is running — we know `pet --version` (already telemetered via `PET.VERSION`) but a single PET version line can correspond to many commits across multiple builds. This PR closes that gap by stamping every PET telemetry event with the binary's `petVersion`, `petBuildId`, and `petCommitSha`, sourced directly from the running PET process via a new `info` JSON-RPC request. We can then `summarize by petCommitSha` in Kusto and join straight to git log to find the offending change. ## Related PRs PET side (both merged): - microsoft/python-environment-tools#470 — adds the `info` JSON-RPC request returning `petVersion` and optional `buildId` - microsoft/python-environment-tools#473 — extends the `info` response with optional `commitSha` baked in from CI env vars (`PET_COMMIT_SHA` / `BUILD_SOURCEVERSION` / `GITHUB_SHA`) Supersedes / replaces: - #1548 — earlier draft that only added `petVersion` + `petBuildId` and accidentally corrupted the enum docstring for `PET_RESOLVE` / `MIGRATION_SYSTEM_ENV_MANAGER`. Please close. ## What this PR does 1. **Defines a typed `NativePetInfo` interface** matching PET's `info` response shape (`petVersion: string`, `buildId?: string`, `commitSha?: string`). 2. **Sends one `info` RPC per PET process start** in `kickoffInfoFetch(connection)`, called immediately after `connection.listen()` inside `start()`. The call is **fire-and-forget** with a 2 s timeout — `start()` does not await it, so discovery is never blocked. The response is cached in `this.petInfo` for the lifetime of that PET process. `this.petInfo` is reset to `undefined` on every `start()` (initial spawn + every crash-recovery restart). 3. **Guards against stale responses** via `connection !== this.connection` checks in both `.then` and `.catch`, so a late reply from a previous PET process can't clobber the cache of a newer one after a restart. 4. **Spreads `getPetInfoProperties()` into the six existing PET telemetry call sites** (success + error paths of `PET_RESOLVE`, `PET_REFRESH`, `PET_PROCESS_RESTART`). The helper always returns concrete strings, defaulting each field to `'unknown'`, so Kusto group-bys work without null handling. 5. **Adds GDPR comments + TypeScript types** for the three new fields on `PET_RESOLVE`, `PET_REFRESH`, `PET_PROCESS_RESTART`. All classified as `SystemMetaData` / `PerformanceAndHealth`. ## Files changed | File | Why | |---|---| | `src/managers/common/nativePythonFinder.ts` | `INFO_TIMEOUT_MS` constant, `NativePetInfo` interface, `petInfo` field, `kickoffInfoFetch` + `getPetInfoProperties` helpers, kickoff wiring in `start()`, payload spread at six telemetry sites | | `src/common/telemetry/constants.ts` | New `petVersion` / `petBuildId` / `petCommitSha` properties on `PET_RESOLVE`, `PET_REFRESH`, `PET_PROCESS_RESTART` (GDPR blocks + TS types) | ## Compatibility with older PET binaries The extension currently ships PET as a bundled binary (downloaded by the Azure pipelines from PET CI artifacts). Until the PET release branch picks up #470 / #473, the bundled binary won't have the `info` handler. In that case: - PET responds with JSON-RPC error code `-1` (`Failed to find handler for request info`) - `sendRequestWithTimeout` rejects → `.catch` swallows → `petInfo` stays `undefined` - All three telemetry properties report `'unknown'` - One harmless `[pet] Failed to find handler for method: info` line surfaces from PET's stderr into the Python Environments output channel Discovery, refresh, resolve, and restart all continue to work normally. No crash, no functional regression — just `'unknown'` values in telemetry until PET catches up. ## Crash attribution semantics A subtle but important detail of where the spread is placed: the crashing PET's commit hash **is** captured in `PET_REFRESH` / `PET_RESOLVE` error events because we call `sendTelemetryEvent(..., ...this.getPetInfoProperties())` **before** killing the process and resetting the cache. So when a user reports "PET crashed during refresh", we can identify which exact commit was running. The `PET_PROCESS_RESTART` success event itself reports `'unknown'` for the **new** PET (its `info` reply usually hasn't landed in the few ms between `start()` and the telemetry call), but the new binary's identity surfaces on the very next refresh/resolve. ## Performance impact - One extra JSON-RPC roundtrip **per PET process lifetime** (typically once per VS Code session), not per telemetry event - ~3 string allocations per telemetry event from the spread — invisible against existing payload assembly - `kickoffInfoFetch` returns `void` synchronously; the response runs on the microtask queue and never blocks refresh/resolve - 2 s timeout caps the worst case if PET hangs entirely ## Validation - `npm run lint` ✅ - `npx tsc -p . --noEmit` ✅ - `npm run compile-tests` ✅ - `npm run unittest` — 1141 passing, 2 pending (unchanged from baseline) ✅ - `npm run compile` (webpack production bundle) ✅ ## Manual testing To get non-`'unknown'` values locally, build PET from main and drop the binary into `python-env-tools/bin/`: ```powershell # In the PET repo: $env:PET_COMMIT_SHA = (git rev-parse HEAD) $env:PET_BUILD_ID = "local-dev" cargo build --release --package pet # In this repo: Copy-Item <pet-repo>\target\release\pet.exe .\python-env-tools\bin\pet.exe -Force ``` Then F5 → open the Python Environments view → run `Python Environments: Refresh Environments`. Set the Python Environments output channel to Debug level to see the `[pet] info: { petVersion, buildId, commitSha }` line confirming the cache was populated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com>
Background
PET (Python Environment Tools) is a Rust-based JSONRPC server used by the VS Code Python extension to discover Python environments. PET does not send its own telemetry — instead, it returns metadata to the calling extension (vscode-python-environments), which forwards it via telemetry.
To help catch regressions, we need a way to know exactly which PET build a user is running by querying telemetry. PR #470 added a JSONRPC
inforequest that returns PET's version and an optional CI build ID. However, the build number alone isn't enough to pinpoint the exact source code — we also need the git commit hash.This PR extends the
inforesponse to include the git commit SHA baked into the binary at build time. On the extension side, the companion PR (vscode-python-environments#1548) passes this information along to telemetry, so we can correlate user-reported issues or regressions back to the specific PET commit.What Changed
1. Build script (
crates/pet/build.rs)PET_COMMIT_SHA— explicit override (highest priority)BUILD_SOURCEVERSION— set by Azure PipelinesGITHUB_SHA— set by GitHub Actionscargo:rustc-env=PET_COMMIT_SHA=....2. JSONRPC response (
crates/pet/src/jsonrpc.rs)commit_shafield to theInfoResponsestruct.InfoResponse::current()populates it from the compile-timePET_COMMIT_SHAenv var (same pattern used forbuild_id).Noneand omitted from the JSON response.3. JSONRPC documentation (
docs/JSONRPC.md)InfoResponseTypeScript interface to document the newcommitSha?: stringfield, including where the value is sourced from.4. Tests (
crates/pet/src/jsonrpc.rs,crates/pet/tests/jsonrpc_server_test.rs,crates/pet/tests/jsonrpc_client.rs)commit_shafield to the test client'sPetInfoResponsestruct.commit_sha, when present, is non-empty (mirrors existingbuild_idassertion pattern).build_idandcommit_shaareNonein local dev builds and only populated in CI.How It Works
Related PRs
infoJSONRPC request with version and build ID (merged)