From bef018373859d241ab8a1ed1983191345a21ffca Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Wed, 6 May 2026 12:42:27 -0400 Subject: [PATCH] fix(vscode): close runtime telemetry consent gap with the CLI The previous `watchTelemetryState` fix updated the webview UI in real time but left the CLI subprocess's PostHog client stuck on its spawn-time `KILO_TELEMETRY_LEVEL` value. A user who started VS Code with telemetry off and toggled it on at runtime saw the thumbs UI appear (good) but every webview event was silently dropped at the CLI's `Client.capture()` gate (bad). Add a runtime sync channel: - New `POST /telemetry/setEnabled` Hono route on the CLI server that calls `Telemetry.setEnabled(enabled)` to flip the `posthog-node` client's opt state. - New `TelemetryProxy.setEnabled(enabled)` method that POSTs to it, using the same fire-and-forget pattern as `capture`. - Extension calls `telemetry.setEnabled(vscode.env.isTelemetryEnabled)` immediately after `telemetry.configure(...)` on every `connected` state change, so a freshly-spawned CLI gets corrected even when its spawn-time env var is stale. - Extension subscribes to `vscode.env.onDidChangeTelemetryEnabled` to forward runtime consent changes to the CLI as they happen. All three changes live in Kilo-owned files (the route is already a `kilocode_change - new file`, and the extension is Kilo-only). Zero upstream OpenCode merge surface. Closes #9872 fully (the previous `d67c5e307c` covered only the webview UI). --- packages/kilo-vscode/src/extension.ts | 13 +++ .../src/services/telemetry/telemetry-proxy.ts | 20 +++++ .../src/server/routes/instance/telemetry.ts | 89 ++++++++++++------- packages/sdk/js/src/v2/gen/sdk.gen.ts | 41 +++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 30 +++++++ 5 files changed, 163 insertions(+), 30 deletions(-) diff --git a/packages/kilo-vscode/src/extension.ts b/packages/kilo-vscode/src/extension.ts index b00c64ecf9..2ba3759cb5 100644 --- a/packages/kilo-vscode/src/extension.ts +++ b/packages/kilo-vscode/src/extension.ts @@ -57,6 +57,11 @@ export function activate(context: vscode.ExtensionContext) { const config = connectionService.getServerConfig() if (config) { telemetry.configure(config.baseUrl, config.password) + // Sync the CLI's PostHog client with the current consent state. The + // CLI reads KILO_TELEMETRY_LEVEL once at spawn, so without this call + // a fresh CLI started while VS Code telemetry was off would stay + // opted out for the rest of the session. + telemetry.setEnabled(vscode.env.isTelemetryEnabled) } try { remoteService.setClient(connectionService.getClient()) @@ -72,6 +77,14 @@ export function activate(context: vscode.ExtensionContext) { } }) + // Propagate runtime telemetry consent changes to the CLI subprocess so its + // PostHog client stays in sync with the user's VS Code telemetry setting. + context.subscriptions.push( + vscode.env.onDidChangeTelemetryEnabled((enabled) => { + telemetry.setEnabled(enabled) + }), + ) + // Prewarm the CLI backend early so autocomplete is ready before first editor use. ensureBackendForAutocomplete(connectionService) diff --git a/packages/kilo-vscode/src/services/telemetry/telemetry-proxy.ts b/packages/kilo-vscode/src/services/telemetry/telemetry-proxy.ts index 410568378c..2c85e931f3 100644 --- a/packages/kilo-vscode/src/services/telemetry/telemetry-proxy.ts +++ b/packages/kilo-vscode/src/services/telemetry/telemetry-proxy.ts @@ -61,6 +61,26 @@ export class TelemetryProxy { }).catch((err) => console.error("[Kilo New] Telemetry capture failed:", err)) } + /** + * Propagate runtime telemetry consent changes to the CLI. The CLI subprocess + * reads `KILO_TELEMETRY_LEVEL` once at spawn — without this call, toggling + * VS Code telemetry consent leaves the CLI's PostHog client stuck on its + * spawn-time state until the process restarts. + */ + setEnabled(enabled: boolean) { + if (!this.url || !this.password) return + + const auth = buildTelemetryAuthHeader(this.password) + fetch(`${this.url}/telemetry/setEnabled`, { + method: "POST", + headers: { + Authorization: auth, + "Content-Type": "application/json", + }, + body: JSON.stringify({ enabled }), + }).catch((err) => console.error("[Kilo New] Telemetry setEnabled failed:", err)) + } + /** * No-op — the CLI server handles PostHog shutdown. */ diff --git a/packages/opencode/src/server/routes/instance/telemetry.ts b/packages/opencode/src/server/routes/instance/telemetry.ts index d6fd1577e0..245a1c4124 100644 --- a/packages/opencode/src/server/routes/instance/telemetry.ts +++ b/packages/opencode/src/server/routes/instance/telemetry.ts @@ -7,39 +7,68 @@ import { lazy } from "@/util/lazy" import { errors } from "../../error" export const TelemetryRoutes = lazy(() => - new Hono().post( - "/capture", - describeRoute({ - summary: "Capture telemetry event", - description: "Forward a telemetry event to PostHog via kilo-telemetry.", - operationId: "telemetry.capture", - responses: { - 200: { - description: "Event captured", - content: { - "application/json": { - schema: resolver(z.boolean()), + new Hono() + .post( + "/capture", + describeRoute({ + summary: "Capture telemetry event", + description: "Forward a telemetry event to PostHog via kilo-telemetry.", + operationId: "telemetry.capture", + responses: { + 200: { + description: "Event captured", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, }, }, + ...errors(400), }, - ...errors(400), - }, - }), - validator( - "json", - z.object({ - event: z.string().meta({ description: "Event name" }), - properties: z.record(z.string(), z.any()).optional().meta({ description: "Event properties" }), }), + validator( + "json", + z.object({ + event: z.string().meta({ description: "Event name" }), + properties: z.record(z.string(), z.any()).optional().meta({ description: "Event properties" }), + }), + ), + async (c) => { + const body = c.req.valid("json") + try { + Telemetry.track(body.event as any, body.properties) + } catch { + // fire-and-forget: swallow errors + } + return c.json(true) + }, + ) + .post( + "/setEnabled", + describeRoute({ + summary: "Set PostHog telemetry enabled state", + description: + "Update the PostHog client's opt-in/out state at runtime. " + + "The CLI reads KILO_TELEMETRY_LEVEL once at spawn — this route lets clients " + + "(e.g. the VS Code extension) propagate runtime telemetry consent changes.", + operationId: "telemetry.setEnabled", + responses: { + 200: { + description: "State updated", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + ...errors(400), + }, + }), + validator("json", z.object({ enabled: z.boolean() })), + async (c) => { + const body = c.req.valid("json") + Telemetry.setEnabled(body.enabled) + return c.json(true) + }, ), - async (c) => { - const body = c.req.valid("json") - try { - Telemetry.track(body.event as any, body.properties) - } catch { - // fire-and-forget: swallow errors - } - return c.json(true) - }, - ), ) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 9f35908ad3..e4473d9e32 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -221,6 +221,8 @@ import type { SyncStartResponses, TelemetryCaptureErrors, TelemetryCaptureResponses, + TelemetrySetEnabledErrors, + TelemetrySetEnabledResponses, TextPartInput, ToolIdsErrors, ToolIdsResponses, @@ -4936,6 +4938,45 @@ export class Telemetry extends HeyApiClient { }, }) } + + /** + * Set PostHog telemetry enabled state + * + * Update the PostHog client's opt-in/out state at runtime. The CLI reads KILO_TELEMETRY_LEVEL once at spawn — this route lets clients (e.g. the VS Code extension) propagate runtime telemetry consent changes. + */ + public setEnabled( + parameters?: { + directory?: string + workspace?: string + enabled?: boolean + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "enabled" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post( + { + url: "/telemetry/setEnabled", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }, + ) + } } export class Remote extends HeyApiClient { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 1d260546e2..6e0ded613d 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -6465,6 +6465,36 @@ export type TelemetryCaptureResponses = { export type TelemetryCaptureResponse = TelemetryCaptureResponses[keyof TelemetryCaptureResponses] +export type TelemetrySetEnabledData = { + body?: { + enabled: boolean + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/telemetry/setEnabled" +} + +export type TelemetrySetEnabledErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TelemetrySetEnabledError = TelemetrySetEnabledErrors[keyof TelemetrySetEnabledErrors] + +export type TelemetrySetEnabledResponses = { + /** + * State updated + */ + 200: boolean +} + +export type TelemetrySetEnabledResponse = TelemetrySetEnabledResponses[keyof TelemetrySetEnabledResponses] + export type RemoteEnableData = { body?: never path?: never