From f17b40761ed71fee5f2bc615a9223eaf0ffbe617 Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Tue, 11 Aug 2026 22:17:03 +0000 Subject: [PATCH 01/10] feat(logging): tag logs with a per-session ID Add SessionLogger, which wraps the Coder output channel and prefixes every message with the activation's session ID so all log lines for a session can be correlated by a single ID. Generate the ID once in the ServiceContainer, reuse it as the telemetry session ID, and expose it via getSessionId() for downstream consumers. --- src/core/container.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/container.ts b/src/core/container.ts index a93eec37a3..9d82596ac4 100644 --- a/src/core/container.ts +++ b/src/core/container.ts @@ -152,6 +152,10 @@ export class ServiceContainer implements vscode.Disposable { return this.logger; } + getSessionId(): string { + return this.sessionId; + } + getCliManager(): CliManager { return this.cliManager; } From a7376661312a08b79bf9dc41332e576998f93ed5 Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Tue, 11 Aug 2026 22:22:55 +0000 Subject: [PATCH 02/10] feat: propagate the session ID to requests and the CLI Attach the session ID to every API request via the W3C baggage header (session_id=) so the server can correlate requests with the session's logs and telemetry, threading it through CoderApi.create at all call sites. Set CODER_TRACE_SESSION_ID on both process.env and the terminal environment collection so the spawned `coder ssh` ProxyCommand reuses the plugin's session ID instead of generating its own. --- src/api/coderApi.ts | 17 ++++++++++++++- src/core/container.ts | 1 + src/deployment/deploymentManager.ts | 10 ++++++++- src/extension.ts | 1 + src/login/loginCoordinator.ts | 18 ++++++++++++++-- src/oauth/authorizer.ts | 9 +++++++- src/oauth/sessionManager.ts | 18 ++++++++++++++-- src/remote/environment.ts | 23 ++++++++++++-------- src/remote/remote.ts | 2 ++ test/mocks/testHelpers.ts | 1 + test/unit/api/coderApi.test.ts | 25 ++++++++++++++++++++++ test/unit/login/loginCoordinator.test.ts | 1 + test/unit/oauth/authorizer.test.ts | 1 + test/unit/remote/environment.test.ts | 27 +++++++++++++++++------- 14 files changed, 130 insertions(+), 24 deletions(-) diff --git a/src/api/coderApi.ts b/src/api/coderApi.ts index 7b02b9b24b..15d3564c86 100644 --- a/src/api/coderApi.ts +++ b/src/api/coderApi.ts @@ -77,6 +77,9 @@ import type { const coderSessionTokenHeader = "Coder-Session-Token"; +/** W3C baggage header used to propagate the session ID to the server. */ +const baggageHeader = "baggage"; + /** * Default timeout for REST requests, so requests hung on half-open TCP * connections (e.g. after system sleep) don't stall pollers forever. @@ -119,6 +122,7 @@ export class CoderApi extends Api implements vscode.Disposable { private readonly telemetry: TelemetryReporter, private readonly httpRequestsTelemetry: HttpRequestsTelemetry, private readonly authConfigTracker: AuthConfigTracker, + private readonly sessionId: string | undefined, ) { super(); wrapWithValidation(this); @@ -130,13 +134,16 @@ export class CoderApi extends Api implements vscode.Disposable { * Automatically sets up logging interceptors, certificate handling, * HTTP request telemetry, and WebSocket connection telemetry. All * telemetry routes through the single reporter passed in (defaults to - * NOOP_TELEMETRY_REPORTER for throwaway clients). + * NOOP_TELEMETRY_REPORTER for throwaway clients). When a session ID is + * provided it is attached to every request via the `baggage` header so the + * server can correlate requests with the session's logs and telemetry. */ static create( baseUrl: string, token: string | undefined, output: Logger, telemetry: TelemetryReporter = NOOP_TELEMETRY_REPORTER, + sessionId?: string, ): CoderApi { const httpRequestsTelemetry = new HttpRequestsTelemetry(telemetry); const authConfigTracker = new AuthConfigTracker(); @@ -145,8 +152,13 @@ export class CoderApi extends Api implements vscode.Disposable { telemetry, httpRequestsTelemetry, authConfigTracker, + sessionId, ); client.getAxiosInstance().defaults.timeout = DEFAULT_REQUEST_TIMEOUT_MS; + if (sessionId) { + client.getAxiosInstance().defaults.headers.common[baggageHeader] = + `session_id=${sessionId}`; + } client.setCredentials(baseUrl, token); setupInterceptors(client, output, httpRequestsTelemetry, authConfigTracker); @@ -379,6 +391,9 @@ export class CoderApi extends Api implements vscode.Disposable { */ const headers = { ...(token ? { [coderSessionTokenHeader]: token } : {}), + ...(this.sessionId + ? { [baggageHeader]: `session_id=${this.sessionId}` } + : {}), ...configs.options?.headers, ...headersFromCommand, }; diff --git a/src/core/container.ts b/src/core/container.ts index 9d82596ac4..74c65032de 100644 --- a/src/core/container.ts +++ b/src/core/container.ts @@ -118,6 +118,7 @@ export class ServiceContainer implements vscode.Disposable { new AuthTelemetry(this.telemetryService), this.oauthCallback, context.extension.id, + this.sessionId, ); this.duplicateWorkspaceIpc = new DuplicateWorkspaceIpc( context.secrets, diff --git a/src/deployment/deploymentManager.ts b/src/deployment/deploymentManager.ts index 7c0c47e3e2..a17f137225 100644 --- a/src/deployment/deploymentManager.ts +++ b/src/deployment/deploymentManager.ts @@ -49,6 +49,7 @@ export class DeploymentManager implements vscode.Disposable { private readonly logger: Logger; private readonly telemetryService: TelemetryService; private readonly deploymentTelemetry: DeploymentTelemetry; + private readonly sessionId: string; readonly #sessionStore = new SessionStore(); #disposed = false; @@ -69,6 +70,7 @@ export class DeploymentManager implements vscode.Disposable { this.logger = serviceContainer.getLogger(); this.telemetryService = serviceContainer.getTelemetryService(); this.deploymentTelemetry = new DeploymentTelemetry(this.telemetryService); + this.sessionId = serviceContainer.getSessionId(); } public static create( @@ -141,7 +143,13 @@ export class DeploymentManager implements vscode.Disposable { url: string, token: string | undefined, ): Promise { - const tempClient = CoderApi.create(url, token, this.logger); + const tempClient = CoderApi.create( + url, + token, + this.logger, + undefined, + this.sessionId, + ); try { return await tempClient.getAuthenticatedUser(); } finally { diff --git a/src/extension.ts b/src/extension.ts index 6dc82209e0..dae226b28d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -141,6 +141,7 @@ async function doActivate( deploymentSessionAuth?.token, output, telemetryService, + serviceContainer.getSessionId(), ); ctx.subscriptions.push(client); diff --git a/src/login/loginCoordinator.ts b/src/login/loginCoordinator.ts index 15c4fd93f2..457e868ab2 100644 --- a/src/login/loginCoordinator.ts +++ b/src/login/loginCoordinator.ts @@ -91,12 +91,14 @@ export class LoginCoordinator implements vscode.Disposable { private readonly authTelemetry: AuthTelemetry, oauthCallback: OAuthCallback, extensionId: string, + private readonly sessionId: string, ) { this.oauthAuthorizer = new OAuthAuthorizer( secretsManager, oauthCallback, logger, extensionId, + sessionId, ); } @@ -248,7 +250,13 @@ export class LoginCoordinator implements vscode.Disposable { safeHostname, async (auth) => { if (auth?.token) { - const client = CoderApi.create(auth.url, auth.token, this.logger); + const client = CoderApi.create( + auth.url, + auth.token, + this.logger, + undefined, + this.sessionId, + ); try { const user = await client.getAuthenticatedUser(); // Stop listening only on success; a bad token shouldn't @@ -284,7 +292,13 @@ export class LoginCoordinator implements vscode.Disposable { providedToken?: string, tokenSignInConfirmed = false, ): Promise { - const client = CoderApi.create(deployment.url, "", this.logger); + const client = CoderApi.create( + deployment.url, + "", + this.logger, + undefined, + this.sessionId, + ); try { return await this.runLoginAttempts( client, diff --git a/src/oauth/authorizer.ts b/src/oauth/authorizer.ts index b22bb90a70..0420bbba46 100644 --- a/src/oauth/authorizer.ts +++ b/src/oauth/authorizer.ts @@ -51,6 +51,7 @@ export class OAuthAuthorizer implements vscode.Disposable { private readonly oauthCallback: OAuthCallback, private readonly logger: Logger, private readonly extensionId: string, + private readonly sessionId: string, ) {} /** @@ -63,7 +64,13 @@ export class OAuthAuthorizer implements vscode.Disposable { progress: vscode.Progress<{ message?: string; increment?: number }>, cancellationToken: vscode.CancellationToken, ): Promise<{ tokenResponse: OAuth2TokenResponse; user: User }> { - const client = CoderApi.create(deployment.url, undefined, this.logger); + const client = CoderApi.create( + deployment.url, + undefined, + this.logger, + undefined, + this.sessionId, + ); try { return await this.runLoginFlow( client, diff --git a/src/oauth/sessionManager.ts b/src/oauth/sessionManager.ts index 5ccc122aa5..baacf2b09f 100644 --- a/src/oauth/sessionManager.ts +++ b/src/oauth/sessionManager.ts @@ -70,6 +70,7 @@ export class OAuthSessionManager implements vscode.Disposable { container.getLogger(), onAuthRequired, new AuthTelemetry(container.getTelemetryService()), + container.getSessionId(), ); manager.setupTokenListener(); manager.scheduleNextRefresh(); @@ -82,6 +83,7 @@ export class OAuthSessionManager implements vscode.Disposable { private readonly logger: Logger, private readonly onAuthRequired: () => Promise, private readonly authTelemetry: AuthTelemetry, + private readonly sessionId: string, ) {} /** @@ -299,7 +301,13 @@ export class OAuthSessionManager implements vscode.Disposable { }) => Promise, ): Promise { const deployment = this.requireDeployment(); - const client = CoderApi.create(deployment.url, token, this.logger); + const client = CoderApi.create( + deployment.url, + token, + this.logger, + undefined, + this.sessionId, + ); try { const axiosInstance = client.getAxiosInstance(); const metadataClient = new OAuthMetadataClient( @@ -459,7 +467,13 @@ export class OAuthSessionManager implements vscode.Disposable { deployment: Deployment, accessToken: string, ): Promise { - const client = CoderApi.create(deployment.url, accessToken, this.logger); + const client = CoderApi.create( + deployment.url, + accessToken, + this.logger, + undefined, + this.sessionId, + ); try { return (await client.getAuthenticatedUser()).username; } catch (error) { diff --git a/src/remote/environment.ts b/src/remote/environment.ts index ab9e35c84f..4c913c13f5 100644 --- a/src/remote/environment.ts +++ b/src/remote/environment.ts @@ -26,13 +26,14 @@ export const SSH_PROXY_SETTINGS: ReadonlyArray<{ /** * Apply the SSH environment that the spawned `coder ssh` ProxyCommand inherits. - * Currently just the proxy config (HTTP_PROXY/HTTPS_PROXY/NO_PROXY), read by the - * coder CLI like any Go HTTP client. Applied via both process.env (ssh spawned as - * a child, `remote.SSH.useLocalServer=true`) and the terminal env collection (ssh - * spawned in a terminal, `useLocalServer=false`, which can't see process.env), - * since the mode isn't knowable up front. Mutating env rather than the SSH config - * keeps credentialed URLs off disk and windows independent. Disposable restores - * both. + * Includes the proxy config (HTTP_PROXY/HTTPS_PROXY/NO_PROXY), read by the coder + * CLI like any Go HTTP client, and the session ID via CODER_TRACE_SESSION_ID so + * the CLI reuses the plugin's session ID instead of generating its own. Applied + * via both process.env (ssh spawned as a child, `remote.SSH.useLocalServer=true`) + * and the terminal env collection (ssh spawned in a terminal, + * `useLocalServer=false`, which can't see process.env), since the mode isn't + * knowable up front. Mutating env rather than the SSH config keeps credentialed + * URLs off disk and windows independent. Disposable restores both. */ export function applySshEnvironment( cfg: Pick, @@ -40,9 +41,13 @@ export function applySshEnvironment( GlobalEnvironmentVariableCollection, "persistent" | "replace" | "clear" >, + sessionId: string, env: Environment = process.env, ): { dispose(): void } { - const values = getSshProxyEnvironment(cfg); + const values: Environment = { + ...getSshProxyEnvironment(cfg), + CODER_TRACE_SESSION_ID: sessionId, + }; const restoreEnv = applyEnvironment(values, env); collection.persistent = false; @@ -83,7 +88,7 @@ export function getSshProxyEnvironment( } function applyEnvironment( - values: SshEnvironment, + values: Environment, env: Environment, ): { dispose(): void } { // Stored `undefined` means the key was absent and should be deleted on cleanup. diff --git a/src/remote/remote.ts b/src/remote/remote.ts index 0c29a0f5da..38a5741dd1 100644 --- a/src/remote/remote.ts +++ b/src/remote/remote.ts @@ -231,6 +231,7 @@ export class Remote { applySshEnvironment( vscode.workspace.getConfiguration(), this.extensionContext.environmentVariableCollection, + this.serviceContainer.getSessionId(), ), ); // Create OAuth session manager for this remote deployment @@ -256,6 +257,7 @@ export class Remote { token, this.logger, this.serviceContainer.getTelemetryService(), + this.serviceContainer.getSessionId(), ); disposables.push(workspaceClient); diff --git a/test/mocks/testHelpers.ts b/test/mocks/testHelpers.ts index f89a0a3162..9de5693afd 100644 --- a/test/mocks/testHelpers.ts +++ b/test/mocks/testHelpers.ts @@ -595,6 +595,7 @@ export function createMockServiceContainer( return { getTelemetryService: () => telemetry, getLogger: () => logger, + getSessionId: () => "0123456789abcdef0123456789abcdef", getSecretsManager: () => require("secretsManager", overrides.secretsManager), getMementoManager: () => diff --git a/test/unit/api/coderApi.test.ts b/test/unit/api/coderApi.test.ts index c79b438699..57ec5e33d3 100644 --- a/test/unit/api/coderApi.test.ts +++ b/test/unit/api/coderApi.test.ts @@ -148,6 +148,31 @@ describe("CoderApi", () => { ); }); + it("attaches the session ID to requests as a baggage header", async () => { + const sessionId = "0123456789abcdef0123456789abcdef"; + api = CoderApi.create( + CODER_URL, + AXIOS_TOKEN, + mockLogger, + NOOP_TELEMETRY_REPORTER, + sessionId, + ); + + const response = await api.getAxiosInstance().get("/api/v2/users/me"); + + expect(response.config.headers["baggage"]).toBe( + `session_id=${sessionId}`, + ); + }); + + it("omits the baggage header when no session ID is provided", async () => { + api = createApi(); + + const response = await api.getAxiosInstance().get("/api/v2/users/me"); + + expect(response.config.headers["baggage"]).toBeUndefined(); + }); + it("applies the default timeout to requests", async () => { api = createApi(); const response = await api.getAxiosInstance().get("/api/v2/users/me"); diff --git a/test/unit/login/loginCoordinator.test.ts b/test/unit/login/loginCoordinator.test.ts index 44bfbb7982..00a9038fb0 100644 --- a/test/unit/login/loginCoordinator.test.ts +++ b/test/unit/login/loginCoordinator.test.ts @@ -142,6 +142,7 @@ function createTestContext(telemetry?: TelemetryService) { authTelemetry, oauthCallback, "coder.coder-remote", + "0123456789abcdef0123456789abcdef", ); const mockSuccessfulAuth = (user = createMockUser()) => { diff --git a/test/unit/oauth/authorizer.test.ts b/test/unit/oauth/authorizer.test.ts index 2f95f7ae8c..b4c5fb6e0f 100644 --- a/test/unit/oauth/authorizer.test.ts +++ b/test/unit/oauth/authorizer.test.ts @@ -68,6 +68,7 @@ function createTestContext() { base.oauthCallback, base.logger, EXTENSION_ID, + "0123456789abcdef0123456789abcdef", ); /** Starts login flow and waits for browser to open. Returns promise and state for completing flow. */ diff --git a/test/unit/remote/environment.test.ts b/test/unit/remote/environment.test.ts index 8476a8332b..ae31304e30 100644 --- a/test/unit/remote/environment.test.ts +++ b/test/unit/remote/environment.test.ts @@ -14,6 +14,8 @@ import { } from "../../mocks/testHelpers"; const proxyEnv = { HTTP_PROXY: proxy, HTTPS_PROXY: proxy }; +const TEST_SESSION_ID = "0123456789abcdef0123456789abcdef"; +const sessionEnv = { CODER_TRACE_SESSION_ID: TEST_SESSION_ID }; type Environment = Record; beforeEach(() => { @@ -108,11 +110,16 @@ describe("applySshEnvironment", () => { it("applies proxy variables to process.env and the collection, and restores on dispose", () => { const env: Environment = {}; const collection = fakeEnvCollection(); - const expected = { ...proxyEnv, NO_PROXY: "internal.example.com" }; + const expected = { + ...proxyEnv, + NO_PROXY: "internal.example.com", + ...sessionEnv, + }; const applied = applySshEnvironment( config(withProxy({ "coder.proxyBypass": "internal.example.com" })), collection, + TEST_SESSION_ID, env, ); @@ -125,14 +132,14 @@ describe("applySshEnvironment", () => { expect(collection.vars).toEqual({}); }); - it("sets nothing when no proxy is configured", () => { + it("sets the session ID even when no proxy is configured", () => { const env: Environment = {}; const collection = fakeEnvCollection(); - applySshEnvironment(config(), collection, env); + applySshEnvironment(config(), collection, TEST_SESSION_ID, env); - expect(env).toEqual({}); - expect(collection.vars).toEqual({}); + expect(env).toEqual(sessionEnv); + expect(collection.vars).toEqual(sessionEnv); }); it("does not clear existing env proxy variables when proxy support is off", () => { @@ -146,11 +153,12 @@ describe("applySshEnvironment", () => { applySshEnvironment( config(withProxy({ "http.proxySupport": "off" })), collection, + TEST_SESSION_ID, env, ); - expect(env).toEqual(original); - expect(collection.vars).toEqual({}); + expect(env).toEqual({ ...original, ...sessionEnv }); + expect(collection.vars).toEqual(sessionEnv); }); it("does not overwrite existing lowercase variables", () => { @@ -163,10 +171,11 @@ describe("applySshEnvironment", () => { const applied = applySshEnvironment( config(withProxy()), fakeEnvCollection(), + TEST_SESSION_ID, env, ); - expect(env).toEqual({ ...original, ...proxyEnv }); + expect(env).toEqual({ ...original, ...proxyEnv, ...sessionEnv }); applied.dispose(); expect(env).toEqual(original); @@ -179,6 +188,7 @@ describe("applySshEnvironment", () => { const applied = applySshEnvironment( config(withProxy()), fakeEnvCollection(), + TEST_SESSION_ID, env, ); expect(env.HTTP_PROXY).toBe(proxy); @@ -193,6 +203,7 @@ describe("applySshEnvironment", () => { const applied = applySshEnvironment( config(withProxy()), fakeEnvCollection(), + TEST_SESSION_ID, ); try { From 44c46b995e345ecce5937667330ba35c5cfb60b5 Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Thu, 13 Aug 2026 00:40:31 +0000 Subject: [PATCH 03/10] refactor(api): rename baggage key to client_session_id Align with the updated RFC: the session ID baggage key changes from session_id to client_session_id. --- src/api/coderApi.ts | 7 +++++-- test/unit/api/coderApi.test.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/api/coderApi.ts b/src/api/coderApi.ts index 15d3564c86..5931e6963d 100644 --- a/src/api/coderApi.ts +++ b/src/api/coderApi.ts @@ -80,6 +80,9 @@ const coderSessionTokenHeader = "Coder-Session-Token"; /** W3C baggage header used to propagate the session ID to the server. */ const baggageHeader = "baggage"; +/** Baggage key that carries the client's session ID. */ +const sessionIdBaggageKey = "client_session_id"; + /** * Default timeout for REST requests, so requests hung on half-open TCP * connections (e.g. after system sleep) don't stall pollers forever. @@ -157,7 +160,7 @@ export class CoderApi extends Api implements vscode.Disposable { client.getAxiosInstance().defaults.timeout = DEFAULT_REQUEST_TIMEOUT_MS; if (sessionId) { client.getAxiosInstance().defaults.headers.common[baggageHeader] = - `session_id=${sessionId}`; + `${sessionIdBaggageKey}=${sessionId}`; } client.setCredentials(baseUrl, token); @@ -392,7 +395,7 @@ export class CoderApi extends Api implements vscode.Disposable { const headers = { ...(token ? { [coderSessionTokenHeader]: token } : {}), ...(this.sessionId - ? { [baggageHeader]: `session_id=${this.sessionId}` } + ? { [baggageHeader]: `${sessionIdBaggageKey}=${this.sessionId}` } : {}), ...configs.options?.headers, ...headersFromCommand, diff --git a/test/unit/api/coderApi.test.ts b/test/unit/api/coderApi.test.ts index 57ec5e33d3..59b02b97a2 100644 --- a/test/unit/api/coderApi.test.ts +++ b/test/unit/api/coderApi.test.ts @@ -161,7 +161,7 @@ describe("CoderApi", () => { const response = await api.getAxiosInstance().get("/api/v2/users/me"); expect(response.config.headers["baggage"]).toBe( - `session_id=${sessionId}`, + `client_session_id=${sessionId}`, ); }); From 9c2997325422e69213189483f50699af3ad70758 Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Wed, 12 Aug 2026 17:43:08 -0700 Subject: [PATCH 04/10] docs: remove waffle --- src/api/coderApi.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api/coderApi.ts b/src/api/coderApi.ts index 5931e6963d..33c9c5faaf 100644 --- a/src/api/coderApi.ts +++ b/src/api/coderApi.ts @@ -80,7 +80,6 @@ const coderSessionTokenHeader = "Coder-Session-Token"; /** W3C baggage header used to propagate the session ID to the server. */ const baggageHeader = "baggage"; -/** Baggage key that carries the client's session ID. */ const sessionIdBaggageKey = "client_session_id"; /** From 243eef4dea7134e2dd1c3a7415eb5110615fd69a Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Thu, 13 Aug 2026 11:43:23 -0700 Subject: [PATCH 05/10] refactor: use constant case for session id baggage key --- src/api/coderApi.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/coderApi.ts b/src/api/coderApi.ts index 33c9c5faaf..ffb8b1b693 100644 --- a/src/api/coderApi.ts +++ b/src/api/coderApi.ts @@ -80,7 +80,7 @@ const coderSessionTokenHeader = "Coder-Session-Token"; /** W3C baggage header used to propagate the session ID to the server. */ const baggageHeader = "baggage"; -const sessionIdBaggageKey = "client_session_id"; +const SESSION_ID_BAGGAGE_KEY = "client_session_id"; /** * Default timeout for REST requests, so requests hung on half-open TCP @@ -159,7 +159,7 @@ export class CoderApi extends Api implements vscode.Disposable { client.getAxiosInstance().defaults.timeout = DEFAULT_REQUEST_TIMEOUT_MS; if (sessionId) { client.getAxiosInstance().defaults.headers.common[baggageHeader] = - `${sessionIdBaggageKey}=${sessionId}`; + `${SESSION_ID_BAGGAGE_KEY}=${sessionId}`; } client.setCredentials(baseUrl, token); @@ -394,7 +394,7 @@ export class CoderApi extends Api implements vscode.Disposable { const headers = { ...(token ? { [coderSessionTokenHeader]: token } : {}), ...(this.sessionId - ? { [baggageHeader]: `${sessionIdBaggageKey}=${this.sessionId}` } + ? { [baggageHeader]: `${SESSION_ID_BAGGAGE_KEY}=${this.sessionId}` } : {}), ...configs.options?.headers, ...headersFromCommand, From 858df85db310e7f0613e174fb9fea05d8fa7bcbe Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Thu, 13 Aug 2026 17:29:03 -0700 Subject: [PATCH 06/10] feat: make baggage header non-overridable --- src/api/coderApi.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/coderApi.ts b/src/api/coderApi.ts index ffb8b1b693..082c013746 100644 --- a/src/api/coderApi.ts +++ b/src/api/coderApi.ts @@ -393,11 +393,11 @@ export class CoderApi extends Api implements vscode.Disposable { */ const headers = { ...(token ? { [coderSessionTokenHeader]: token } : {}), + ...configs.options?.headers, + ...headersFromCommand, ...(this.sessionId ? { [baggageHeader]: `${SESSION_ID_BAGGAGE_KEY}=${this.sessionId}` } : {}), - ...configs.options?.headers, - ...headersFromCommand, }; const baseUrl = new URL(baseUrlRaw); From cdb8d50c594da4e442b9295c35343ac09ca125b2 Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Fri, 14 Aug 2026 00:45:11 +0000 Subject: [PATCH 07/10] refactor(core): expose session ID as a module constant Add core/sessionId.ts exporting a single session ID constant (generated once per activation) and remove newSessionId() from telemetry/ids.ts. Consumers now import the constant directly instead of threading it through constructors and CoderApi.create, so every CoderApi client attaches the client_session_id baggage header unconditionally. Addresses review feedback on #1074. --- src/api/coderApi.ts | 20 +++++++------------- src/core/container.ts | 15 +++------------ src/core/sessionId.ts | 11 +++++++++++ src/deployment/deploymentManager.ts | 10 +--------- src/extension.ts | 1 - src/login/loginCoordinator.ts | 18 ++---------------- src/oauth/authorizer.ts | 9 +-------- src/oauth/sessionManager.ts | 18 ++---------------- src/remote/environment.ts | 2 +- src/remote/remote.ts | 2 -- src/telemetry/ids.ts | 6 ------ test/mocks/testHelpers.ts | 1 - test/unit/api/coderApi.test.ts | 24 +++++++----------------- test/unit/login/loginCoordinator.test.ts | 1 - test/unit/oauth/authorizer.test.ts | 1 - test/unit/remote/environment.test.ts | 11 +++-------- 16 files changed, 38 insertions(+), 112 deletions(-) create mode 100644 src/core/sessionId.ts diff --git a/src/api/coderApi.ts b/src/api/coderApi.ts index 082c013746..a1853feeb9 100644 --- a/src/api/coderApi.ts +++ b/src/api/coderApi.ts @@ -12,6 +12,7 @@ import { CONFIG_CHANGE_DEBOUNCE_MS, watchConfigurationChanges, } from "../configWatcher"; +import { sessionId } from "../core/sessionId"; import { ClientCertificateError } from "../error/clientCertificateError"; import { toError } from "../error/errorUtils"; import { ServerCertificateError } from "../error/serverCertificateError"; @@ -124,7 +125,6 @@ export class CoderApi extends Api implements vscode.Disposable { private readonly telemetry: TelemetryReporter, private readonly httpRequestsTelemetry: HttpRequestsTelemetry, private readonly authConfigTracker: AuthConfigTracker, - private readonly sessionId: string | undefined, ) { super(); wrapWithValidation(this); @@ -136,16 +136,15 @@ export class CoderApi extends Api implements vscode.Disposable { * Automatically sets up logging interceptors, certificate handling, * HTTP request telemetry, and WebSocket connection telemetry. All * telemetry routes through the single reporter passed in (defaults to - * NOOP_TELEMETRY_REPORTER for throwaway clients). When a session ID is - * provided it is attached to every request via the `baggage` header so the - * server can correlate requests with the session's logs and telemetry. + * NOOP_TELEMETRY_REPORTER for throwaway clients). The session ID is + * attached to every request via the `baggage` header so the server can + * correlate requests with the session's logs and telemetry. */ static create( baseUrl: string, token: string | undefined, output: Logger, telemetry: TelemetryReporter = NOOP_TELEMETRY_REPORTER, - sessionId?: string, ): CoderApi { const httpRequestsTelemetry = new HttpRequestsTelemetry(telemetry); const authConfigTracker = new AuthConfigTracker(); @@ -154,13 +153,10 @@ export class CoderApi extends Api implements vscode.Disposable { telemetry, httpRequestsTelemetry, authConfigTracker, - sessionId, ); client.getAxiosInstance().defaults.timeout = DEFAULT_REQUEST_TIMEOUT_MS; - if (sessionId) { - client.getAxiosInstance().defaults.headers.common[baggageHeader] = - `${SESSION_ID_BAGGAGE_KEY}=${sessionId}`; - } + client.getAxiosInstance().defaults.headers.common[baggageHeader] = + `${SESSION_ID_BAGGAGE_KEY}=${sessionId}`; client.setCredentials(baseUrl, token); setupInterceptors(client, output, httpRequestsTelemetry, authConfigTracker); @@ -395,9 +391,7 @@ export class CoderApi extends Api implements vscode.Disposable { ...(token ? { [coderSessionTokenHeader]: token } : {}), ...configs.options?.headers, ...headersFromCommand, - ...(this.sessionId - ? { [baggageHeader]: `${SESSION_ID_BAGGAGE_KEY}=${this.sessionId}` } - : {}), + [baggageHeader]: `${SESSION_ID_BAGGAGE_KEY}=${sessionId}`, }; const baseUrl = new URL(baseUrlRaw); diff --git a/src/core/container.ts b/src/core/container.ts index 74c65032de..56cd0edf83 100644 --- a/src/core/container.ts +++ b/src/core/container.ts @@ -6,7 +6,6 @@ import { shortId } from "../logging/utils"; import { LoginCoordinator } from "../login/loginCoordinator"; import { OAuthCallback } from "../oauth/oauthCallback"; import { buildSession, extractExtensionVersion } from "../telemetry/event"; -import { newSessionId } from "../telemetry/ids"; import { TelemetryService } from "../telemetry/service"; import { LocalJsonlSink } from "../telemetry/sinks/localJsonlSink"; import { NetcheckPanelFactory } from "../webviews/netcheck/netcheckPanelFactory"; @@ -20,6 +19,7 @@ import { ContextManager } from "./contextManager"; import { MementoManager } from "./mementoManager"; import { PathResolver } from "./pathResolver"; import { SecretsManager } from "./secretsManager"; +import { sessionId } from "./sessionId"; import type { Logger } from "../logging/logger"; @@ -29,7 +29,6 @@ import type { Logger } from "../logging/logger"; */ export class ServiceContainer implements vscode.Disposable { private readonly outputChannel: vscode.LogOutputChannel; - private readonly sessionId: string; private readonly logger: Logger; private readonly pathResolver: PathResolver; private readonly mementoManager: MementoManager; @@ -49,12 +48,9 @@ export class ServiceContainer implements vscode.Disposable { this.outputChannel = vscode.window.createOutputChannel("Coder", { log: true, }); - // One session ID per activation, shared by logs, API requests, - // telemetry, and the CLI so all data for a session correlates. - this.sessionId = newSessionId(); this.logger = prefixLogger( this.outputChannel, - `[session ${shortId(this.sessionId)}]`, + `[session ${shortId(sessionId)}]`, ); this.pathResolver = new PathResolver( context.globalStorageUri.fsPath, @@ -69,7 +65,7 @@ export class ServiceContainer implements vscode.Disposable { const session = buildSession( extractExtensionVersion(context.extension.packageJSON), - this.sessionId, + sessionId, ); const localJsonlSink = LocalJsonlSink.start( { @@ -118,7 +114,6 @@ export class ServiceContainer implements vscode.Disposable { new AuthTelemetry(this.telemetryService), this.oauthCallback, context.extension.id, - this.sessionId, ); this.duplicateWorkspaceIpc = new DuplicateWorkspaceIpc( context.secrets, @@ -153,10 +148,6 @@ export class ServiceContainer implements vscode.Disposable { return this.logger; } - getSessionId(): string { - return this.sessionId; - } - getCliManager(): CliManager { return this.cliManager; } diff --git a/src/core/sessionId.ts b/src/core/sessionId.ts new file mode 100644 index 0000000000..5a131b3273 --- /dev/null +++ b/src/core/sessionId.ts @@ -0,0 +1,11 @@ +import { randomBytes } from "node:crypto"; + +/** + * One session ID per activation, shared by logs, API requests, telemetry, and + * the CLI so all data for a session can be correlated by a single ID. + * + * 16 bytes / 32 lowercase hex, matching the OTel id format so a future OTel + * exporter maps 1:1. Avoids `vscode.env.sessionId`, which is a UUID + * concatenated with a timestamp. + */ +export const sessionId = randomBytes(16).toString("hex"); diff --git a/src/deployment/deploymentManager.ts b/src/deployment/deploymentManager.ts index a17f137225..7c0c47e3e2 100644 --- a/src/deployment/deploymentManager.ts +++ b/src/deployment/deploymentManager.ts @@ -49,7 +49,6 @@ export class DeploymentManager implements vscode.Disposable { private readonly logger: Logger; private readonly telemetryService: TelemetryService; private readonly deploymentTelemetry: DeploymentTelemetry; - private readonly sessionId: string; readonly #sessionStore = new SessionStore(); #disposed = false; @@ -70,7 +69,6 @@ export class DeploymentManager implements vscode.Disposable { this.logger = serviceContainer.getLogger(); this.telemetryService = serviceContainer.getTelemetryService(); this.deploymentTelemetry = new DeploymentTelemetry(this.telemetryService); - this.sessionId = serviceContainer.getSessionId(); } public static create( @@ -143,13 +141,7 @@ export class DeploymentManager implements vscode.Disposable { url: string, token: string | undefined, ): Promise { - const tempClient = CoderApi.create( - url, - token, - this.logger, - undefined, - this.sessionId, - ); + const tempClient = CoderApi.create(url, token, this.logger); try { return await tempClient.getAuthenticatedUser(); } finally { diff --git a/src/extension.ts b/src/extension.ts index dae226b28d..6dc82209e0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -141,7 +141,6 @@ async function doActivate( deploymentSessionAuth?.token, output, telemetryService, - serviceContainer.getSessionId(), ); ctx.subscriptions.push(client); diff --git a/src/login/loginCoordinator.ts b/src/login/loginCoordinator.ts index 457e868ab2..15c4fd93f2 100644 --- a/src/login/loginCoordinator.ts +++ b/src/login/loginCoordinator.ts @@ -91,14 +91,12 @@ export class LoginCoordinator implements vscode.Disposable { private readonly authTelemetry: AuthTelemetry, oauthCallback: OAuthCallback, extensionId: string, - private readonly sessionId: string, ) { this.oauthAuthorizer = new OAuthAuthorizer( secretsManager, oauthCallback, logger, extensionId, - sessionId, ); } @@ -250,13 +248,7 @@ export class LoginCoordinator implements vscode.Disposable { safeHostname, async (auth) => { if (auth?.token) { - const client = CoderApi.create( - auth.url, - auth.token, - this.logger, - undefined, - this.sessionId, - ); + const client = CoderApi.create(auth.url, auth.token, this.logger); try { const user = await client.getAuthenticatedUser(); // Stop listening only on success; a bad token shouldn't @@ -292,13 +284,7 @@ export class LoginCoordinator implements vscode.Disposable { providedToken?: string, tokenSignInConfirmed = false, ): Promise { - const client = CoderApi.create( - deployment.url, - "", - this.logger, - undefined, - this.sessionId, - ); + const client = CoderApi.create(deployment.url, "", this.logger); try { return await this.runLoginAttempts( client, diff --git a/src/oauth/authorizer.ts b/src/oauth/authorizer.ts index 0420bbba46..b22bb90a70 100644 --- a/src/oauth/authorizer.ts +++ b/src/oauth/authorizer.ts @@ -51,7 +51,6 @@ export class OAuthAuthorizer implements vscode.Disposable { private readonly oauthCallback: OAuthCallback, private readonly logger: Logger, private readonly extensionId: string, - private readonly sessionId: string, ) {} /** @@ -64,13 +63,7 @@ export class OAuthAuthorizer implements vscode.Disposable { progress: vscode.Progress<{ message?: string; increment?: number }>, cancellationToken: vscode.CancellationToken, ): Promise<{ tokenResponse: OAuth2TokenResponse; user: User }> { - const client = CoderApi.create( - deployment.url, - undefined, - this.logger, - undefined, - this.sessionId, - ); + const client = CoderApi.create(deployment.url, undefined, this.logger); try { return await this.runLoginFlow( client, diff --git a/src/oauth/sessionManager.ts b/src/oauth/sessionManager.ts index baacf2b09f..5ccc122aa5 100644 --- a/src/oauth/sessionManager.ts +++ b/src/oauth/sessionManager.ts @@ -70,7 +70,6 @@ export class OAuthSessionManager implements vscode.Disposable { container.getLogger(), onAuthRequired, new AuthTelemetry(container.getTelemetryService()), - container.getSessionId(), ); manager.setupTokenListener(); manager.scheduleNextRefresh(); @@ -83,7 +82,6 @@ export class OAuthSessionManager implements vscode.Disposable { private readonly logger: Logger, private readonly onAuthRequired: () => Promise, private readonly authTelemetry: AuthTelemetry, - private readonly sessionId: string, ) {} /** @@ -301,13 +299,7 @@ export class OAuthSessionManager implements vscode.Disposable { }) => Promise, ): Promise { const deployment = this.requireDeployment(); - const client = CoderApi.create( - deployment.url, - token, - this.logger, - undefined, - this.sessionId, - ); + const client = CoderApi.create(deployment.url, token, this.logger); try { const axiosInstance = client.getAxiosInstance(); const metadataClient = new OAuthMetadataClient( @@ -467,13 +459,7 @@ export class OAuthSessionManager implements vscode.Disposable { deployment: Deployment, accessToken: string, ): Promise { - const client = CoderApi.create( - deployment.url, - accessToken, - this.logger, - undefined, - this.sessionId, - ); + const client = CoderApi.create(deployment.url, accessToken, this.logger); try { return (await client.getAuthenticatedUser()).username; } catch (error) { diff --git a/src/remote/environment.ts b/src/remote/environment.ts index 4c913c13f5..29a0d4c8d9 100644 --- a/src/remote/environment.ts +++ b/src/remote/environment.ts @@ -1,4 +1,5 @@ import { joinNoProxy } from "../api/proxy"; +import { sessionId } from "../core/sessionId"; import type { GlobalEnvironmentVariableCollection, @@ -41,7 +42,6 @@ export function applySshEnvironment( GlobalEnvironmentVariableCollection, "persistent" | "replace" | "clear" >, - sessionId: string, env: Environment = process.env, ): { dispose(): void } { const values: Environment = { diff --git a/src/remote/remote.ts b/src/remote/remote.ts index 38a5741dd1..0c29a0f5da 100644 --- a/src/remote/remote.ts +++ b/src/remote/remote.ts @@ -231,7 +231,6 @@ export class Remote { applySshEnvironment( vscode.workspace.getConfiguration(), this.extensionContext.environmentVariableCollection, - this.serviceContainer.getSessionId(), ), ); // Create OAuth session manager for this remote deployment @@ -257,7 +256,6 @@ export class Remote { token, this.logger, this.serviceContainer.getTelemetryService(), - this.serviceContainer.getSessionId(), ); disposables.push(workspaceClient); diff --git a/src/telemetry/ids.ts b/src/telemetry/ids.ts index 7de0be12e5..486eeb61fa 100644 --- a/src/telemetry/ids.ts +++ b/src/telemetry/ids.ts @@ -12,9 +12,3 @@ export function newTraceId(): string { export function newSpanId(): string { return randomBytes(8).toString("hex"); } - -/** Our own session id (16 bytes / 32 hex). Avoids `vscode.env.sessionId`, - * which is a UUID concatenated with a timestamp. */ -export function newSessionId(): string { - return randomBytes(16).toString("hex"); -} diff --git a/test/mocks/testHelpers.ts b/test/mocks/testHelpers.ts index 9de5693afd..f89a0a3162 100644 --- a/test/mocks/testHelpers.ts +++ b/test/mocks/testHelpers.ts @@ -595,7 +595,6 @@ export function createMockServiceContainer( return { getTelemetryService: () => telemetry, getLogger: () => logger, - getSessionId: () => "0123456789abcdef0123456789abcdef", getSecretsManager: () => require("secretsManager", overrides.secretsManager), getMementoManager: () => diff --git a/test/unit/api/coderApi.test.ts b/test/unit/api/coderApi.test.ts index 59b02b97a2..b3eca3fc48 100644 --- a/test/unit/api/coderApi.test.ts +++ b/test/unit/api/coderApi.test.ts @@ -29,6 +29,7 @@ import { } from "@/api/responseValidation"; import { createHttpAgent } from "@/api/utils"; import { CONFIG_CHANGE_DEBOUNCE_MS } from "@/configWatcher"; +import { sessionId } from "@/core/sessionId"; import { ClientCertificateError } from "@/error/clientCertificateError"; import { ServerCertificateError } from "@/error/serverCertificateError"; import { getHeaders } from "@/headers"; @@ -148,15 +149,8 @@ describe("CoderApi", () => { ); }); - it("attaches the session ID to requests as a baggage header", async () => { - const sessionId = "0123456789abcdef0123456789abcdef"; - api = CoderApi.create( - CODER_URL, - AXIOS_TOKEN, - mockLogger, - NOOP_TELEMETRY_REPORTER, - sessionId, - ); + it("attaches the session ID to every request as a baggage header", async () => { + api = createApi(); const response = await api.getAxiosInstance().get("/api/v2/users/me"); @@ -165,14 +159,6 @@ describe("CoderApi", () => { ); }); - it("omits the baggage header when no session ID is provided", async () => { - api = createApi(); - - const response = await api.getAxiosInstance().get("/api/v2/users/me"); - - expect(response.config.headers["baggage"]).toBeUndefined(); - }); - it("applies the default timeout to requests", async () => { api = createApi(); const response = await api.getAxiosInstance().get("/api/v2/users/me"); @@ -498,6 +484,7 @@ describe("CoderApi", () => { headers: { "X-Custom-Header": "custom-value", "Coder-Session-Token": AXIOS_TOKEN, + baggage: `client_session_id=${sessionId}`, }, }); }); @@ -511,6 +498,7 @@ describe("CoderApi", () => { followRedirects: true, headers: { "Coder-Session-Token": AXIOS_TOKEN, + baggage: `client_session_id=${sessionId}`, }, }); @@ -528,6 +516,7 @@ describe("CoderApi", () => { headers: { "Coder-Session-Token": "from-config", "X-Config-Header": "config-value", + baggage: `client_session_id=${sessionId}`, }, }); @@ -547,6 +536,7 @@ describe("CoderApi", () => { followRedirects: true, headers: { "Coder-Session-Token": "from-header-command", + baggage: `client_session_id=${sessionId}`, }, }); }); diff --git a/test/unit/login/loginCoordinator.test.ts b/test/unit/login/loginCoordinator.test.ts index 00a9038fb0..44bfbb7982 100644 --- a/test/unit/login/loginCoordinator.test.ts +++ b/test/unit/login/loginCoordinator.test.ts @@ -142,7 +142,6 @@ function createTestContext(telemetry?: TelemetryService) { authTelemetry, oauthCallback, "coder.coder-remote", - "0123456789abcdef0123456789abcdef", ); const mockSuccessfulAuth = (user = createMockUser()) => { diff --git a/test/unit/oauth/authorizer.test.ts b/test/unit/oauth/authorizer.test.ts index b4c5fb6e0f..2f95f7ae8c 100644 --- a/test/unit/oauth/authorizer.test.ts +++ b/test/unit/oauth/authorizer.test.ts @@ -68,7 +68,6 @@ function createTestContext() { base.oauthCallback, base.logger, EXTENSION_ID, - "0123456789abcdef0123456789abcdef", ); /** Starts login flow and waits for browser to open. Returns promise and state for completing flow. */ diff --git a/test/unit/remote/environment.test.ts b/test/unit/remote/environment.test.ts index ae31304e30..c616e92f62 100644 --- a/test/unit/remote/environment.test.ts +++ b/test/unit/remote/environment.test.ts @@ -1,6 +1,7 @@ import { spawnSync } from "node:child_process"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { sessionId } from "@/core/sessionId"; import { applySshEnvironment, getSshProxyEnvironment, @@ -14,8 +15,7 @@ import { } from "../../mocks/testHelpers"; const proxyEnv = { HTTP_PROXY: proxy, HTTPS_PROXY: proxy }; -const TEST_SESSION_ID = "0123456789abcdef0123456789abcdef"; -const sessionEnv = { CODER_TRACE_SESSION_ID: TEST_SESSION_ID }; +const sessionEnv = { CODER_TRACE_SESSION_ID: sessionId }; type Environment = Record; beforeEach(() => { @@ -119,7 +119,6 @@ describe("applySshEnvironment", () => { const applied = applySshEnvironment( config(withProxy({ "coder.proxyBypass": "internal.example.com" })), collection, - TEST_SESSION_ID, env, ); @@ -136,7 +135,7 @@ describe("applySshEnvironment", () => { const env: Environment = {}; const collection = fakeEnvCollection(); - applySshEnvironment(config(), collection, TEST_SESSION_ID, env); + applySshEnvironment(config(), collection, env); expect(env).toEqual(sessionEnv); expect(collection.vars).toEqual(sessionEnv); @@ -153,7 +152,6 @@ describe("applySshEnvironment", () => { applySshEnvironment( config(withProxy({ "http.proxySupport": "off" })), collection, - TEST_SESSION_ID, env, ); @@ -171,7 +169,6 @@ describe("applySshEnvironment", () => { const applied = applySshEnvironment( config(withProxy()), fakeEnvCollection(), - TEST_SESSION_ID, env, ); @@ -188,7 +185,6 @@ describe("applySshEnvironment", () => { const applied = applySshEnvironment( config(withProxy()), fakeEnvCollection(), - TEST_SESSION_ID, env, ); expect(env.HTTP_PROXY).toBe(proxy); @@ -203,7 +199,6 @@ describe("applySshEnvironment", () => { const applied = applySshEnvironment( config(withProxy()), fakeEnvCollection(), - TEST_SESSION_ID, ); try { From 2c72a3734f97e3da9e6b77c30b7a37aff7c0820f Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Thu, 13 Aug 2026 17:59:38 -0700 Subject: [PATCH 08/10] refactor: rename SshEnvironment to SshProxyEnvironment --- src/remote/environment.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/remote/environment.ts b/src/remote/environment.ts index 29a0d4c8d9..15bf1f40fe 100644 --- a/src/remote/environment.ts +++ b/src/remote/environment.ts @@ -7,7 +7,7 @@ import type { } from "vscode"; type Environment = Record; -type SshEnvironment = Partial< +type SshProxyEnvironment = Partial< Record<"HTTP_PROXY" | "HTTPS_PROXY" | "NO_PROXY", string> >; @@ -70,7 +70,7 @@ export function applySshEnvironment( /** The proxy portion of the SSH environment, derived from VS Code's settings. */ export function getSshProxyEnvironment( cfg: Pick, -): SshEnvironment { +): SshProxyEnvironment { if (cfg.get("http.proxySupport") === "off") { return {}; } From 0b01f27a6a03fe336ac6080e96715642face72e3 Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Mon, 17 Aug 2026 13:34:35 -0700 Subject: [PATCH 09/10] refactor: extract SESSION_ID_BAGGAGE string variable --- src/api/coderApi.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/api/coderApi.ts b/src/api/coderApi.ts index a1853feeb9..c401cb9e46 100644 --- a/src/api/coderApi.ts +++ b/src/api/coderApi.ts @@ -79,9 +79,9 @@ import type { const coderSessionTokenHeader = "Coder-Session-Token"; /** W3C baggage header used to propagate the session ID to the server. */ -const baggageHeader = "baggage"; - +const BAGGAGE_HEADER = "baggage"; const SESSION_ID_BAGGAGE_KEY = "client_session_id"; +const SESSION_ID_BAGGAGE = `${SESSION_ID_BAGGAGE_KEY}=${sessionId}`; /** * Default timeout for REST requests, so requests hung on half-open TCP @@ -155,8 +155,8 @@ export class CoderApi extends Api implements vscode.Disposable { authConfigTracker, ); client.getAxiosInstance().defaults.timeout = DEFAULT_REQUEST_TIMEOUT_MS; - client.getAxiosInstance().defaults.headers.common[baggageHeader] = - `${SESSION_ID_BAGGAGE_KEY}=${sessionId}`; + client.getAxiosInstance().defaults.headers.common[BAGGAGE_HEADER] = + SESSION_ID_BAGGAGE; client.setCredentials(baseUrl, token); setupInterceptors(client, output, httpRequestsTelemetry, authConfigTracker); @@ -391,7 +391,7 @@ export class CoderApi extends Api implements vscode.Disposable { ...(token ? { [coderSessionTokenHeader]: token } : {}), ...configs.options?.headers, ...headersFromCommand, - [baggageHeader]: `${SESSION_ID_BAGGAGE_KEY}=${sessionId}`, + [BAGGAGE_HEADER]: SESSION_ID_BAGGAGE, }; const baseUrl = new URL(baseUrlRaw); From 26b957f9c8feee1d68906ffddc6037dc6d46d3d9 Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Mon, 17 Aug 2026 13:41:35 -0700 Subject: [PATCH 10/10] docs: specify that sessionId is associated with extension host process, not activation --- src/core/sessionId.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/core/sessionId.ts b/src/core/sessionId.ts index 5a131b3273..44bd804660 100644 --- a/src/core/sessionId.ts +++ b/src/core/sessionId.ts @@ -1,8 +1,13 @@ import { randomBytes } from "node:crypto"; /** - * One session ID per activation, shared by logs, API requests, telemetry, and - * the CLI so all data for a session can be correlated by a single ID. + * One session ID per extension host process, shared across logs, API + * requests, telemetry, and the CLI so all data for a session can be correlated + * by a single ID. + * + * In rare cases when the extension is deactivated then activated within the + * same window, it should reuse the same ID. However, reloading or closing- + * then-reopening a window creates a new process with a new ID. * * 16 bytes / 32 lowercase hex, matching the OTel id format so a future OTel * exporter maps 1:1. Avoids `vscode.env.sessionId`, which is a UUID