Skip to content
This repository was archived by the owner on Feb 25, 2026. It is now read-only.

Commit 67107e5

Browse files
author
Mark IJbema
authored
Merge pull request #410 from Kilo-Org/mark/telemetry-implementation
feat: implement proxy-based telemetry for VS Code extension
2 parents e553be6 + f2f7baf commit 67107e5

25 files changed

Lines changed: 480 additions & 81 deletions

File tree

‎AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ We regularly merge upstream changes from opencode. To minimize merge conflicts a
153153
2. **Minimize changes to shared files** - When you must modify files that exist in upstream opencode, keep changes as small and isolated as possible.
154154

155155
3. **Use `kilocode_change` markers** - When modifying shared code, mark your changes with `kilocode_change` comments so they can be easily identified during merges.
156+
Do not use these markers in files within directories with kilo in the name
156157

157158
4. **Avoid restructuring upstream code** - Don't refactor or reorganize code that comes from opencode unless absolutely necessary.
158159

‎packages/kilo-telemetry/src/__tests__/telemetry.test.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,11 @@ describe("PostHogSpanExporter", () => {
114114
}
115115

116116
test("export returns success when disabled", () => {
117-
const exporter = new PostHogSpanExporter(createMockPostHogClient())
117+
const exporter = new PostHogSpanExporter(createMockPostHogClient(), {
118+
appName: "test",
119+
appVersion: "1.0.0",
120+
platform: "test",
121+
})
118122
exporter.setEnabled(false)
119123

120124
const span = createMockSpan("ai.generateText", { "ai.model.id": "gpt-4" })
@@ -131,7 +135,11 @@ describe("PostHogSpanExporter", () => {
131135
test("sensitive attributes are not included in exported properties", () => {
132136
// This test verifies the filtering logic by checking the SENSITIVE_ATTRIBUTES set
133137
// and the mapAttributes method behavior through the export function
134-
const exporter = new PostHogSpanExporter(createMockPostHogClient())
138+
const exporter = new PostHogSpanExporter(createMockPostHogClient(), {
139+
appName: "test",
140+
appVersion: "1.0.0",
141+
platform: "test",
142+
})
135143

136144
// Create a span with both safe and sensitive attributes
137145
const span = createMockSpan("ai.generateText", {

‎packages/kilo-telemetry/src/identity.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ export namespace Identity {
1414
export async function getMachineId(): Promise<string> {
1515
if (machineId) return machineId
1616

17+
const override = process.env.KILO_MACHINE_ID
18+
if (override) {
19+
machineId = override
20+
return machineId
21+
}
22+
1723
const filepath = path.join(dataPath, "telemetry-id")
1824
const file = Bun.file(filepath)
1925

‎packages/kilo-telemetry/src/otel-exporter.ts‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,22 @@ const SENSITIVE_ATTRIBUTES = new Set([
3838
export class PostHogSpanExporter implements SpanExporter {
3939
private client: PostHog
4040
private enabled = true
41-
private appName = "kilo-cli"
42-
private appVersion = "unknown"
43-
44-
constructor(client: PostHog, options?: { appVersion?: string }) {
41+
private appName: string
42+
private appVersion: string
43+
private platform: string
44+
private editorName?: string
45+
private vscodeVersion?: string
46+
47+
constructor(
48+
client: PostHog,
49+
options: { appName: string; appVersion: string; platform: string; editorName?: string; vscodeVersion?: string },
50+
) {
4551
this.client = client
46-
if (options?.appVersion) {
47-
this.appVersion = options.appVersion
48-
}
52+
this.appName = options.appName
53+
this.appVersion = options.appVersion
54+
this.platform = options.platform
55+
this.editorName = options.editorName
56+
this.vscodeVersion = options.vscodeVersion
4957
}
5058

5159
setEnabled(value: boolean) {
@@ -82,7 +90,9 @@ export class PostHogSpanExporter implements SpanExporter {
8290
const properties: Record<string, unknown> = {
8391
appName: this.appName,
8492
appVersion: this.appVersion,
85-
platform: process.platform,
93+
platform: this.platform,
94+
...(this.editorName && { editorName: this.editorName }),
95+
...(this.vscodeVersion && { vscodeVersion: this.vscodeVersion }),
8696
$ai_trace_id: span.spanContext().traceId,
8797
$ai_span_id: span.spanContext().spanId,
8898
$ai_span_name: name,

‎packages/kilo-telemetry/src/telemetry.ts‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export interface TelemetryProperties {
88
appName: string
99
appVersion: string
1010
platform: string
11+
editorName?: string
12+
vscodeVersion?: string
1113
}
1214

1315
export namespace Telemetry {
@@ -25,11 +27,32 @@ export namespace Telemetry {
2527
Identity.setDataPath(options.dataPath)
2628
props.appVersion = options.version
2729

30+
const app = process.env.KILO_APP_NAME
31+
if (app) props.appName = app
32+
const editor = process.env.KILO_EDITOR_NAME
33+
if (editor) props.editorName = editor
34+
const platform = process.env.KILO_PLATFORM
35+
if (platform) props.platform = platform
36+
const version = process.env.KILO_APP_VERSION
37+
if (version) props.appVersion = version
38+
const vscodeVersion = process.env.KILO_VSCODE_VERSION
39+
if (vscodeVersion) props.vscodeVersion = vscodeVersion
40+
2841
Client.init()
29-
Client.setEnabled(options.enabled)
42+
43+
const level = process.env.KILO_TELEMETRY_LEVEL
44+
const enabled = level ? level === "all" : options.enabled
45+
Client.setEnabled(enabled)
3046

3147
// Initialize OpenTelemetry tracer for AI SDK spans
32-
TracerSetup.init({ version: options.version, enabled: options.enabled })
48+
TracerSetup.init({
49+
version: props.appVersion,
50+
enabled,
51+
appName: props.appName,
52+
platform: props.platform,
53+
editorName: props.editorName,
54+
vscodeVersion: props.vscodeVersion,
55+
})
3356

3457
await Identity.getMachineId()
3558

‎packages/kilo-telemetry/src/tracer.ts‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,33 @@ let exporter: PostHogSpanExporter | null = null
1111
let tracer: Tracer | null = null
1212

1313
export namespace TracerSetup {
14-
export function init(options: { version: string; enabled: boolean }): Tracer {
14+
export function init(options: {
15+
version: string
16+
enabled: boolean
17+
appName: string
18+
platform: string
19+
editorName?: string
20+
vscodeVersion?: string
21+
}): Tracer {
1522
if (tracer) return tracer
1623

1724
const client = Client.getClient()
1825
if (!client) {
1926
throw new Error("PostHog client not initialized. Call Client.init() first.")
2027
}
2128

22-
exporter = new PostHogSpanExporter(client, { appVersion: options.version })
29+
exporter = new PostHogSpanExporter(client, {
30+
appName: options.appName,
31+
appVersion: options.version,
32+
platform: options.platform,
33+
editorName: options.editorName,
34+
vscodeVersion: options.vscodeVersion,
35+
})
2336
exporter.setEnabled(options.enabled)
2437

2538
provider = new NodeTracerProvider({
2639
resource: new Resource({
27-
[ATTR_SERVICE_NAME]: "kilo-cli",
40+
[ATTR_SERVICE_NAME]: options.appName,
2841
[ATTR_SERVICE_VERSION]: options.version,
2942
}),
3043
spanProcessors: [new SimpleSpanProcessor(exporter)],
@@ -34,7 +47,7 @@ export namespace TracerSetup {
3447
provider.register()
3548

3649
// Get tracer from our provider
37-
tracer = provider.getTracer("kilo-cli", options.version)
50+
tracer = provider.getTracer(options.appName, options.version)
3851

3952
return tracer
4053
}

‎packages/kilo-vscode/docs/non-agent-features/telemetry.md‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,12 @@ All events are defined in `TelemetryEventName` enum (`packages/types/src/telemet
111111

112112
### 3.1 Task Lifecycle
113113

114-
| Event | Properties | Capture Method |
115-
| ---------------------- | ----------------------------------- | ------------------------------ |
116-
| `Task Created` | `taskId` | `captureTaskCreated()` |
117-
| `Task Reopened` | `taskId` | `captureTaskRestarted()` |
118-
| `Task Completed` | `taskId` | `captureTaskCompleted()` |
119-
| `Conversation Message` | `taskId`, `source` (user/assistant) | `captureConversationMessage()` |
114+
| Event | Properties | Capture Method |
115+
| ---------------------- | ----------------------------------- | ----------------------------------------------------------- |
116+
| `Task Created` | `taskId` | `TelemetryProxy.capture(TelemetryEventName.TASK_CREATED)` |
117+
| `Task Reopened` | `taskId` | `TelemetryProxy.capture(TelemetryEventName.TASK_RESTARTED)` |
118+
| `Task Completed` | `taskId` | `TelemetryProxy.capture(TelemetryEventName.TASK_COMPLETED)` |
119+
| `Conversation Message` | `taskId`, `source` (user/assistant) | `TelemetryProxy.capture(TelemetryEventName.TASK_CONVERSATION_MESSAGE)` |
120120

121121
### 3.2 LLM & AI
122122

@@ -307,15 +307,15 @@ Both have type guards (`isApiProviderError()`, `isConsecutiveMistakeError()`) an
307307
## 7. Implementation Recommendations for New Extension
308308

309309
1. **Use `kilo-telemetry` via CLI proxy** — all PostHog communication goes through the CLI's `POST /telemetry/capture` endpoint. The extension does not include `posthog-node` or `posthog-js` directly.
310-
2. **Singleton service pattern** — single `TelemetryService` instance, multiple pluggable clients
310+
2. **Singleton pattern** — single `TelemetryProxy` instance that sends to CLI + logs to console, no pluggable clients
311311
3. **Properties provider pattern** — `KiloProvider` implements `TelemetryPropertiesProvider` to inject VS Code context into every event
312-
4. **Typed events** — all event names in an enum, with typed capture methods on the service
313-
5. **Event subscription/filtering** — clients can include/exclude specific events
314-
6. **Property filtering** — per-client property filtering (privacy controls)
312+
4. **Typed events** — all event names in an enum, callers use `TelemetryProxy.capture(TelemetryEventName.XXX, props)`
313+
5. **Event filtering** — `TelemetryProxy` can include/exclude specific events before forwarding to CLI
314+
6. **Property filtering** — privacy controls applied before forwarding to CLI
315315
7. **Dual opt-in** — respect both IDE-level and extension-level telemetry settings
316316
8. **Identity upgrade** — anonymous by default, upgrade to user identity on auth
317317
9. **Graceful degradation** — never crash on telemetry failures; all capture calls are fire-and-forget
318-
10. **Debug client** — separate console-logging client for development
318+
10. **Console logging** — `TelemetryProxy` logs events to console in development mode
319319

320320
---
321321

‎packages/kilo-vscode/script/local-bin.ts‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,31 @@ function log(msg: string) {
2828
console.log(`[local-bin] ${msg}`)
2929
}
3030

31+
function platformTag(): string {
32+
const os = process.platform === "win32" ? "windows" : process.platform
33+
return `cli-${os}-${process.arch}`
34+
}
35+
3136
async function findKiloBinaryInOpencodeDist(): Promise<string | null> {
3237
const distDir = join(opencodeDir, "dist")
3338

34-
// Check if dist directory exists using readdirSync
3539
try {
3640
readdirSync(distDir)
3741
} catch {
3842
return null
3943
}
4044

41-
// Expected: packages/opencode/dist/@kilocode/cli-<platform>/bin/kilo
42-
// But keep it flexible: find any dist/**/bin/kilo
45+
// Prefer the binary matching the current platform (e.g. cli-darwin-arm64)
46+
const tag = platformTag()
47+
const preferred = join(distDir, `@kilocode`, tag, "bin", "kilo")
48+
try {
49+
statSync(preferred)
50+
return preferred
51+
} catch {
52+
// fall through to generic search
53+
}
54+
55+
// Fallback: find any dist/**/bin/kilo
4356
const queue = [distDir]
4457
while (queue.length) {
4558
const dir = queue.pop()

‎packages/kilo-vscode/src/KiloProvider.ts‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { type HttpClient, type SessionInfo, type SSEEvent, type KiloConnectionSe
44
import { handleChatCompletionRequest } from "./services/autocomplete/chat-autocomplete/handleChatCompletionRequest"
55
import { handleChatCompletionAccepted } from "./services/autocomplete/chat-autocomplete/handleChatCompletionAccepted"
66
import { buildWebviewHtml } from "./utils"
7+
import { TelemetryProxy, type TelemetryPropertiesProvider } from "./services/telemetry"
78

8-
export class KiloProvider implements vscode.WebviewViewProvider {
9+
export class KiloProvider implements vscode.WebviewViewProvider, TelemetryPropertiesProvider {
910
public static readonly viewType = "kilo-code.new.sidebarView"
1011

1112
private webview: vscode.Webview | null = null
@@ -36,7 +37,21 @@ export class KiloProvider implements vscode.WebviewViewProvider {
3637
constructor(
3738
private readonly extensionUri: vscode.Uri,
3839
private readonly connectionService: KiloConnectionService,
39-
) {}
40+
) {
41+
TelemetryProxy.getInstance().setProvider(this)
42+
}
43+
44+
getTelemetryProperties(): Record<string, unknown> {
45+
return {
46+
appName: "kilo-code",
47+
appVersion: this.extensionVersion,
48+
platform: "vscode",
49+
editorName: vscode.env.appName,
50+
vscodeVersion: vscode.version,
51+
machineId: vscode.env.machineId,
52+
vscodeIsTelemetryEnabled: vscode.env.isTelemetryEnabled,
53+
}
54+
}
4055

4156
/**
4257
* Convenience getter that returns the shared HttpClient or null if not yet connected.
@@ -386,6 +401,9 @@ export class KiloProvider implements vscode.WebviewViewProvider {
386401
case "resetAllSettings":
387402
await this.handleResetAllSettings()
388403
break
404+
case "telemetry":
405+
TelemetryProxy.capture(message.event, message.properties)
406+
break
389407
}
390408
})
391409
}

‎packages/kilo-vscode/src/extension.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,28 @@ import { EXTENSION_DISPLAY_NAME } from "./constants"
55
import { KiloConnectionService } from "./services/cli-backend"
66
import { registerAutocompleteProvider } from "./services/autocomplete"
77
import { BrowserAutomationService } from "./services/browser-automation"
8+
import { TelemetryProxy } from "./services/telemetry"
89

910
export function activate(context: vscode.ExtensionContext) {
1011
console.log("Kilo Code extension is now active")
1112

13+
const telemetry = TelemetryProxy.getInstance()
14+
1215
// Create shared connection service (one server for all webviews)
1316
const connectionService = new KiloConnectionService(context)
1417

1518
// Create browser automation service (manages Playwright MCP registration)
1619
const browserAutomationService = new BrowserAutomationService(connectionService)
1720
browserAutomationService.syncWithSettings()
1821

19-
// Re-register browser automation MCP server on CLI backend reconnect
22+
// Re-register browser automation MCP server on CLI backend reconnect and configure telemetry
2023
const unsubscribeStateChange = connectionService.onStateChange((state) => {
2124
if (state === "connected") {
2225
browserAutomationService.reregisterIfEnabled()
26+
const config = connectionService.getServerConfig()
27+
if (config) {
28+
telemetry.configure(config.baseUrl, config.password)
29+
}
2330
}
2431
})
2532

@@ -83,7 +90,9 @@ export function activate(context: vscode.ExtensionContext) {
8390
})
8491
}
8592

86-
export function deactivate() {}
93+
export function deactivate() {
94+
TelemetryProxy.getInstance().shutdown()
95+
}
8796

8897
async function openKiloInNewTab(context: vscode.ExtensionContext, connectionService: KiloConnectionService) {
8998
const lastCol = Math.max(...vscode.window.visibleTextEditors.map((e) => e.viewColumn || 0), 0)

0 commit comments

Comments
 (0)