From 695a3bc64e109e10d345568a90b13b7316d08494 Mon Sep 17 00:00:00 2001 From: "@rugpanov" Date: Fri, 14 Aug 2026 16:05:47 +0200 Subject: [PATCH 1/6] feat(python-setup): once-per-session env adoption telemetry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *Why* The extension persists a Python-setup state per project but has no measure of whether the managed environment actually sticks. Compute-drift detection (python_env.drift) compares the selected compute's env key against the recorded one, but it never checks that the .venv still exists — a user who deletes the environment while compute is unchanged is not "drifted", yet has plainly stopped using it. Drift is also event-driven, so it has no per-session denominator to turn into an adoption rate. *What* Add python_env.adoption: a once-per-session, categorical gauge emitted only for a project that has a uv-native setup on record (databricks.pythonSetup.setupState present). Its mere presence is the adoption-rate denominator; it carries venvPresent (does the managed .venv interpreter still exist on disk) and currentTargetType (cluster | serverless | none, read straight from the connection — no env key derived, so no second source of truth vs the CLI). - constants.ts: new Events.PYTHON_ENV_ADOPTION + EventType with per-field comments. - pythonSetupExtensions.ts: recordPythonSetupAdoption, enumerating both fields explicitly (allowlist discipline) so the schema stays compiler-enforced. - PythonSetupAdoptionReporter: thin, dependency-injected controller that dedupes once per project root per session, gates on VPEX-active, and is best-effort (a throwing seam never propagates into the observed flow, and does not latch the dedup so a transient failure retries). - extension.ts: wire real seams and fire on the first CONNECTED transition (so the compute kind is known); dedup makes repeats safe. - telemetry/README.md: rationale for the event (VPEX gate, distinction from drift, why it derives no env key). Additive and measurement-only: no persisted-state change, no migration, and no behavior change to the flow it observes. *Verification* - npx tsc --noEmit -p tsconfig.json — clean. - yarn test:unit — 831 passing, 10 pending, 0 failing (incl. 8 new controller tests and the new recordPythonSetupAdoption emitter tests). - yarn fix && yarn test:lint — eslint + prettier clean. Co-authored-by: Isaac --- packages/databricks-vscode/src/extension.ts | 46 ++++++++ .../PythonSetupAdoptionReporter.test.ts | 103 ++++++++++++++++++ .../PythonSetupAdoptionReporter.ts | 68 ++++++++++++ .../databricks-vscode/src/telemetry/README.md | 33 ++++++ .../src/telemetry/constants.ts | 23 ++++ .../telemetry/pythonSetupExtensions.test.ts | 63 +++++++++++ .../src/telemetry/pythonSetupExtensions.ts | 35 ++++++ 7 files changed, 371 insertions(+) create mode 100644 packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionReporter.test.ts create mode 100644 packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionReporter.ts diff --git a/packages/databricks-vscode/src/extension.ts b/packages/databricks-vscode/src/extension.ts index 906d0161e..1e052a5f9 100644 --- a/packages/databricks-vscode/src/extension.ts +++ b/packages/databricks-vscode/src/extension.ts @@ -45,6 +45,7 @@ import { import {CustomWhenContext} from "./vscode-objs/CustomWhenContext"; import {StateStorage} from "./vscode-objs/StateStorage"; import path from "node:path"; +import {existsSync} from "node:fs"; import { FeatureId, FeatureManager, @@ -58,7 +59,9 @@ import { resolveComputeFrom, } from "./python-setup/controllers/pythonSetupDeps"; import {PythonSetupDriftManager} from "./python-setup/controllers/PythonSetupDriftManager"; +import {PythonSetupAdoptionReporter} from "./python-setup/controllers/PythonSetupAdoptionReporter"; import {SetupCompute} from "./python-setup/controllers/PythonSetupEnvironmentSetup"; +import {venvInterpreterPath} from "./python-setup/utils/venvInterpreterPath"; import {resolveCliPath} from "./python-setup/utils/setupLocalArgs"; import { isPythonSetupEnabled, @@ -1148,6 +1151,49 @@ export async function activate( ); context.subscriptions.push(pythonSetupEntry); + // Once-per-session adoption gauge: for a project with a uv-native setup on + // record, whether its managed .venv still exists. Distinct from drift above + // (which compares compute env keys) — drift never checks that the venv is + // actually present. Measurement only, best-effort, and it derives no env key + // of its own: drift already reports env-key mismatch via python_env.drift. + const pythonSetupAdoption = new PythonSetupAdoptionReporter({ + projectRoot: () => { + try { + return workspaceFolderManager.activeProjectUri.fsPath; + } catch { + return undefined; + } + }, + // setupState is workspace-scoped (a single key), matching the drift + // manager's baseline; presence is what "VPEX-active" means. + isVpexActive: () => + stateStorage.get("databricks.pythonSetup.setupState") !== undefined, + getTargetType: () => + connectionManager.serverless + ? "serverless" + : connectionManager.cluster + ? "cluster" + : "none", + venvExists: (root) => + existsSync(venvInterpreterPath(path.join(root, ".venv"))), + record: (report) => telemetry.recordPythonSetupAdoption(report), + }); + context.subscriptions.push( + // Report once the connection is CONNECTED, so the compute kind is known; + // the reporter dedupes, so repeated transitions are safe. Firing before + // connect would latch a misleading "none" reading. + connectionManager.onDidChangeState(() => { + if (connectionManager.state === "CONNECTED") { + pythonSetupAdoption.report(); + } + }) + ); + // Cover activation while already connected (a reload with a live session), + // where onDidChangeState may not fire again. + if (connectionManager.state === "CONNECTED") { + pythonSetupAdoption.report(); + } + const environmentCommands = new EnvironmentCommands( featureManager, pythonExtensionWrapper, diff --git a/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionReporter.test.ts b/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionReporter.test.ts new file mode 100644 index 000000000..b54e52e1a --- /dev/null +++ b/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionReporter.test.ts @@ -0,0 +1,103 @@ +import {expect} from "chai"; +import {PythonSetupAdoption} from "../../telemetry/pythonSetupExtensions"; +import { + PythonSetupAdoptionDeps, + PythonSetupAdoptionReporter, +} from "./PythonSetupAdoptionReporter"; + +function makeDeps(over: Partial = {}): { + deps: PythonSetupAdoptionDeps; + recorded: PythonSetupAdoption[]; +} { + const recorded: PythonSetupAdoption[] = []; + const deps: PythonSetupAdoptionDeps = { + projectRoot: () => "/ws/project", + isVpexActive: () => true, + getTargetType: () => "serverless", + venvExists: () => true, + record: (r) => recorded.push(r), + ...over, + }; + return {deps, recorded}; +} + +describe("PythonSetupAdoptionReporter", () => { + it("emits the adoption gauge once for a VPEX-active project", () => { + const {deps, recorded} = makeDeps(); + new PythonSetupAdoptionReporter(deps).report(); + expect(recorded).to.deep.equal([ + {venvPresent: true, currentTargetType: "serverless"}, + ]); + }); + + it("reports an absent venv as venvPresent=false", () => { + const {deps, recorded} = makeDeps({venvExists: () => false}); + new PythonSetupAdoptionReporter(deps).report(); + expect(recorded).to.deep.equal([ + {venvPresent: false, currentTargetType: "serverless"}, + ]); + }); + + it("passes through the attached compute kind, including none", () => { + const {deps, recorded} = makeDeps({getTargetType: () => "none"}); + new PythonSetupAdoptionReporter(deps).report(); + expect(recorded[0].currentTargetType).to.equal("none"); + }); + + it("dedupes: repeated report() calls emit at most once per session", () => { + const {deps, recorded} = makeDeps(); + const reporter = new PythonSetupAdoptionReporter(deps); + reporter.report(); + reporter.report(); + reporter.report(); + expect(recorded).to.have.length(1); + }); + + it("does not emit when no project root is resolvable", () => { + const {deps, recorded} = makeDeps({projectRoot: () => undefined}); + new PythonSetupAdoptionReporter(deps).report(); + expect(recorded).to.be.empty; + }); + + it("does not emit when the project is not VPEX-active (no setup on record)", () => { + const {deps, recorded} = makeDeps({isVpexActive: () => false}); + new PythonSetupAdoptionReporter(deps).report(); + expect(recorded).to.be.empty; + }); + + it("re-checks VPEX-active on later calls until it can emit, then dedupes", () => { + // A setup can complete mid-session: the not-active early return must not + // latch the dedup, or a project that becomes VPEX-active would never be + // reported. + let active = false; + const {deps, recorded} = makeDeps({isVpexActive: () => active}); + const reporter = new PythonSetupAdoptionReporter(deps); + reporter.report(); + expect(recorded).to.be.empty; + active = true; + reporter.report(); + reporter.report(); + expect(recorded).to.have.length(1); + }); + + it("emits per distinct project root (multi-root)", () => { + let root = "/ws/a"; + const {deps, recorded} = makeDeps({projectRoot: () => root}); + const reporter = new PythonSetupAdoptionReporter(deps); + reporter.report(); + root = "/ws/b"; + reporter.report(); + expect(recorded).to.have.length(2); + }); + + it("is best-effort: a throwing seam never propagates into the caller", () => { + const {deps, recorded} = makeDeps({ + venvExists: () => { + throw new Error("fs blew up"); + }, + }); + const reporter = new PythonSetupAdoptionReporter(deps); + expect(() => reporter.report()).to.not.throw(); + expect(recorded).to.be.empty; + }); +}); diff --git a/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionReporter.ts b/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionReporter.ts new file mode 100644 index 000000000..2b05c007e --- /dev/null +++ b/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionReporter.ts @@ -0,0 +1,68 @@ +import {TargetCompute} from "../../telemetry/constants"; +import {PythonSetupAdoption} from "../../telemetry/pythonSetupExtensions"; + +export interface PythonSetupAdoptionDeps { + /** The active project's root, or undefined when none is resolvable. */ + projectRoot: () => string | undefined; + /** + * Whether a uv-native setup is on record for the workspace (the persisted + * `databricks.pythonSetup.setupState` exists). The gauge is emitted only + * when true, so the event's presence is the adoption-rate denominator. + */ + isVpexActive: () => boolean; + /** The compute kind attached right now (cluster / serverless / none). */ + getTargetType: () => TargetCompute; + /** Whether the project's managed `.venv` interpreter exists on disk. */ + venvExists: (root: string) => boolean; + /** Emit the gauge. Wraps telemetry; must be best-effort (never throw). */ + record: (report: PythonSetupAdoption) => void; +} + +/** + * Emits the once-per-session {@link PythonSetupAdoption} gauge: for a project + * that has a uv-native Python setup on record, whether its managed `.venv` is + * still present and what compute is attached. Purely a measurement — it reads + * state and records, never changing the flow it observes. + * + * Deliberately thin: the caller fires {@link report} on a trigger where the + * compute is known (a `CONNECTED` transition), and this dedupes so repeats are + * safe. Distinct from {@link PythonSetupDriftManager}, which measures compute + * env-key drift; this measures whether the environment still exists at all. + */ +export class PythonSetupAdoptionReporter { + /** Project roots already reported this session, to emit at most once each. */ + private readonly reported = new Set(); + + constructor(private readonly deps: PythonSetupAdoptionDeps) {} + + /** + * Read and emit the gauge for the active project if it is VPEX-active and + * has not been reported yet this session. Wrapped so telemetry can never + * throw into the caller (best-effort); the reported latch is set only after + * a successful record, so a transient read failure retries next time rather + * than silently swallowing the session's one reading. + */ + report(): void { + try { + const root = this.deps.projectRoot(); + if (root === undefined || this.reported.has(root)) { + return; + } + // Not VPEX-active: no setup on record, so there is nothing to gauge. + // Do NOT latch — a setup completing later this session should still + // get reported on a subsequent call. + if (!this.deps.isVpexActive()) { + return; + } + this.deps.record({ + venvPresent: this.deps.venvExists(root), + currentTargetType: this.deps.getTargetType(), + }); + this.reported.add(root); + } catch { + // Measurement must never break the observed flow. A throw here (a + // failed read, a telemetry error) is swallowed and, because the latch + // was not set, retried on the next trigger. + } + } +} diff --git a/packages/databricks-vscode/src/telemetry/README.md b/packages/databricks-vscode/src/telemetry/README.md index c27f455e1..0c941ff64 100644 --- a/packages/databricks-vscode/src/telemetry/README.md +++ b/packages/databricks-vscode/src/telemetry/README.md @@ -83,6 +83,39 @@ this flow: its `explicit_command` trigger fires only from the _legacy_ `databricks.environment.setupPythonEnv`, and the config view renders the two mutually exclusively. A user who sees this entry never emits that event. +## Python environment adoption (VPEX) + +`python_env.adoption`, emitted by `pythonSetupExtensions.ts` +(`recordPythonSetupAdoption`) and driven by +`python-setup/controllers/PythonSetupAdoptionReporter.ts`. A once-per-session +gauge, fired on the first `CONNECTED` transition so the attached compute is known. + +### Why it is emitted only when VPEX-active + +The event is recorded only for a project that has a uv-native setup on record +(`databricks.pythonSetup.setupState` is present). That gate is deliberate: the +event's mere presence is the **denominator** — one reading per session per project +that ever completed a setup — so `venvPresent` over all such events is a true +adoption rate, not a count without a base. + +### Why it is separate from `python_env.drift` + +Drift (from the drift detector) compares the selected compute's environment key +against the recorded one. It never checks that the `.venv` still exists: a user who +deletes the environment while compute is unchanged is not "drifted", yet has plainly +stopped using the managed env. `venvPresent` measures exactly that — whether the +managed interpreter is still on disk — which is orthogonal to drift. `venvPresent: +false` is a real value (the env is gone), not an omitted-because-unknown field. + +### Why it derives no environment key + +An earlier plan had this event re-derive the current compute's env key in +TypeScript to also report drift. It does not: the CLI is the authority on env keys +(resolved via a `--dry-run`), and the drift detector already emits `python_env.drift` +off that authoritative value. Re-deriving here would be a second, divergent source +of truth. This event only reports the compute _kind_ (`currentTargetType`), read +straight from the connection with no key involved. + ## Privacy Only categorical/enum values and durations — no file paths, cluster names or IDs, diff --git a/packages/databricks-vscode/src/telemetry/constants.ts b/packages/databricks-vscode/src/telemetry/constants.ts index bc04eaad4..50b3e79c1 100644 --- a/packages/databricks-vscode/src/telemetry/constants.ts +++ b/packages/databricks-vscode/src/telemetry/constants.ts @@ -28,6 +28,7 @@ export enum Events { PYTHON_ENV_SETUP_ATTEMPT = "python_env.setup.attempt", PYTHON_ENV_SETUP_RESULT = "python_env.setup.result", PYTHON_ENV_DRIFT = "python_env.drift", + PYTHON_ENV_ADOPTION = "python_env.adoption", AITOOLS_INSTALL = "aitoolsInstall", AITOOLS_UPDATE = "aitoolsUpdate", AITOOLS_UNINSTALL = "aitoolsUninstall", @@ -594,6 +595,28 @@ export class EventTypes { 'vocabulary as fromEnvKey (else "other")', }, }; + [Events.PYTHON_ENV_ADOPTION]: EventType<{ + venvPresent: boolean; + currentTargetType: TargetCompute; + }> = { + comment: + "A once-per-session adoption gauge for a project that has a uv-native Python setup on " + + "record (databricks.pythonSetup.setupState is present). Emitted only in that case, so " + + "its mere presence is a per-session denominator of VPEX-managed projects; it then " + + "reports whether the managed environment is still in place. Distinct from " + + "python_env.drift, which compares compute env keys — this reports whether the .venv " + + "still physically exists, which drift never checks. Categorical/boolean data only.", + venvPresent: { + comment: + "Whether the project's managed .venv interpreter still exists on disk. False means " + + "the environment was provisioned once but is now gone (deleted or never restored)", + }, + currentTargetType: { + comment: + "The compute kind attached when the session check ran (cluster | serverless | " + + "none), so adoption can be sliced by compute. No cluster IDs or names", + }, + }; } /** diff --git a/packages/databricks-vscode/src/telemetry/pythonSetupExtensions.test.ts b/packages/databricks-vscode/src/telemetry/pythonSetupExtensions.test.ts index d541d1bf1..f07988316 100644 --- a/packages/databricks-vscode/src/telemetry/pythonSetupExtensions.test.ts +++ b/packages/databricks-vscode/src/telemetry/pythonSetupExtensions.test.ts @@ -466,4 +466,67 @@ describe(__filename, () => { expect(drift.props["event.toEnvKey"]).to.equal("other"); }); }); + + describe("recordPythonSetupAdoption", () => { + it("emits python_env.adoption with the gauge as boolean and compute properties", () => { + const {telemetry, events} = makeTelemetry(); + telemetry.recordPythonSetupAdoption({ + venvPresent: true, + currentTargetType: "serverless", + }); + expect(events).to.have.length(1); + expect(events[0].name).to.equal("python_env.adoption"); + expect(events[0].props).to.deep.equal({ + "version": "1.0", + // A boolean lands as a "true"/"false" property, never a metric. + "event.venvPresent": "true", + "event.currentTargetType": "serverless", + }); + expect(events[0].metrics).to.not.have.property("event.venvPresent"); + }); + + it("records an absent venv as the boolean 'false', not an omitted field", () => { + const {telemetry, events} = makeTelemetry(); + telemetry.recordPythonSetupAdoption({ + venvPresent: false, + currentTargetType: "cluster", + }); + // venvPresent=false is a real value (the venv is gone) — it must be + // emitted, not dropped like an absent optional would be. + expect(events[0].props["event.venvPresent"]).to.equal("false"); + expect(events[0].props["event.currentTargetType"]).to.equal( + "cluster" + ); + }); + + it("carries currentTargetType 'none' when no compute is attached", () => { + const {telemetry, events} = makeTelemetry(); + telemetry.recordPythonSetupAdoption({ + venvPresent: true, + currentTargetType: "none", + }); + expect(events[0].props["event.currentTargetType"]).to.equal("none"); + }); + + it("emits only the schema's fields, never extra ones on the caller's object", () => { + const {telemetry, events} = makeTelemetry(); + // A future refactor could widen the payload or route a wider object + // through this seam; the transport must stay an allowlist. + telemetry.recordPythonSetupAdoption({ + venvPresent: true, + currentTargetType: "cluster", + projectPath: "/Users/jane/projects/acme", + clusterId: "0710-142042-secretcluster", + } as any); + const serialized = JSON.stringify(events[0].props); + expect(serialized).to.not.contain("jane"); + expect(serialized).to.not.contain("acme"); + expect(serialized).to.not.contain("0710"); + expect(Object.keys(events[0].props).sort()).to.deep.equal([ + "event.currentTargetType", + "event.venvPresent", + "version", + ]); + }); + }); }); diff --git a/packages/databricks-vscode/src/telemetry/pythonSetupExtensions.ts b/packages/databricks-vscode/src/telemetry/pythonSetupExtensions.ts index 24490ac20..529af3ac7 100644 --- a/packages/databricks-vscode/src/telemetry/pythonSetupExtensions.ts +++ b/packages/databricks-vscode/src/telemetry/pythonSetupExtensions.ts @@ -8,6 +8,7 @@ import { PythonSetupMode, PythonSetupOutcome, PythonSetupRunTrigger, + TargetCompute, } from "./constants"; import {PythonSetupWarning} from "../python-setup/models/PythonSetupResult"; @@ -45,6 +46,18 @@ export interface PythonSetupDrift { toEnvKey: string; } +/** + * A once-per-session adoption reading for a project with a Python setup on + * record: whether the managed environment is still in place, and the compute + * kind attached when the reading was taken. Both categorical/boolean. + */ +export interface PythonSetupAdoption { + /** Whether the project's managed `.venv` interpreter still exists on disk. */ + venvPresent: boolean; + /** The compute kind attached at the time of the reading. */ + currentTargetType: TargetCompute; +} + /** How a setup run ended, reduced to the categorical fields we report. */ export interface PythonSetupOutcomeReport { outcome: PythonSetupOutcome; @@ -205,6 +218,15 @@ declare module "." { * constrained to the categorical envKey vocabulary before emission. */ recordPythonSetupDrift(report: PythonSetupDrift): void; + + /** + * Record the once-per-session adoption gauge for a project with a Python + * setup on record: whether its managed `.venv` still exists and the + * compute kind attached at the time. Emitted only when the project is + * VPEX-active (a setup state is persisted), so the event's presence is + * itself the adoption-rate denominator. + */ + recordPythonSetupAdoption(report: PythonSetupAdoption): void; } } @@ -304,3 +326,16 @@ Telemetry.prototype.recordPythonSetupDrift = function ( toEnvKey: categoricalEnvKey(report.toEnvKey)!, }); }; + +Telemetry.prototype.recordPythonSetupAdoption = function ( + report: PythonSetupAdoption +): void { + // Named explicitly (not spread) for the same allowlist reason as the emitters + // above. Both fields are required, so there is no optional to spread: a + // boolean becomes a "true"/"false" property and the categorical target type a + // property, per recordEvent's serialization. + this.recordEvent(Events.PYTHON_ENV_ADOPTION, { + venvPresent: report.venvPresent, + currentTargetType: report.currentTargetType, + }); +}; From 9de02417a915d0a183490671953c76a9d87caffc Mon Sep 17 00:00:00 2001 From: "@rugpanov" Date: Fri, 14 Aug 2026 16:13:57 +0200 Subject: [PATCH 2/6] refactor(python-setup): address review of adoption telemetry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *Why* Multi-source review of the adoption gauge surfaced three items. *What* - Rename the controller PythonSetupAdoptionReporter -> PythonSetupAdoptionManager: "Reporter" is not a documented class-role suffix (CODE_CONVENTIONS §2); the class reacts to triggers and owns the per-session dedup, which is the XManager role, and it now matches its sibling PythonSetupDriftManager. - Also fire the gauge on setup completion, not only on CONNECTED. A first setup runs while already connected, so a project that becomes VPEX-active mid-session emitted nothing until the next reload; mirroring the drift manager's setupCompleted trigger closes that gap. The manager dedupes per root, so the added trigger never double-emits. - Document the multi-root shared-baseline limitation in telemetry/README.md: with a single workspace-scoped setupState key, a never-set-up sibling root can emit a spurious venvPresent=false. Same limitation the drift detector carries; the fix is the deferred per-project storage schema. *Verification* - npx tsc --noEmit -p tsconfig.json — clean. - yarn test:unit — 840 passing, 10 pending, 0 failing. - yarn fix && eslint/prettier on changed files — clean. Co-authored-by: Isaac --- packages/databricks-vscode/src/extension.ts | 36 +++++++++++-------- ....ts => PythonSetupAdoptionManager.test.ts} | 24 ++++++------- ...orter.ts => PythonSetupAdoptionManager.ts} | 2 +- .../databricks-vscode/src/telemetry/README.md | 16 +++++++-- 4 files changed, 48 insertions(+), 30 deletions(-) rename packages/databricks-vscode/src/python-setup/controllers/{PythonSetupAdoptionReporter.test.ts => PythonSetupAdoptionManager.test.ts} (83%) rename packages/databricks-vscode/src/python-setup/controllers/{PythonSetupAdoptionReporter.ts => PythonSetupAdoptionManager.ts} (98%) diff --git a/packages/databricks-vscode/src/extension.ts b/packages/databricks-vscode/src/extension.ts index 1e052a5f9..bff560340 100644 --- a/packages/databricks-vscode/src/extension.ts +++ b/packages/databricks-vscode/src/extension.ts @@ -59,7 +59,7 @@ import { resolveComputeFrom, } from "./python-setup/controllers/pythonSetupDeps"; import {PythonSetupDriftManager} from "./python-setup/controllers/PythonSetupDriftManager"; -import {PythonSetupAdoptionReporter} from "./python-setup/controllers/PythonSetupAdoptionReporter"; +import {PythonSetupAdoptionManager} from "./python-setup/controllers/PythonSetupAdoptionManager"; import {SetupCompute} from "./python-setup/controllers/PythonSetupEnvironmentSetup"; import {venvInterpreterPath} from "./python-setup/utils/venvInterpreterPath"; import {resolveCliPath} from "./python-setup/utils/setupLocalArgs"; @@ -1156,7 +1156,7 @@ export async function activate( // (which compares compute env keys) — drift never checks that the venv is // actually present. Measurement only, best-effort, and it derives no env key // of its own: drift already reports env-key mismatch via python_env.drift. - const pythonSetupAdoption = new PythonSetupAdoptionReporter({ + const pythonSetupAdoption = new PythonSetupAdoptionManager({ projectRoot: () => { try { return workspaceFolderManager.activeProjectUri.fsPath; @@ -1164,8 +1164,10 @@ export async function activate( return undefined; } }, - // setupState is workspace-scoped (a single key), matching the drift - // manager's baseline; presence is what "VPEX-active" means. + // setupState is workspace-scoped (a single key), so in a multi-root + // workspace this shares the drift manager's baseline limitation: another + // root's setup makes every root read as VPEX-active. Accepted here (see + // the drift follow-up); presence is what "VPEX-active" means. isVpexActive: () => stateStorage.get("databricks.pythonSetup.setupState") !== undefined, getTargetType: () => @@ -1178,21 +1180,25 @@ export async function activate( existsSync(venvInterpreterPath(path.join(root, ".venv"))), record: (report) => telemetry.recordPythonSetupAdoption(report), }); + // Report only once the connection is CONNECTED, so the compute kind is known; + // the manager dedupes per session, so repeated fires are safe. Firing before + // connect would latch a misleading "none" reading. + const reportAdoptionIfConnected = () => { + if (connectionManager.state === "CONNECTED") { + pythonSetupAdoption.report(); + } + }; context.subscriptions.push( - // Report once the connection is CONNECTED, so the compute kind is known; - // the reporter dedupes, so repeated transitions are safe. Firing before - // connect would latch a misleading "none" reading. - connectionManager.onDidChangeState(() => { - if (connectionManager.state === "CONNECTED") { - pythonSetupAdoption.report(); - } - }) + connectionManager.onDidChangeState(reportAdoptionIfConnected), + // A first setup completes while already CONNECTED (no new connection + // transition), so without this the session that turns a project + // VPEX-active would never be reported — it would wait for the next + // reload. Mirrors the drift manager's setupCompleted trigger. + pythonSetupEnvironment.onDidChangeState(reportAdoptionIfConnected) ); // Cover activation while already connected (a reload with a live session), // where onDidChangeState may not fire again. - if (connectionManager.state === "CONNECTED") { - pythonSetupAdoption.report(); - } + reportAdoptionIfConnected(); const environmentCommands = new EnvironmentCommands( featureManager, diff --git a/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionReporter.test.ts b/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionManager.test.ts similarity index 83% rename from packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionReporter.test.ts rename to packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionManager.test.ts index b54e52e1a..42b5ed9c8 100644 --- a/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionReporter.test.ts +++ b/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionManager.test.ts @@ -2,8 +2,8 @@ import {expect} from "chai"; import {PythonSetupAdoption} from "../../telemetry/pythonSetupExtensions"; import { PythonSetupAdoptionDeps, - PythonSetupAdoptionReporter, -} from "./PythonSetupAdoptionReporter"; + PythonSetupAdoptionManager, +} from "./PythonSetupAdoptionManager"; function makeDeps(over: Partial = {}): { deps: PythonSetupAdoptionDeps; @@ -21,10 +21,10 @@ function makeDeps(over: Partial = {}): { return {deps, recorded}; } -describe("PythonSetupAdoptionReporter", () => { +describe("PythonSetupAdoptionManager", () => { it("emits the adoption gauge once for a VPEX-active project", () => { const {deps, recorded} = makeDeps(); - new PythonSetupAdoptionReporter(deps).report(); + new PythonSetupAdoptionManager(deps).report(); expect(recorded).to.deep.equal([ {venvPresent: true, currentTargetType: "serverless"}, ]); @@ -32,7 +32,7 @@ describe("PythonSetupAdoptionReporter", () => { it("reports an absent venv as venvPresent=false", () => { const {deps, recorded} = makeDeps({venvExists: () => false}); - new PythonSetupAdoptionReporter(deps).report(); + new PythonSetupAdoptionManager(deps).report(); expect(recorded).to.deep.equal([ {venvPresent: false, currentTargetType: "serverless"}, ]); @@ -40,13 +40,13 @@ describe("PythonSetupAdoptionReporter", () => { it("passes through the attached compute kind, including none", () => { const {deps, recorded} = makeDeps({getTargetType: () => "none"}); - new PythonSetupAdoptionReporter(deps).report(); + new PythonSetupAdoptionManager(deps).report(); expect(recorded[0].currentTargetType).to.equal("none"); }); it("dedupes: repeated report() calls emit at most once per session", () => { const {deps, recorded} = makeDeps(); - const reporter = new PythonSetupAdoptionReporter(deps); + const reporter = new PythonSetupAdoptionManager(deps); reporter.report(); reporter.report(); reporter.report(); @@ -55,13 +55,13 @@ describe("PythonSetupAdoptionReporter", () => { it("does not emit when no project root is resolvable", () => { const {deps, recorded} = makeDeps({projectRoot: () => undefined}); - new PythonSetupAdoptionReporter(deps).report(); + new PythonSetupAdoptionManager(deps).report(); expect(recorded).to.be.empty; }); it("does not emit when the project is not VPEX-active (no setup on record)", () => { const {deps, recorded} = makeDeps({isVpexActive: () => false}); - new PythonSetupAdoptionReporter(deps).report(); + new PythonSetupAdoptionManager(deps).report(); expect(recorded).to.be.empty; }); @@ -71,7 +71,7 @@ describe("PythonSetupAdoptionReporter", () => { // reported. let active = false; const {deps, recorded} = makeDeps({isVpexActive: () => active}); - const reporter = new PythonSetupAdoptionReporter(deps); + const reporter = new PythonSetupAdoptionManager(deps); reporter.report(); expect(recorded).to.be.empty; active = true; @@ -83,7 +83,7 @@ describe("PythonSetupAdoptionReporter", () => { it("emits per distinct project root (multi-root)", () => { let root = "/ws/a"; const {deps, recorded} = makeDeps({projectRoot: () => root}); - const reporter = new PythonSetupAdoptionReporter(deps); + const reporter = new PythonSetupAdoptionManager(deps); reporter.report(); root = "/ws/b"; reporter.report(); @@ -96,7 +96,7 @@ describe("PythonSetupAdoptionReporter", () => { throw new Error("fs blew up"); }, }); - const reporter = new PythonSetupAdoptionReporter(deps); + const reporter = new PythonSetupAdoptionManager(deps); expect(() => reporter.report()).to.not.throw(); expect(recorded).to.be.empty; }); diff --git a/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionReporter.ts b/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionManager.ts similarity index 98% rename from packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionReporter.ts rename to packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionManager.ts index 2b05c007e..2d11c9061 100644 --- a/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionReporter.ts +++ b/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionManager.ts @@ -29,7 +29,7 @@ export interface PythonSetupAdoptionDeps { * safe. Distinct from {@link PythonSetupDriftManager}, which measures compute * env-key drift; this measures whether the environment still exists at all. */ -export class PythonSetupAdoptionReporter { +export class PythonSetupAdoptionManager { /** Project roots already reported this session, to emit at most once each. */ private readonly reported = new Set(); diff --git a/packages/databricks-vscode/src/telemetry/README.md b/packages/databricks-vscode/src/telemetry/README.md index 0c941ff64..1a2fab0ef 100644 --- a/packages/databricks-vscode/src/telemetry/README.md +++ b/packages/databricks-vscode/src/telemetry/README.md @@ -87,8 +87,10 @@ mutually exclusively. A user who sees this entry never emits that event. `python_env.adoption`, emitted by `pythonSetupExtensions.ts` (`recordPythonSetupAdoption`) and driven by -`python-setup/controllers/PythonSetupAdoptionReporter.ts`. A once-per-session -gauge, fired on the first `CONNECTED` transition so the attached compute is known. +`python-setup/controllers/PythonSetupAdoptionManager.ts`. A once-per-session +gauge, fired on the first `CONNECTED` transition (so the attached compute is known) +and on setup completion (to catch a project that becomes VPEX-active mid-session); +it dedupes per project root, so whichever fires first is the one reading. ### Why it is emitted only when VPEX-active @@ -116,6 +118,16 @@ off that authoritative value. Re-deriving here would be a second, divergent sour of truth. This event only reports the compute _kind_ (`currentTargetType`), read straight from the connection with no key involved. +### Known limitation: multi-root workspaces + +`setupState` is a single workspace-scoped key with no per-project namespacing, so +in a multi-root workspace where one project ran setup, _every_ root reads as +VPEX-active while `venvPresent` is checked against the **active** project's `.venv`. +A never-set-up sibling root can therefore emit a spurious `venvPresent: false`. This +is the same shared-baseline limitation the drift detector carries; the correct fix +is a per-project storage schema, deferred with it. Multi-root Databricks workspaces +are uncommon, so the skew is small. + ## Privacy Only categorical/enum values and durations — no file paths, cluster names or IDs, From 9b58794146e1e0c55dc8f970354c916aa3d3584b Mon Sep 17 00:00:00 2001 From: "@rugpanov" Date: Fri, 14 Aug 2026 16:21:11 +0200 Subject: [PATCH 3/6] fix(python-setup): drop the racy setup-completed adoption trigger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *Why* Re-review showed the setup-completed trigger added in the previous commit does not work: the persisted setupState write is fire-and-forget and lands on a later microtask, but the setup controller's state event fires synchronously, so a reading taken there reads the project as not-yet-VPEX-active and emits nothing. It also read the *active* project at fire time, which a mid-setup root switch could make the wrong one. *What* - Remove the `pythonSetupEnvironment.onDidChangeState` trigger; adoption is now a clean connect-time reading on `CONNECTED` (plus the activation-already-connected case). A project set up for the first time this session is measured from its next connect; the venv it just provisioned is already implied by python_env.setup.result = ok. - Update the inline and README docs to describe the connect-time semantics and why setup completion is deliberately not a trigger, and soften the "compute is known" wording (CONNECTED can carry currentTargetType = none). *Verification* - npx tsc --noEmit -p tsconfig.json — clean. - yarn test:unit — 840 passing, 10 pending, 0 failing. - yarn fix && eslint/prettier — clean. Co-authored-by: Isaac --- packages/databricks-vscode/src/extension.ts | 21 +++++++++++-------- .../databricks-vscode/src/telemetry/README.md | 12 ++++++++--- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/databricks-vscode/src/extension.ts b/packages/databricks-vscode/src/extension.ts index bff560340..6236b0ac4 100644 --- a/packages/databricks-vscode/src/extension.ts +++ b/packages/databricks-vscode/src/extension.ts @@ -1180,21 +1180,24 @@ export async function activate( existsSync(venvInterpreterPath(path.join(root, ".venv"))), record: (report) => telemetry.recordPythonSetupAdoption(report), }); - // Report only once the connection is CONNECTED, so the compute kind is known; - // the manager dedupes per session, so repeated fires are safe. Firing before - // connect would latch a misleading "none" reading. + // A connect-time reading: report once the connection is CONNECTED (so the + // compute is attached, though it may be "none" — auth-connected with nothing + // selected is a real slice). The manager dedupes per session, so repeated + // transitions are safe; firing before connect would latch "none". + // + // Deliberately NOT fired on setup completion. A first-ever setup's state + // write is fire-and-forget and lands on a later microtask, but the setup + // controller's state event fires synchronously — so a report() there would + // still read the project as not-yet-VPEX-active and emit nothing. Such a + // session is instead measured from its next connect; its just-provisioned + // venv is already implied by python_env.setup.result = ok. const reportAdoptionIfConnected = () => { if (connectionManager.state === "CONNECTED") { pythonSetupAdoption.report(); } }; context.subscriptions.push( - connectionManager.onDidChangeState(reportAdoptionIfConnected), - // A first setup completes while already CONNECTED (no new connection - // transition), so without this the session that turns a project - // VPEX-active would never be reported — it would wait for the next - // reload. Mirrors the drift manager's setupCompleted trigger. - pythonSetupEnvironment.onDidChangeState(reportAdoptionIfConnected) + connectionManager.onDidChangeState(reportAdoptionIfConnected) ); // Cover activation while already connected (a reload with a live session), // where onDidChangeState may not fire again. diff --git a/packages/databricks-vscode/src/telemetry/README.md b/packages/databricks-vscode/src/telemetry/README.md index 1a2fab0ef..fff30b741 100644 --- a/packages/databricks-vscode/src/telemetry/README.md +++ b/packages/databricks-vscode/src/telemetry/README.md @@ -88,9 +88,15 @@ mutually exclusively. A user who sees this entry never emits that event. `python_env.adoption`, emitted by `pythonSetupExtensions.ts` (`recordPythonSetupAdoption`) and driven by `python-setup/controllers/PythonSetupAdoptionManager.ts`. A once-per-session -gauge, fired on the first `CONNECTED` transition (so the attached compute is known) -and on setup completion (to catch a project that becomes VPEX-active mid-session); -it dedupes per project root, so whichever fires first is the one reading. +gauge, read on the first `CONNECTED` transition (so a compute is attached — possibly +`none`, i.e. auth-connected with nothing selected) and deduped per project root. + +It is deliberately **not** fired on setup completion. A first-ever setup's state +write is fire-and-forget and lands on a later microtask, while the setup +controller's state event fires synchronously — so a reading taken there would still +see the project as not-yet-VPEX-active and emit nothing. Such a session is measured +from its next connect instead; the venv it just provisioned is already implied by a +`python_env.setup.result` with `outcome: ok`. ### Why it is emitted only when VPEX-active From ebe0e224a31f6294b5cc4bd6319bf21992726527 Mon Sep 17 00:00:00 2001 From: "@rugpanov" Date: Fri, 14 Aug 2026 16:28:25 +0200 Subject: [PATCH 4/6] fix(python-setup): skip adoption gauge in multi-root workspaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *Why* setupState is a single workspace-scoped key with no per-project namespacing, so in a multi-root workspace it can't be attributed to the active root: a never-set-up sibling root would emit a spurious venvPresent=false and inflate the adoption denominator. Documenting the skew (as done for the drift detector) still leaves the adoption metric — whose whole purpose is an accurate rate — contaminated in that case. *What* Add an isAttributable seam to PythonSetupAdoptionManager; report() suppresses the reading (without latching, so a later single-root state still reports) when attribution is ambiguous. Wire it to `workspace.workspaceFolders.length <= 1`, so the gauge is emitted only when the single setupState key unambiguously describes the one root. Update the README to say multi-root is skipped rather than emitting spurious data. The drift detector still fires under the same single-key limitation; the real fix for both is the deferred per-project storage schema. *Verification* - npx tsc --noEmit -p tsconfig.json — clean. - yarn test:unit — 841 passing, 10 pending, 0 failing (adds a multi-root skip test). - yarn fix && eslint/prettier — clean. Co-authored-by: Isaac --- packages/databricks-vscode/src/extension.ts | 10 ++++++---- .../PythonSetupAdoptionManager.test.ts | 17 +++++++++++++++++ .../controllers/PythonSetupAdoptionManager.ts | 15 +++++++++++++++ .../databricks-vscode/src/telemetry/README.md | 18 +++++++++--------- 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/packages/databricks-vscode/src/extension.ts b/packages/databricks-vscode/src/extension.ts index 6236b0ac4..7e4336af4 100644 --- a/packages/databricks-vscode/src/extension.ts +++ b/packages/databricks-vscode/src/extension.ts @@ -1164,10 +1164,12 @@ export async function activate( return undefined; } }, - // setupState is workspace-scoped (a single key), so in a multi-root - // workspace this shares the drift manager's baseline limitation: another - // root's setup makes every root read as VPEX-active. Accepted here (see - // the drift follow-up); presence is what "VPEX-active" means. + // In a multi-root workspace the single workspace-scoped setupState key + // can't be pinned to the active root, so a reading could be a spurious + // venvPresent=false; skip rather than emit an untrustworthy one. (The + // drift detector shares this single-key limitation; the real fix is the + // deferred per-project storage schema.) + isAttributable: () => (workspace.workspaceFolders?.length ?? 0) <= 1, isVpexActive: () => stateStorage.get("databricks.pythonSetup.setupState") !== undefined, getTargetType: () => diff --git a/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionManager.test.ts b/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionManager.test.ts index 42b5ed9c8..a86de60c1 100644 --- a/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionManager.test.ts +++ b/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionManager.test.ts @@ -12,6 +12,7 @@ function makeDeps(over: Partial = {}): { const recorded: PythonSetupAdoption[] = []; const deps: PythonSetupAdoptionDeps = { projectRoot: () => "/ws/project", + isAttributable: () => true, isVpexActive: () => true, getTargetType: () => "serverless", venvExists: () => true, @@ -65,6 +66,22 @@ describe("PythonSetupAdoptionManager", () => { expect(recorded).to.be.empty; }); + it("does not emit when attribution is ambiguous (multi-root workspace)", () => { + // The shared setupState key could belong to another root, so emitting + // here would risk a spurious venvPresent=false. It must not latch either, + // so the workspace becoming single-root later can still report. + let attributable = false; + const {deps, recorded} = makeDeps({ + isAttributable: () => attributable, + }); + const reporter = new PythonSetupAdoptionManager(deps); + reporter.report(); + expect(recorded).to.be.empty; + attributable = true; + reporter.report(); + expect(recorded).to.have.length(1); + }); + it("re-checks VPEX-active on later calls until it can emit, then dedupes", () => { // A setup can complete mid-session: the not-active early return must not // latch the dedup, or a project that becomes VPEX-active would never be diff --git a/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionManager.ts b/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionManager.ts index 2d11c9061..6021374be 100644 --- a/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionManager.ts +++ b/packages/databricks-vscode/src/python-setup/controllers/PythonSetupAdoptionManager.ts @@ -4,6 +4,15 @@ import {PythonSetupAdoption} from "../../telemetry/pythonSetupExtensions"; export interface PythonSetupAdoptionDeps { /** The active project's root, or undefined when none is resolvable. */ projectRoot: () => string | undefined; + /** + * Whether the persisted setup state can be attributed to the active project. + * `setupState` is a single workspace-scoped key with no per-project + * namespacing, so in a multi-root workspace it may belong to a sibling root: + * checking a different root's `.venv` against it would emit a spurious + * `venvPresent: false` and inflate the denominator. False there suppresses the + * reading rather than emit an untrustworthy one. + */ + isAttributable: () => boolean; /** * Whether a uv-native setup is on record for the workspace (the persisted * `databricks.pythonSetup.setupState` exists). The gauge is emitted only @@ -48,6 +57,12 @@ export class PythonSetupAdoptionManager { if (root === undefined || this.reported.has(root)) { return; } + // Ambiguous attribution (multi-root workspace): the shared setupState + // key may describe a different root, so a reading here could be a + // spurious venvPresent=false. Suppress without latching. + if (!this.deps.isAttributable()) { + return; + } // Not VPEX-active: no setup on record, so there is nothing to gauge. // Do NOT latch — a setup completing later this session should still // get reported on a subsequent call. diff --git a/packages/databricks-vscode/src/telemetry/README.md b/packages/databricks-vscode/src/telemetry/README.md index fff30b741..8f0107b8a 100644 --- a/packages/databricks-vscode/src/telemetry/README.md +++ b/packages/databricks-vscode/src/telemetry/README.md @@ -124,15 +124,15 @@ off that authoritative value. Re-deriving here would be a second, divergent sour of truth. This event only reports the compute _kind_ (`currentTargetType`), read straight from the connection with no key involved. -### Known limitation: multi-root workspaces - -`setupState` is a single workspace-scoped key with no per-project namespacing, so -in a multi-root workspace where one project ran setup, _every_ root reads as -VPEX-active while `venvPresent` is checked against the **active** project's `.venv`. -A never-set-up sibling root can therefore emit a spurious `venvPresent: false`. This -is the same shared-baseline limitation the drift detector carries; the correct fix -is a per-project storage schema, deferred with it. Multi-root Databricks workspaces -are uncommon, so the skew is small. +### Multi-root workspaces are skipped + +`setupState` is a single workspace-scoped key with no per-project namespacing, so in +a multi-root workspace it can't be pinned to the active root: a never-set-up sibling +root would emit a spurious `venvPresent: false` and inflate the denominator. Rather +than record an untrustworthy reading, the gauge is **suppressed entirely** when the +workspace has more than one root. The drift detector shares this single-key +limitation (it still fires there); the real fix for both is a per-project storage +schema, deferred. Multi-root Databricks workspaces are uncommon, so little is lost. ## Privacy From 8b6888710b14c5b9f4aca87ee79a844fe48c064d Mon Sep 17 00:00:00 2001 From: "@rugpanov" Date: Fri, 14 Aug 2026 16:32:14 +0200 Subject: [PATCH 5/6] docs(python-setup): note the multi-root guard is a heuristic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *Why* The adoption README claimed multi-root workspaces were "suppressed entirely", which over-claims: the one-root guard is a heuristic, not proof of provenance, since the single workspace-scoped setupState key records no root. *What* Reword the limitation note to say the gauge is skipped for multi-root and that the guard reduces but does not eliminate mis-attribution (a mid-session reduction to one root can still orphan a sibling's key); the complete fix is the deferred per-project storage schema. Docs only, no behavior change. *Verification* - prettier -c — clean. No code changed. Co-authored-by: Isaac --- .../databricks-vscode/src/telemetry/README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/databricks-vscode/src/telemetry/README.md b/packages/databricks-vscode/src/telemetry/README.md index 8f0107b8a..fc774880f 100644 --- a/packages/databricks-vscode/src/telemetry/README.md +++ b/packages/databricks-vscode/src/telemetry/README.md @@ -128,11 +128,16 @@ straight from the connection with no key involved. `setupState` is a single workspace-scoped key with no per-project namespacing, so in a multi-root workspace it can't be pinned to the active root: a never-set-up sibling -root would emit a spurious `venvPresent: false` and inflate the denominator. Rather -than record an untrustworthy reading, the gauge is **suppressed entirely** when the -workspace has more than one root. The drift detector shares this single-key -limitation (it still fires there); the real fix for both is a per-project storage -schema, deferred. Multi-root Databricks workspaces are uncommon, so little is lost. +root would emit a spurious `venvPresent: false` and inflate the denominator. So the +gauge is skipped when the workspace has more than one root, rather than record an +untrustworthy reading. + +The one-root guard is a heuristic, not proof of provenance — the key records no root, +so a rare edge (a multi-root workspace reduced to one root mid-session, leaving the +prior root's key) can still mis-attribute. Eliminating that needs the per-project +storage schema, deferred (the drift detector shares the single-key limitation and +does not even guard multi-root). Multi-root Databricks workspaces are uncommon, so +the residual skew is negligible. ## Privacy From 1d54af64d9374a83463a0b67f746c0eef8a6e0dc Mon Sep 17 00:00:00 2001 From: "@rugpanov" Date: Fri, 14 Aug 2026 16:53:46 +0200 Subject: [PATCH 6/6] docs(python-setup): state the no-env-key rationale on its own terms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *Why* The README explained "why it derives no env key" by referring to an earlier design plan, which a reader of the code has no context for. *What* Reword the section to state the rationale directly (reports the compute kind only; the CLI is the env-key authority and drift already emits off it) without referencing any prior plan. Docs only. *Verification* - prettier -c — clean. No code changed. Co-authored-by: Isaac --- packages/databricks-vscode/src/telemetry/README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/databricks-vscode/src/telemetry/README.md b/packages/databricks-vscode/src/telemetry/README.md index fc774880f..4e9fd9606 100644 --- a/packages/databricks-vscode/src/telemetry/README.md +++ b/packages/databricks-vscode/src/telemetry/README.md @@ -117,12 +117,11 @@ false` is a real value (the env is gone), not an omitted-because-unknown field. ### Why it derives no environment key -An earlier plan had this event re-derive the current compute's env key in -TypeScript to also report drift. It does not: the CLI is the authority on env keys -(resolved via a `--dry-run`), and the drift detector already emits `python_env.drift` -off that authoritative value. Re-deriving here would be a second, divergent source -of truth. This event only reports the compute _kind_ (`currentTargetType`), read -straight from the connection with no key involved. +This event reports the compute _kind_ (`currentTargetType`) only, read straight from +the connection — it never derives an environment key. The CLI is the authority on env +keys (resolved via a `--dry-run`), and the drift detector already emits +`python_env.drift` off that authoritative value; deriving a key here would be a +second, divergent source of truth. ### Multi-root workspaces are skipped