From 14c8e8ee8f2dbf0fbc84cd56316846b5b70dbbc5 Mon Sep 17 00:00:00 2001 From: Anton Nekipelov <226657+anton-107@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:42:28 +0000 Subject: [PATCH] ssh: record the serverless environment version in tunnel telemetry The CLI default is still environment 4, and moving it to 5 is planned once the driver's sandbox port forwarding reaches every region. The tunnel event has no environment field today, so neither opt-in adoption nor the effect of the flip can be measured. Send the environment version the server job is submitted with. The submit path and the event now read it from one helper, so the two cannot drift. Dedicated clusters and custom base environments leave it unset. The matching proto field lands in universe separately; ingestion drops the field until then. Co-authored-by: Isaac --- experimental/ssh/internal/client/client.go | 13 +++++++- .../internal/client/client_internal_test.go | 30 +++++++++++++------ libs/telemetry/protos/ssh_tunnel.go | 5 ++++ 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/experimental/ssh/internal/client/client.go b/experimental/ssh/internal/client/client.go index 80142d54d4c..a31a5a075a9 100644 --- a/experimental/ssh/internal/client/client.go +++ b/experimental/ssh/internal/client/client.go @@ -226,6 +226,16 @@ func (o *ClientOptions) IsServerlessMode() bool { return o.ClusterID == "" && o.ConnectionName != "" } +// submittedEnvironmentVersion returns the serverless environment version the server job is +// submitted with, or 0 when it sets none: dedicated clusters have no environment, and a custom +// base environment carries its own version. +func (o *ClientOptions) submittedEnvironmentVersion() int { + if !o.IsServerlessMode() || o.BaseEnvironment != "" { + return 0 + } + return max(o.EnvironmentVersion, minEnvironmentVersion) +} + // SessionIdentifier returns the unique identifier for the session. // For dedicated clusters, this is the cluster ID. For serverless, this is the connection name. func (o *ClientOptions) SessionIdentifier() string { @@ -842,7 +852,7 @@ func buildSSHServerSubmitRun(version, secretScopeName, jobNotebookPath, baseEnvi if baseEnvironment != "" { spec.BaseEnvironment = baseEnvironment } else { - spec.EnvironmentVersion = strconv.Itoa(max(opts.EnvironmentVersion, minEnvironmentVersion)) + spec.EnvironmentVersion = strconv.Itoa(opts.submittedEnvironmentVersion()) } submitRequest.Environments = []jobs.JobEnvironment{ { @@ -1595,6 +1605,7 @@ func buildSshTunnelEvent(opts ClientOptions, outcome connectOutcome) *protos.Ssh ServerStartTimeMs: outcome.serverStartTimeMs, IsSuccess: outcome.isSuccess, HasBaseEnvironment: opts.BaseEnvironment != "", + EnvironmentVersion: opts.submittedEnvironmentVersion(), HasUsagePolicy: opts.UsagePolicyID != "", // The connect side can only report that the knob was asked for. Whether any detached // process was there to keep is reported by the server, at teardown. diff --git a/experimental/ssh/internal/client/client_internal_test.go b/experimental/ssh/internal/client/client_internal_test.go index 1db7adbd88a..452a8162b26 100644 --- a/experimental/ssh/internal/client/client_internal_test.go +++ b/experimental/ssh/internal/client/client_internal_test.go @@ -483,27 +483,39 @@ func TestBuildSshTunnelEvent(t *testing.T) { name: "serverless with accelerator", opts: ClientOptions{ConnectionName: "my-conn", Accelerator: "GPU_1xA10"}, want: protos.SshTunnelEvent{ - ComputeType: protos.SshTunnelComputeTypeServerless, - AcceleratorType: "GPU_1xA10", - ClientMode: protos.SshTunnelClientModeSSH, + ComputeType: protos.SshTunnelComputeTypeServerless, + AcceleratorType: "GPU_1xA10", + ClientMode: protos.SshTunnelClientModeSSH, + EnvironmentVersion: 4, }, }, { name: "proxy mode takes precedence over IDE", opts: ClientOptions{ConnectionName: "my-conn", ProxyMode: true, IDE: "vscode"}, want: protos.SshTunnelEvent{ - ComputeType: protos.SshTunnelComputeTypeServerless, - IdeType: "vscode", - ClientMode: protos.SshTunnelClientModeProxy, + ComputeType: protos.SshTunnelComputeTypeServerless, + IdeType: "vscode", + ClientMode: protos.SshTunnelClientModeProxy, + EnvironmentVersion: 4, }, }, { name: "IDE mode", opts: ClientOptions{ConnectionName: "my-conn", IDE: "cursor"}, want: protos.SshTunnelEvent{ - ComputeType: protos.SshTunnelComputeTypeServerless, - IdeType: "cursor", - ClientMode: protos.SshTunnelClientModeIDE, + ComputeType: protos.SshTunnelComputeTypeServerless, + IdeType: "cursor", + ClientMode: protos.SshTunnelClientModeIDE, + EnvironmentVersion: 4, + }, + }, + { + name: "explicit serverless environment version", + opts: ClientOptions{ConnectionName: "my-conn", EnvironmentVersion: 6}, + want: protos.SshTunnelEvent{ + ComputeType: protos.SshTunnelComputeTypeServerless, + ClientMode: protos.SshTunnelClientModeSSH, + EnvironmentVersion: 6, }, }, { diff --git a/libs/telemetry/protos/ssh_tunnel.go b/libs/telemetry/protos/ssh_tunnel.go index 708a453ac63..e4f77f91056 100644 --- a/libs/telemetry/protos/ssh_tunnel.go +++ b/libs/telemetry/protos/ssh_tunnel.go @@ -156,6 +156,11 @@ type SshTunnelEvent struct { // Whether the connection was successful. IsSuccess bool `json:"is_success"` + // Serverless environment version the SSH server job is submitted with: the + // --environment-version value or the CLI default. Unset for dedicated clusters and + // for a custom base environment, which carries its own version. + EnvironmentVersion int `json:"environment_version,omitempty"` + // Whether a serverless usage policy was set via --usage-policy-id. // Only the presence is recorded, not the policy ID itself. HasUsagePolicy bool `json:"has_usage_policy"`