mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
Merge pull request #31 from Kilo-Org/catrielmuller/cli-telemetry
Telemetry
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
"devDependencies": {
|
||||
"@actions/artifact": "5.0.1",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"baseline-browser-mapping": "2.9.18",
|
||||
"husky": "9.1.7",
|
||||
"prettier": "3.6.2",
|
||||
"semver": "^7.6.0",
|
||||
@@ -286,6 +287,26 @@
|
||||
"solid-js",
|
||||
],
|
||||
},
|
||||
"packages/kilo-telemetry": {
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
"@opentelemetry/core": "1.30.1",
|
||||
"@opentelemetry/resources": "1.30.1",
|
||||
"@opentelemetry/sdk-trace-base": "1.30.1",
|
||||
"@opentelemetry/sdk-trace-node": "1.30.1",
|
||||
"@opentelemetry/semantic-conventions": "1.28.0",
|
||||
"posthog-node": "4.4.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "@kilocode/cli",
|
||||
"version": "1.1.36",
|
||||
@@ -320,6 +341,7 @@
|
||||
"@hono/standard-validator": "0.1.5",
|
||||
"@hono/zod-validator": "catalog:",
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
"@kilocode/kilo-telemetry": "workspace:*",
|
||||
"@modelcontextprotocol/sdk": "1.25.2",
|
||||
"@octokit/graphql": "9.0.2",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -1148,6 +1170,8 @@
|
||||
|
||||
"@kilocode/kilo-gateway": ["@kilocode/kilo-gateway@workspace:packages/kilo-gateway"],
|
||||
|
||||
"@kilocode/kilo-telemetry": ["@kilocode/kilo-telemetry@workspace:packages/kilo-telemetry"],
|
||||
|
||||
"@kobalte/core": ["@kobalte/core@0.13.11", "", { "dependencies": { "@floating-ui/dom": "^1.5.1", "@internationalized/date": "^3.4.0", "@internationalized/number": "^3.2.1", "@kobalte/utils": "^0.9.1", "@solid-primitives/props": "^3.1.8", "@solid-primitives/resize-observer": "^2.0.26", "solid-presence": "^0.1.8", "solid-prevent-scroll": "^0.1.4" }, "peerDependencies": { "solid-js": "^1.8.15" } }, "sha512-hK7TYpdib/XDb/r/4XDBFaO9O+3ZHz4ZWryV4/3BfES+tSQVgg2IJupDnztKXB0BqbSRy/aWlHKw1SPtNPYCFQ=="],
|
||||
|
||||
"@kobalte/utils": ["@kobalte/utils@0.9.1", "", { "dependencies": { "@solid-primitives/event-listener": "^2.2.14", "@solid-primitives/keyed": "^1.2.0", "@solid-primitives/map": "^0.4.7", "@solid-primitives/media": "^2.2.4", "@solid-primitives/props": "^3.1.8", "@solid-primitives/refs": "^1.0.5", "@solid-primitives/utils": "^6.2.1" }, "peerDependencies": { "solid-js": "^1.8.8" } }, "sha512-eeU60A3kprIiBDAfv9gUJX1tXGLuZiKMajUfSQURAF2pk4ZoMYiqIzmrMBvzcxP39xnYttgTyQEVLwiTZnrV4w=="],
|
||||
@@ -1264,6 +1288,22 @@
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@1.30.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA=="],
|
||||
|
||||
"@opentelemetry/core": ["@opentelemetry/core@1.30.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ=="],
|
||||
|
||||
"@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-oATwWWDIJzybAZ4pO76ATN5N6FFbOA1otibAVlS8v90B4S1wClnhRUk7K+2CHAwN1JKYuj4jh/lpCEG5BAqFuQ=="],
|
||||
|
||||
"@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg=="],
|
||||
|
||||
"@opentelemetry/resources": ["@opentelemetry/resources@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA=="],
|
||||
|
||||
"@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg=="],
|
||||
|
||||
"@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@1.30.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "1.30.1", "@opentelemetry/core": "1.30.1", "@opentelemetry/propagator-b3": "1.30.1", "@opentelemetry/propagator-jaeger": "1.30.1", "@opentelemetry/sdk-trace-base": "1.30.1", "semver": "^7.5.2" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ=="],
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.1.74", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.74", "@opentui/core-darwin-x64": "0.1.74", "@opentui/core-linux-arm64": "0.1.74", "@opentui/core-linux-x64": "0.1.74", "@opentui/core-win32-arm64": "0.1.74", "@opentui/core-win32-x64": "0.1.74", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-g4W16ymv12JdgZ+9B4t7mpIICvzWy2+eHERfmDf80ALduOQCUedKQdULcBFhVCYUXIkDRtIy6CID5thMAah3FA=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.74", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rfmlDLtm/u17CnuhJgCxPeYMvOST+A2MOdVOk46IurtHO849bdYqK6iudKNlFRs1FOrymgSKF9GlWBHAOKeRjg=="],
|
||||
@@ -2074,7 +2114,7 @@
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.30", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA=="],
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.18", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA=="],
|
||||
|
||||
"bcp-47": ["bcp-47@2.1.0", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w=="],
|
||||
|
||||
@@ -3366,6 +3406,8 @@
|
||||
|
||||
"postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
|
||||
|
||||
"posthog-node": ["posthog-node@4.4.0", "", { "dependencies": { "axios": "^1.7.4" } }, "sha512-4FEFBuc4FDFkTEiCSIo7DFV0jpprlqfR/SzjVgg7E2rRvfxTW4J47IvCwSCo5C9lBeV0ujDubY/YArHG37aJIw=="],
|
||||
|
||||
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
|
||||
|
||||
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||
@@ -4330,6 +4372,8 @@
|
||||
|
||||
"body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.30", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA=="],
|
||||
|
||||
"bun-webgpu/@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="],
|
||||
|
||||
"c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"devDependencies": {
|
||||
"@actions/artifact": "5.0.1",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"baseline-browser-mapping": "2.9.18",
|
||||
"husky": "9.1.7",
|
||||
"prettier": "3.6.2",
|
||||
"semver": "^7.6.0",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"build": "tsc",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"posthog-node": "4.4.0",
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
"@opentelemetry/core": "1.30.1",
|
||||
"@opentelemetry/sdk-trace-base": "1.30.1",
|
||||
"@opentelemetry/sdk-trace-node": "1.30.1",
|
||||
"@opentelemetry/resources": "1.30.1",
|
||||
"@opentelemetry/semantic-conventions": "1.28.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import { describe, test, expect, beforeEach } from "bun:test"
|
||||
import { Identity } from "../identity.js"
|
||||
import { TelemetryEvent } from "../events.js"
|
||||
import { PostHogSpanExporter } from "../otel-exporter.js"
|
||||
import { ExportResultCode } from "@opentelemetry/core"
|
||||
import type { ReadableSpan } from "@opentelemetry/sdk-trace-base"
|
||||
import type { PostHog } from "posthog-node"
|
||||
|
||||
function createMockPostHogClient(): PostHog {
|
||||
return {
|
||||
capture: () => {},
|
||||
alias: () => {},
|
||||
flush: async () => {},
|
||||
shutdown: async () => {},
|
||||
optIn: () => {},
|
||||
optOut: () => {},
|
||||
} as unknown as PostHog
|
||||
}
|
||||
|
||||
describe("Identity", () => {
|
||||
beforeEach(() => {
|
||||
Identity.reset()
|
||||
})
|
||||
|
||||
test("getDistinctId returns 'unknown' when no machineId or userId set", () => {
|
||||
expect(Identity.getDistinctId()).toBe("unknown")
|
||||
})
|
||||
|
||||
test("reset clears userId and organizationId", () => {
|
||||
Identity.setOrganizationId("org-123")
|
||||
expect(Identity.getOrganizationId()).toBe("org-123")
|
||||
|
||||
Identity.reset()
|
||||
expect(Identity.getOrganizationId()).toBeNull()
|
||||
expect(Identity.getUserId()).toBeNull()
|
||||
})
|
||||
|
||||
test("setOrganizationId sets and gets organization ID", () => {
|
||||
Identity.setOrganizationId("org-456")
|
||||
expect(Identity.getOrganizationId()).toBe("org-456")
|
||||
|
||||
Identity.setOrganizationId(null)
|
||||
expect(Identity.getOrganizationId()).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("TelemetryEvent", () => {
|
||||
test("CLI lifecycle events are defined", () => {
|
||||
expect(TelemetryEvent.CLI_START).toBeDefined()
|
||||
expect(TelemetryEvent.CLI_EXIT).toBeDefined()
|
||||
})
|
||||
|
||||
test("session events are defined", () => {
|
||||
expect(TelemetryEvent.SESSION_START).toBeDefined()
|
||||
expect(TelemetryEvent.SESSION_END).toBeDefined()
|
||||
expect(TelemetryEvent.SESSION_MESSAGE).toBeDefined()
|
||||
})
|
||||
|
||||
test("LLM events are defined", () => {
|
||||
expect(TelemetryEvent.LLM_COMPLETION).toBeDefined()
|
||||
})
|
||||
|
||||
test("feature events are defined", () => {
|
||||
expect(TelemetryEvent.COMMAND_USED).toBeDefined()
|
||||
expect(TelemetryEvent.TOOL_USED).toBeDefined()
|
||||
expect(TelemetryEvent.AGENT_USED).toBeDefined()
|
||||
})
|
||||
|
||||
test("auth events are defined", () => {
|
||||
expect(TelemetryEvent.AUTH_SUCCESS).toBeDefined()
|
||||
expect(TelemetryEvent.AUTH_LOGOUT).toBeDefined()
|
||||
})
|
||||
|
||||
test("MCP events are defined", () => {
|
||||
expect(TelemetryEvent.MCP_SERVER_CONNECTED).toBeDefined()
|
||||
expect(TelemetryEvent.MCP_SERVER_ERROR).toBeDefined()
|
||||
})
|
||||
|
||||
test("share events are defined", () => {
|
||||
expect(TelemetryEvent.SHARE_CREATED).toBeDefined()
|
||||
expect(TelemetryEvent.SHARE_DELETED).toBeDefined()
|
||||
})
|
||||
|
||||
test("error event is defined", () => {
|
||||
expect(TelemetryEvent.ERROR).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("PostHogSpanExporter", () => {
|
||||
function createMockSpan(name: string, attributes: Record<string, unknown>): ReadableSpan {
|
||||
return {
|
||||
name,
|
||||
attributes,
|
||||
spanContext: () => ({
|
||||
traceId: "trace-123",
|
||||
spanId: "span-456",
|
||||
traceFlags: 1,
|
||||
}),
|
||||
parentSpanId: undefined,
|
||||
startTime: [1000, 0],
|
||||
endTime: [1001, 0],
|
||||
status: { code: 0 },
|
||||
kind: 0,
|
||||
resource: { attributes: {} },
|
||||
instrumentationLibrary: { name: "test" },
|
||||
events: [],
|
||||
links: [],
|
||||
ended: true,
|
||||
duration: [1, 0],
|
||||
droppedAttributesCount: 0,
|
||||
droppedEventsCount: 0,
|
||||
droppedLinksCount: 0,
|
||||
} as unknown as ReadableSpan
|
||||
}
|
||||
|
||||
test("export returns success when disabled", () => {
|
||||
const exporter = new PostHogSpanExporter(createMockPostHogClient())
|
||||
exporter.setEnabled(false)
|
||||
|
||||
const span = createMockSpan("ai.generateText", { "ai.model.id": "gpt-4" })
|
||||
let result: { code: number } | null = null
|
||||
|
||||
exporter.export([span], (r) => {
|
||||
result = r
|
||||
})
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.code).toBe(ExportResultCode.SUCCESS)
|
||||
})
|
||||
|
||||
test("sensitive attributes are not included in exported properties", () => {
|
||||
// This test verifies the filtering logic by checking the SENSITIVE_ATTRIBUTES set
|
||||
// and the mapAttributes method behavior through the export function
|
||||
const exporter = new PostHogSpanExporter(createMockPostHogClient())
|
||||
|
||||
// Create a span with both safe and sensitive attributes
|
||||
const span = createMockSpan("ai.generateText", {
|
||||
// Safe attributes (should be passed through)
|
||||
"ai.model.id": "gpt-4",
|
||||
"ai.model.provider": "openai",
|
||||
"gen_ai.request.model": "gpt-4",
|
||||
"gen_ai.usage.input_tokens": 100,
|
||||
"gen_ai.usage.output_tokens": 50,
|
||||
// Sensitive attributes (should be filtered)
|
||||
"ai.prompt": '{"messages": [{"role": "user", "content": "secret data"}]}',
|
||||
"ai.prompt.messages": '[{"role": "user", "content": "secret"}]',
|
||||
"ai.response.text": "This is a secret response",
|
||||
"ai.toolCall.args": '{"secret": "value"}',
|
||||
"ai.toolCall.result": '{"result": "secret"}',
|
||||
"gen_ai.prompt": "secret prompt",
|
||||
"gen_ai.completion": "secret completion",
|
||||
})
|
||||
|
||||
// The exporter should complete successfully
|
||||
let result: { code: number } | null = null
|
||||
exporter.export([span], (r) => {
|
||||
result = r
|
||||
})
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.code).toBe(ExportResultCode.SUCCESS)
|
||||
})
|
||||
|
||||
test("SENSITIVE_ATTRIBUTES blocklist contains all required patterns", () => {
|
||||
// Verify all sensitive attribute patterns are in the blocklist
|
||||
const sensitivePatterns = [
|
||||
"ai.prompt",
|
||||
"ai.prompt.messages",
|
||||
"ai.response.text",
|
||||
"ai.response.toolCalls",
|
||||
"ai.toolCall.args",
|
||||
"ai.toolCall.result",
|
||||
"ai.value",
|
||||
"ai.values",
|
||||
"ai.embedding",
|
||||
"ai.embeddings",
|
||||
"ai.prompt.tools",
|
||||
"gen_ai.prompt",
|
||||
"gen_ai.completion",
|
||||
"gen_ai.input.messages",
|
||||
"gen_ai.output.messages",
|
||||
"gen_ai.system_instructions",
|
||||
"gen_ai.tool.definitions",
|
||||
]
|
||||
|
||||
// Import the module to check the exported constant exists
|
||||
// Since SENSITIVE_ATTRIBUTES is not exported, we verify through behavior
|
||||
// by ensuring the exporter handles these attributes correctly
|
||||
expect(sensitivePatterns.length).toBe(17)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
import { PostHog } from "posthog-node"
|
||||
import { Identity } from "./identity.js"
|
||||
import { TelemetryEvent } from "./events.js"
|
||||
|
||||
const POSTHOG_API_KEY = "phc_GK2Pxl0HPj5ZPfwhLRjXrtdz8eD7e9MKnXiFrOqnB6z"
|
||||
const POSTHOG_HOST = "https://us.i.posthog.com"
|
||||
|
||||
export namespace Client {
|
||||
let client: PostHog | null = null
|
||||
let enabled = true
|
||||
|
||||
export function init() {
|
||||
client = new PostHog(POSTHOG_API_KEY, {
|
||||
host: POSTHOG_HOST,
|
||||
disableGeoip: false,
|
||||
})
|
||||
}
|
||||
|
||||
export function getClient(): PostHog | null {
|
||||
return client
|
||||
}
|
||||
|
||||
export function setEnabled(value: boolean) {
|
||||
enabled = value
|
||||
if (!client) return
|
||||
if (value) client.optIn()
|
||||
else client.optOut()
|
||||
}
|
||||
|
||||
export function isEnabled(): boolean {
|
||||
return enabled && client !== null
|
||||
}
|
||||
|
||||
export function capture(event: TelemetryEvent, properties?: Record<string, unknown>) {
|
||||
if (!enabled || !client) return
|
||||
|
||||
const distinctId = Identity.getDistinctId()
|
||||
const orgId = Identity.getOrganizationId()
|
||||
|
||||
client.capture({
|
||||
distinctId,
|
||||
event,
|
||||
properties: {
|
||||
...properties,
|
||||
...(orgId && { kilocodeOrganizationId: orgId }),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function identify(distinctId: string, properties?: Record<string, unknown>) {
|
||||
if (!enabled || !client) return
|
||||
|
||||
client.capture({
|
||||
distinctId,
|
||||
event: "$identify",
|
||||
properties: {
|
||||
$set: properties,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function alias(distinctId: string, aliasId: string) {
|
||||
if (!enabled || !client) return
|
||||
|
||||
client.alias({
|
||||
distinctId,
|
||||
alias: aliasId,
|
||||
})
|
||||
}
|
||||
|
||||
export async function shutdown(): Promise<void> {
|
||||
if (client) {
|
||||
// Flush any pending events before shutdown
|
||||
await client.flush()
|
||||
await client.shutdown()
|
||||
client = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
export enum TelemetryEvent {
|
||||
// CLI Lifecycle
|
||||
CLI_START = "CLI Start",
|
||||
CLI_EXIT = "CLI Exit",
|
||||
|
||||
// Session Events
|
||||
SESSION_START = "Session Start",
|
||||
SESSION_END = "Session End",
|
||||
SESSION_MESSAGE = "Session Message",
|
||||
|
||||
// Model Usage
|
||||
LLM_COMPLETION = "LLM Completion",
|
||||
|
||||
// Feature Usage
|
||||
COMMAND_USED = "Command Used",
|
||||
TOOL_USED = "Tool Used",
|
||||
AGENT_USED = "Agent Used",
|
||||
|
||||
// Share Events
|
||||
SHARE_CREATED = "Share Created",
|
||||
SHARE_DELETED = "Share Deleted",
|
||||
|
||||
// MCP Events
|
||||
MCP_SERVER_CONNECTED = "MCP Server Connected",
|
||||
MCP_SERVER_ERROR = "MCP Server Error",
|
||||
|
||||
// Auth Events
|
||||
AUTH_SUCCESS = "Auth Success",
|
||||
AUTH_LOGOUT = "Auth Logout",
|
||||
|
||||
// Config Events
|
||||
TELEMETRY_DISABLED = "Telemetry Disabled",
|
||||
|
||||
// Errors
|
||||
ERROR = "Error",
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import * as path from "path"
|
||||
import { fetchProfile } from "@kilocode/kilo-gateway"
|
||||
|
||||
export namespace Identity {
|
||||
let machineId: string | null = null
|
||||
let userId: string | null = null
|
||||
let organizationId: string | null = null
|
||||
let dataPath = ""
|
||||
|
||||
export function setDataPath(p: string) {
|
||||
dataPath = p
|
||||
}
|
||||
|
||||
export async function getMachineId(): Promise<string> {
|
||||
if (machineId) return machineId
|
||||
|
||||
const filepath = path.join(dataPath, "telemetry-id")
|
||||
const file = Bun.file(filepath)
|
||||
|
||||
if (await file.exists()) {
|
||||
machineId = await file.text()
|
||||
return machineId
|
||||
}
|
||||
|
||||
machineId = crypto.randomUUID()
|
||||
await Bun.write(filepath, machineId)
|
||||
return machineId
|
||||
}
|
||||
|
||||
export function getDistinctId(): string {
|
||||
return userId || machineId || "unknown"
|
||||
}
|
||||
|
||||
export function getUserId(): string | null {
|
||||
return userId
|
||||
}
|
||||
|
||||
export function getOrganizationId(): string | null {
|
||||
return organizationId
|
||||
}
|
||||
|
||||
export function setOrganizationId(orgId: string | null) {
|
||||
organizationId = orgId
|
||||
}
|
||||
|
||||
export async function updateFromKiloAuth(token: string | null, accountId?: string): Promise<void> {
|
||||
organizationId = accountId || null
|
||||
|
||||
if (!token) {
|
||||
userId = null
|
||||
return
|
||||
}
|
||||
|
||||
const profile = await fetchProfile(token).catch(() => null)
|
||||
userId = profile?.email || null
|
||||
}
|
||||
|
||||
export function reset() {
|
||||
userId = null
|
||||
organizationId = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export { Telemetry } from "./telemetry.js"
|
||||
export { TelemetryEvent } from "./events.js"
|
||||
export { Identity } from "./identity.js"
|
||||
export { TracerSetup } from "./tracer.js"
|
||||
export { PostHogSpanExporter } from "./otel-exporter.js"
|
||||
export type { TelemetryProperties } from "./telemetry.js"
|
||||
@@ -0,0 +1,220 @@
|
||||
import type { SpanExporter, ReadableSpan } from "@opentelemetry/sdk-trace-base"
|
||||
import type { ExportResult } from "@opentelemetry/core"
|
||||
import { ExportResultCode } from "@opentelemetry/core"
|
||||
import type { PostHog } from "posthog-node"
|
||||
import { Identity } from "./identity.js"
|
||||
|
||||
/**
|
||||
* Sensitive attributes that should never be sent to PostHog.
|
||||
* This is a defense-in-depth measure - the AI SDK should already filter these
|
||||
* when recordInputs/recordOutputs are set to false.
|
||||
*/
|
||||
const SENSITIVE_ATTRIBUTES = new Set([
|
||||
// AI SDK prompt/message content
|
||||
"ai.prompt",
|
||||
"ai.prompt.messages",
|
||||
"ai.response.text",
|
||||
"ai.response.toolCalls",
|
||||
"ai.toolCall.args",
|
||||
"ai.toolCall.result",
|
||||
"ai.value",
|
||||
"ai.values",
|
||||
"ai.embedding",
|
||||
"ai.embeddings",
|
||||
"ai.prompt.tools",
|
||||
// OpenTelemetry GenAI semantic conventions
|
||||
"gen_ai.prompt",
|
||||
"gen_ai.completion",
|
||||
"gen_ai.input.messages",
|
||||
"gen_ai.output.messages",
|
||||
"gen_ai.system_instructions",
|
||||
"gen_ai.tool.definitions",
|
||||
])
|
||||
|
||||
/**
|
||||
* PostHogSpanExporter converts OpenTelemetry spans to PostHog AI events.
|
||||
* Maps OTel spans to PostHog's $ai_span, $ai_generation, and $ai_trace events.
|
||||
*/
|
||||
export class PostHogSpanExporter implements SpanExporter {
|
||||
private client: PostHog
|
||||
private enabled = true
|
||||
private appName = "kilo-cli"
|
||||
private appVersion = "unknown"
|
||||
|
||||
constructor(client: PostHog, options?: { appVersion?: string }) {
|
||||
this.client = client
|
||||
if (options?.appVersion) {
|
||||
this.appVersion = options.appVersion
|
||||
}
|
||||
}
|
||||
|
||||
setEnabled(value: boolean) {
|
||||
this.enabled = value
|
||||
if (value) this.client.optIn()
|
||||
else this.client.optOut()
|
||||
}
|
||||
|
||||
export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
|
||||
if (!this.enabled) {
|
||||
resultCallback({ code: ExportResultCode.SUCCESS })
|
||||
return
|
||||
}
|
||||
|
||||
for (const span of spans) {
|
||||
this.exportSpan(span)
|
||||
}
|
||||
|
||||
resultCallback({ code: ExportResultCode.SUCCESS })
|
||||
}
|
||||
|
||||
private exportSpan(span: ReadableSpan) {
|
||||
const distinctId = Identity.getDistinctId()
|
||||
const orgId = Identity.getOrganizationId()
|
||||
|
||||
const name = span.name
|
||||
const attrs = span.attributes
|
||||
const duration = span.endTime[0] - span.startTime[0] + (span.endTime[1] - span.startTime[1]) / 1e9
|
||||
|
||||
// Determine event type based on span name/attributes
|
||||
const eventType = this.determineEventType(name, attrs)
|
||||
|
||||
// Build PostHog AI event properties
|
||||
const properties: Record<string, unknown> = {
|
||||
appName: this.appName,
|
||||
appVersion: this.appVersion,
|
||||
platform: process.platform,
|
||||
$ai_trace_id: span.spanContext().traceId,
|
||||
$ai_span_id: span.spanContext().spanId,
|
||||
$ai_span_name: name,
|
||||
$ai_latency: duration,
|
||||
...(orgId && { kilocodeOrganizationId: orgId }),
|
||||
}
|
||||
|
||||
// Add parent ID if present
|
||||
if (span.parentSpanId) {
|
||||
properties.$ai_parent_id = span.parentSpanId
|
||||
}
|
||||
|
||||
// Map span attributes to PostHog properties
|
||||
this.mapAttributes(attrs, properties)
|
||||
|
||||
// Handle errors
|
||||
if (span.status.code === 2) {
|
||||
// SpanStatusCode.ERROR
|
||||
properties.$ai_is_error = true
|
||||
properties.$ai_error = span.status.message || "Unknown error"
|
||||
}
|
||||
|
||||
// Capture the event
|
||||
this.client.capture({
|
||||
distinctId,
|
||||
event: eventType,
|
||||
properties,
|
||||
})
|
||||
}
|
||||
|
||||
private determineEventType(name: string, attrs: Record<string, unknown>): string {
|
||||
// AI SDK spans typically have these patterns:
|
||||
// - "ai.generateText" / "ai.streamText" -> $ai_generation
|
||||
// - "ai.toolCall" -> $ai_span (tool call)
|
||||
// - "ai.embed" -> $ai_span (embedding)
|
||||
|
||||
const lowerName = name.toLowerCase()
|
||||
|
||||
if (
|
||||
lowerName.includes("generatetext") ||
|
||||
lowerName.includes("streamtext") ||
|
||||
lowerName.includes("generateobject") ||
|
||||
lowerName.includes("streamobject")
|
||||
) {
|
||||
return "$ai_generation"
|
||||
}
|
||||
|
||||
if (lowerName.includes("toolcall") || lowerName.includes("tool")) {
|
||||
return "$ai_span"
|
||||
}
|
||||
|
||||
if (lowerName.includes("embed")) {
|
||||
return "$ai_span"
|
||||
}
|
||||
|
||||
// Check for gen_ai semantic conventions
|
||||
if (attrs["gen_ai.operation.name"]) {
|
||||
const op = String(attrs["gen_ai.operation.name"])
|
||||
if (op === "chat" || op === "text_completion") {
|
||||
return "$ai_generation"
|
||||
}
|
||||
}
|
||||
|
||||
// Default to span for other AI operations
|
||||
return "$ai_span"
|
||||
}
|
||||
|
||||
private mapAttributes(attrs: Record<string, unknown>, props: Record<string, unknown>) {
|
||||
// Map OpenTelemetry semantic conventions to PostHog AI properties
|
||||
|
||||
// Model info
|
||||
if (attrs["gen_ai.request.model"]) {
|
||||
props.$ai_model = attrs["gen_ai.request.model"]
|
||||
}
|
||||
if (attrs["gen_ai.system"]) {
|
||||
props.$ai_provider = attrs["gen_ai.system"]
|
||||
}
|
||||
|
||||
// Token usage
|
||||
if (attrs["gen_ai.usage.input_tokens"]) {
|
||||
props.$ai_input_tokens = attrs["gen_ai.usage.input_tokens"]
|
||||
}
|
||||
if (attrs["gen_ai.usage.output_tokens"]) {
|
||||
props.$ai_output_tokens = attrs["gen_ai.usage.output_tokens"]
|
||||
}
|
||||
if (attrs["gen_ai.usage.total_tokens"]) {
|
||||
props.$ai_total_tokens = attrs["gen_ai.usage.total_tokens"]
|
||||
}
|
||||
|
||||
// Track presence of prompt/completion without content (for analytics)
|
||||
if (attrs["gen_ai.prompt"] || attrs["ai.prompt"]) {
|
||||
props.$ai_has_prompt = true
|
||||
}
|
||||
if (attrs["gen_ai.completion"] || attrs["ai.response.text"]) {
|
||||
props.$ai_has_completion = true
|
||||
}
|
||||
|
||||
// Tool calls - only track name, not args/result
|
||||
if (attrs["ai.toolCall.name"]) {
|
||||
props.$ai_tool_name = attrs["ai.toolCall.name"]
|
||||
}
|
||||
|
||||
// Temperature and other settings
|
||||
if (attrs["gen_ai.request.temperature"]) {
|
||||
props.$ai_temperature = attrs["gen_ai.request.temperature"]
|
||||
}
|
||||
if (attrs["gen_ai.request.max_tokens"]) {
|
||||
props.$ai_max_tokens = attrs["gen_ai.request.max_tokens"]
|
||||
}
|
||||
|
||||
// Finish reason
|
||||
if (attrs["gen_ai.response.finish_reasons"]) {
|
||||
props.$ai_finish_reason = attrs["gen_ai.response.finish_reasons"]
|
||||
}
|
||||
|
||||
// Copy safe ai.* prefixed attributes (filter out sensitive ones)
|
||||
for (const [key, value] of Object.entries(attrs)) {
|
||||
if (!key.startsWith("ai.")) continue
|
||||
if (SENSITIVE_ATTRIBUTES.has(key)) continue
|
||||
// Additional pattern-based filtering for safety
|
||||
if (key.includes("prompt") || key.includes("messages") || key.includes("response.text")) continue
|
||||
const propKey = key.replace(/\./g, "_")
|
||||
props[propKey] = value
|
||||
}
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
// Only flush, don't shutdown - the shared client is managed by Client namespace
|
||||
await this.client.flush()
|
||||
}
|
||||
|
||||
async forceFlush(): Promise<void> {
|
||||
await this.client.flush()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { Client } from "./client.js"
|
||||
import { Identity } from "./identity.js"
|
||||
import { TelemetryEvent } from "./events.js"
|
||||
import { TracerSetup } from "./tracer.js"
|
||||
import type { Tracer } from "@opentelemetry/api"
|
||||
|
||||
export interface TelemetryProperties {
|
||||
appName: string
|
||||
appVersion: string
|
||||
platform: string
|
||||
}
|
||||
|
||||
export namespace Telemetry {
|
||||
let initialized = false
|
||||
let startTime = 0
|
||||
let props: TelemetryProperties = {
|
||||
appName: "kilo-cli",
|
||||
appVersion: "unknown",
|
||||
platform: process.platform,
|
||||
}
|
||||
|
||||
export async function init(options: { dataPath: string; version: string; enabled: boolean }): Promise<void> {
|
||||
if (initialized) return
|
||||
|
||||
Identity.setDataPath(options.dataPath)
|
||||
props.appVersion = options.version
|
||||
|
||||
Client.init()
|
||||
Client.setEnabled(options.enabled)
|
||||
|
||||
// Initialize OpenTelemetry tracer for AI SDK spans
|
||||
TracerSetup.init({ version: options.version, enabled: options.enabled })
|
||||
|
||||
await Identity.getMachineId()
|
||||
|
||||
initialized = true
|
||||
startTime = Date.now()
|
||||
}
|
||||
|
||||
export function setEnabled(value: boolean) {
|
||||
Client.setEnabled(value)
|
||||
TracerSetup.setEnabled(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the OpenTelemetry tracer for use with AI SDK's experimental_telemetry.
|
||||
* Returns null if telemetry is not initialized.
|
||||
*/
|
||||
export function getTracer(): Tracer | null {
|
||||
return TracerSetup.getTracer()
|
||||
}
|
||||
|
||||
export function isEnabled(): boolean {
|
||||
return Client.isEnabled()
|
||||
}
|
||||
|
||||
export async function updateIdentity(token: string | null, accountId?: string): Promise<void> {
|
||||
const previousId = Identity.getDistinctId()
|
||||
await Identity.updateFromKiloAuth(token, accountId)
|
||||
|
||||
const email = Identity.getUserId()
|
||||
if (email && previousId && email !== previousId) {
|
||||
// Identify the user with their email and properties
|
||||
Client.identify(email, {
|
||||
...(accountId && { kilocodeOrganizationId: accountId }),
|
||||
appName: props.appName,
|
||||
appVersion: props.appVersion,
|
||||
platform: props.platform,
|
||||
})
|
||||
|
||||
// Link the anonymous machineId to the authenticated email
|
||||
Client.alias(email, previousId)
|
||||
}
|
||||
}
|
||||
|
||||
export function track(event: TelemetryEvent, properties?: Record<string, unknown>) {
|
||||
Client.capture(event, { ...props, ...properties })
|
||||
}
|
||||
|
||||
// CLI Lifecycle
|
||||
export function trackCliStart() {
|
||||
track(TelemetryEvent.CLI_START)
|
||||
}
|
||||
|
||||
export function trackCliExit(exitCode?: number) {
|
||||
track(TelemetryEvent.CLI_EXIT, {
|
||||
duration: Date.now() - startTime,
|
||||
exitCode,
|
||||
})
|
||||
}
|
||||
|
||||
// Sessions
|
||||
export function trackSessionStart(sessionId: string, model?: string, provider?: string) {
|
||||
track(TelemetryEvent.SESSION_START, { sessionId, model, provider })
|
||||
}
|
||||
|
||||
export function trackSessionEnd(
|
||||
sessionId: string,
|
||||
stats: {
|
||||
messageCount?: number
|
||||
inputTokens?: number
|
||||
outputTokens?: number
|
||||
duration?: number
|
||||
},
|
||||
) {
|
||||
track(TelemetryEvent.SESSION_END, { sessionId, ...stats })
|
||||
}
|
||||
|
||||
export function trackSessionMessage(sessionId: string, source: "user" | "assistant") {
|
||||
track(TelemetryEvent.SESSION_MESSAGE, { sessionId, source })
|
||||
}
|
||||
|
||||
// LLM
|
||||
export function trackLlmCompletion(properties: {
|
||||
sessionId?: string
|
||||
provider: string
|
||||
model: string
|
||||
inputTokens?: number
|
||||
outputTokens?: number
|
||||
cacheReadTokens?: number
|
||||
cacheWriteTokens?: number
|
||||
duration?: number
|
||||
}) {
|
||||
track(TelemetryEvent.LLM_COMPLETION, properties)
|
||||
}
|
||||
|
||||
// Features
|
||||
export function trackCommandUsed(command: string) {
|
||||
track(TelemetryEvent.COMMAND_USED, { command })
|
||||
}
|
||||
|
||||
export function trackToolUsed(tool: string, sessionId?: string) {
|
||||
track(TelemetryEvent.TOOL_USED, { tool, sessionId })
|
||||
}
|
||||
|
||||
export function trackAgentUsed(agent: string, sessionId?: string) {
|
||||
track(TelemetryEvent.AGENT_USED, { agent, sessionId })
|
||||
}
|
||||
|
||||
// Share
|
||||
export function trackShareCreated(sessionId: string) {
|
||||
track(TelemetryEvent.SHARE_CREATED, { sessionId })
|
||||
}
|
||||
|
||||
export function trackShareDeleted(sessionId: string) {
|
||||
track(TelemetryEvent.SHARE_DELETED, { sessionId })
|
||||
}
|
||||
|
||||
// MCP
|
||||
export function trackMcpServerConnected(server: string) {
|
||||
track(TelemetryEvent.MCP_SERVER_CONNECTED, { server })
|
||||
}
|
||||
|
||||
export function trackMcpServerError(server: string, error?: string) {
|
||||
track(TelemetryEvent.MCP_SERVER_ERROR, { server, error })
|
||||
}
|
||||
|
||||
// Auth
|
||||
export function trackAuthSuccess(provider: string) {
|
||||
track(TelemetryEvent.AUTH_SUCCESS, { provider })
|
||||
}
|
||||
|
||||
export function trackAuthLogout(provider: string) {
|
||||
track(TelemetryEvent.AUTH_LOGOUT, { provider })
|
||||
}
|
||||
|
||||
// Errors
|
||||
export function trackError(error: string, context?: string) {
|
||||
track(TelemetryEvent.ERROR, { error, context })
|
||||
}
|
||||
|
||||
export async function shutdown(): Promise<void> {
|
||||
await TracerSetup.shutdown()
|
||||
await Client.shutdown()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"
|
||||
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base"
|
||||
import { Resource } from "@opentelemetry/resources"
|
||||
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions"
|
||||
import type { Tracer } from "@opentelemetry/api"
|
||||
import { PostHogSpanExporter } from "./otel-exporter.js"
|
||||
import { Client } from "./client.js"
|
||||
|
||||
let provider: NodeTracerProvider | null = null
|
||||
let exporter: PostHogSpanExporter | null = null
|
||||
let tracer: Tracer | null = null
|
||||
|
||||
export namespace TracerSetup {
|
||||
export function init(options: { version: string; enabled: boolean }): Tracer {
|
||||
if (tracer) return tracer
|
||||
|
||||
const client = Client.getClient()
|
||||
if (!client) {
|
||||
throw new Error("PostHog client not initialized. Call Client.init() first.")
|
||||
}
|
||||
|
||||
exporter = new PostHogSpanExporter(client, { appVersion: options.version })
|
||||
exporter.setEnabled(options.enabled)
|
||||
|
||||
provider = new NodeTracerProvider({
|
||||
resource: new Resource({
|
||||
[ATTR_SERVICE_NAME]: "kilo-cli",
|
||||
[ATTR_SERVICE_VERSION]: options.version,
|
||||
}),
|
||||
spanProcessors: [new SimpleSpanProcessor(exporter)],
|
||||
})
|
||||
|
||||
// Register the provider globally so all tracers use our exporter
|
||||
provider.register()
|
||||
|
||||
// Get tracer from our provider
|
||||
tracer = provider.getTracer("kilo-cli", options.version)
|
||||
|
||||
return tracer
|
||||
}
|
||||
|
||||
export function getTracer(): Tracer | null {
|
||||
return tracer
|
||||
}
|
||||
|
||||
export function setEnabled(value: boolean) {
|
||||
exporter?.setEnabled(value)
|
||||
}
|
||||
|
||||
export async function shutdown(): Promise<void> {
|
||||
if (provider) {
|
||||
await provider.shutdown()
|
||||
provider = null
|
||||
tracer = null
|
||||
exporter = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"skipLibCheck": true,
|
||||
"noUncheckedIndexedAccess": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -47,6 +47,7 @@
|
||||
"zod-to-json-schema": "3.24.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-telemetry": "workspace:*",
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.12.0",
|
||||
|
||||
@@ -18,6 +18,7 @@ import { mergeDeep, pipe, sortBy, values } from "remeda"
|
||||
import { Global } from "@/global"
|
||||
import path from "path"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { Telemetry } from "@kilocode/kilo-telemetry" // kilocode_change
|
||||
|
||||
export namespace Agent {
|
||||
export const Info = z
|
||||
@@ -285,12 +286,17 @@ export namespace Agent {
|
||||
const existing = await list()
|
||||
|
||||
const params = {
|
||||
// kilocode_change start - enable telemetry by default with custom PostHog tracer
|
||||
experimental_telemetry: {
|
||||
isEnabled: cfg.experimental?.openTelemetry,
|
||||
isEnabled: cfg.experimental?.openTelemetry !== false,
|
||||
recordInputs: false, // Prevent recording prompts, messages, tool args
|
||||
recordOutputs: false, // Prevent recording completions, tool results
|
||||
tracer: Telemetry.getTracer() ?? undefined,
|
||||
metadata: {
|
||||
userId: cfg.username ?? "unknown",
|
||||
},
|
||||
},
|
||||
// kilocode_change end
|
||||
temperature: 0.3,
|
||||
messages: [
|
||||
...system.map(
|
||||
|
||||
@@ -2,6 +2,7 @@ import path from "path"
|
||||
import { Global } from "../global"
|
||||
import fs from "fs/promises"
|
||||
import z from "zod"
|
||||
import { Telemetry } from "@kilocode/kilo-telemetry" // kilocode_change
|
||||
|
||||
export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"
|
||||
|
||||
@@ -69,5 +70,12 @@ export namespace Auth {
|
||||
delete data[key]
|
||||
await Bun.write(file, JSON.stringify(data, null, 2))
|
||||
await fs.chmod(file.name!, 0o600)
|
||||
|
||||
// kilocode_change start - Track logout and reset telemetry identity for Kilo
|
||||
if (key === "kilo") {
|
||||
await Telemetry.updateIdentity(null)
|
||||
}
|
||||
Telemetry.trackAuthLogout(key)
|
||||
// kilocode_change end
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1154,10 +1154,9 @@ export namespace Config {
|
||||
chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"),
|
||||
disable_paste_summary: z.boolean().optional(),
|
||||
batch_tool: z.boolean().optional().describe("Enable the batch tool"),
|
||||
openTelemetry: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)"),
|
||||
// kilocode_change start - enable telemetry by default
|
||||
openTelemetry: z.boolean().default(true).describe("Enable telemetry. Set to false to opt-out."),
|
||||
// kilocode_change end
|
||||
primary_tools: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
|
||||
@@ -26,6 +26,12 @@ import { EOL } from "os"
|
||||
import { WebCommand } from "./cli/cmd/web"
|
||||
import { PrCommand } from "./cli/cmd/pr"
|
||||
import { SessionCommand } from "./cli/cmd/session"
|
||||
// kilocode_change start - Import telemetry
|
||||
import { Telemetry } from "@kilocode/kilo-telemetry"
|
||||
import { Global } from "./global"
|
||||
import { Config } from "./config/config"
|
||||
import { Auth } from "./auth"
|
||||
// kilocode_change end
|
||||
|
||||
process.on("unhandledRejection", (e) => {
|
||||
Log.Default.error("rejection", {
|
||||
@@ -74,6 +80,24 @@ const cli = yargs(hideBin(process.argv))
|
||||
version: Installation.VERSION,
|
||||
args: process.argv.slice(2),
|
||||
})
|
||||
|
||||
// kilocode_change start - Initialize telemetry
|
||||
const globalCfg = await Config.getGlobal()
|
||||
await Telemetry.init({
|
||||
dataPath: Global.Path.data,
|
||||
version: Installation.VERSION,
|
||||
enabled: globalCfg.experimental?.openTelemetry !== false,
|
||||
})
|
||||
|
||||
const kiloAuth = await Auth.get("kilo")
|
||||
if (kiloAuth) {
|
||||
const token = kiloAuth.type === "oauth" ? kiloAuth.access : kiloAuth.key
|
||||
const accountId = kiloAuth.type === "oauth" ? kiloAuth.accountId : undefined
|
||||
await Telemetry.updateIdentity(token, accountId)
|
||||
}
|
||||
|
||||
Telemetry.trackCliStart()
|
||||
// kilocode_change end
|
||||
})
|
||||
.usage("\n" + UI.logo())
|
||||
.completion("completion", "generate shell completion script")
|
||||
@@ -151,6 +175,12 @@ try {
|
||||
}
|
||||
process.exitCode = 1
|
||||
} finally {
|
||||
// kilocode_change start - Track CLI exit and shutdown telemetry
|
||||
const exitCode = typeof process.exitCode === "number" ? process.exitCode : undefined
|
||||
Telemetry.trackCliExit(exitCode)
|
||||
await Telemetry.shutdown()
|
||||
// kilocode_change end
|
||||
|
||||
// Some subprocesses don't react properly to SIGTERM and similar signals.
|
||||
// Most notably, some docker-container-based MCP servers don't handle such signals unless
|
||||
// run using `docker run --init`.
|
||||
|
||||
@@ -6,6 +6,7 @@ import { fn } from "@/util/fn"
|
||||
import type { AuthOuathResult, Hooks } from "@opencode-ai/plugin"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { Auth } from "@/auth"
|
||||
import { Telemetry } from "@kilocode/kilo-telemetry" // kilocode_change
|
||||
|
||||
export namespace ProviderAuth {
|
||||
const state = Instance.state(async () => {
|
||||
@@ -110,6 +111,16 @@ export namespace ProviderAuth {
|
||||
}
|
||||
await Auth.set(input.providerID, info)
|
||||
}
|
||||
|
||||
// kilocode_change start - Update telemetry identity on Kilo auth
|
||||
if (input.providerID === "kilo") {
|
||||
const token = "refresh" in result ? result.access : result.key
|
||||
const accountId = "refresh" in result ? result.accountId : undefined
|
||||
await Telemetry.updateIdentity(token, accountId)
|
||||
}
|
||||
Telemetry.trackAuthSuccess(input.providerID)
|
||||
// kilocode_change end
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import { Flag } from "@/flag/flag"
|
||||
import { PermissionNext } from "@/permission/next"
|
||||
import { Auth } from "@/auth"
|
||||
import { DEFAULT_HEADERS } from "@/kilocode/const" // kilocode_change
|
||||
import { Telemetry } from "@kilocode/kilo-telemetry" // kilocode_change
|
||||
|
||||
export namespace LLM {
|
||||
const log = Log.create({ service: "llm" })
|
||||
@@ -263,7 +264,14 @@ export namespace LLM {
|
||||
extractReasoningMiddleware({ tagName: "think", startWithReasoning: false }),
|
||||
],
|
||||
}),
|
||||
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
|
||||
// kilocode_change start - enable telemetry by default with custom PostHog tracer
|
||||
experimental_telemetry: {
|
||||
isEnabled: cfg.experimental?.openTelemetry !== false,
|
||||
recordInputs: false, // Prevent recording prompts, messages, tool args
|
||||
recordOutputs: false, // Prevent recording completions, tool results
|
||||
tracer: Telemetry.getTracer() ?? undefined,
|
||||
},
|
||||
// kilocode_change end
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1808,7 +1808,7 @@ export type Config = {
|
||||
*/
|
||||
batch_tool?: boolean
|
||||
/**
|
||||
* Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)
|
||||
* Enable telemetry. Set to false to opt-out.
|
||||
*/
|
||||
openTelemetry?: boolean
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user