From 94753668617cac789e745ea565e0e64d9057e3df Mon Sep 17 00:00:00 2001 From: 7Sageer <7sageer@djwcb.cn> Date: Mon, 29 Jun 2026 14:49:59 +0800 Subject: [PATCH] chore(telemetry): normalize telemetry property keys to snake_case - Rename camelCase telemetry keys to snake_case on compaction_finished, compaction_failed, micro_compaction_finished, and the tool error event (tokens_before, tokens_after, compacted_count, retry_count, thinking_level, error_type, input_tokens/output_tokens, and the micro compaction config/effect keys). - Emit a fixed client-attribution key set (client_id/name/version/ui_mode, null when absent) from both session_started producers (core-impl and kimi-harness) so they share a stable schema. - Drop the duplicate current/latest keys on update_prompted and the redundant ui_mode on server_started. - Additive fields: login.method=oauth and question_answered.answered. Telemetry-only change; no changeset. --- apps/kimi-code/src/cli/sub/server/run.ts | 4 +- apps/kimi-code/src/cli/update/preflight.ts | 2 - apps/kimi-code/src/tui/commands/auth.ts | 1 + .../test/cli/update/preflight.test.ts | 8 +-- .../test/tui/kimi-tui-startup.test.ts | 2 + packages/acp-adapter/src/session.ts | 2 +- .../test/session-question-handler.test.ts | 6 +-- .../agent-core/src/agent/compaction/full.ts | 31 +++++++----- .../agent-core/src/agent/compaction/micro.ts | 18 ++++--- packages/agent-core/src/agent/tool/index.ts | 2 +- packages/agent-core/src/rpc/core-impl.ts | 23 ++++----- .../test/agent/compaction/full.test.ts | 49 +++++++++---------- .../test/agent/compaction/micro.test.ts | 35 +++++++------ packages/node-sdk/src/kimi-harness.ts | 5 ++ .../test/create-session-transport.test.ts | 9 ++++ 15 files changed, 111 insertions(+), 86 deletions(-) diff --git a/apps/kimi-code/src/cli/sub/server/run.ts b/apps/kimi-code/src/cli/sub/server/run.ts index a81aa13c1..2ebb04cd0 100644 --- a/apps/kimi-code/src/cli/sub/server/run.ts +++ b/apps/kimi-code/src/cli/sub/server/run.ts @@ -18,7 +18,7 @@ import { startServer, type RunningServer } from '@moonshot-ai/server'; import chalk from 'chalk'; import { Option, type Command } from 'commander'; -import { CLI_SHUTDOWN_TIMEOUT_MS, WEB_UI_MODE } from '#/constant/app'; +import { CLI_SHUTDOWN_TIMEOUT_MS } from '#/constant/app'; import { getNativeWebAssetsDir } from '#/native/web-assets'; import { darkColors } from '#/tui/theme/colors'; import { openUrl as defaultOpenUrl } from '#/utils/open-url'; @@ -346,7 +346,7 @@ async function runServerInProcess( }, }); - track('server_started', { ui_mode: WEB_UI_MODE, daemon: mode.daemon }); + track('server_started', { daemon: mode.daemon }); process.once('SIGINT', () => { void shutdown('SIGINT'); diff --git a/apps/kimi-code/src/cli/update/preflight.ts b/apps/kimi-code/src/cli/update/preflight.ts index fe893a50a..1ad8aefa3 100644 --- a/apps/kimi-code/src/cli/update/preflight.ts +++ b/apps/kimi-code/src/cli/update/preflight.ts @@ -430,8 +430,6 @@ function trackUpdatePrompted( rolloutTelemetry: RolloutTelemetry, ): void { trackUpdateEvent(track, 'update_prompted', { - current: currentVersion, - latest: target.version, current_version: currentVersion, target_version: target.version, source, diff --git a/apps/kimi-code/src/tui/commands/auth.ts b/apps/kimi-code/src/tui/commands/auth.ts index 8064c089b..5f811e393 100644 --- a/apps/kimi-code/src/tui/commands/auth.ts +++ b/apps/kimi-code/src/tui/commands/auth.ts @@ -70,6 +70,7 @@ async function handleKimiCodeOAuthLogin(host: SlashCommandHost): Promise { } host.track('login', { provider: DEFAULT_OAUTH_PROVIDER_NAME, + method: 'oauth', already_logged_in: alreadyLoggedIn, }); if (alreadyLoggedIn) { diff --git a/apps/kimi-code/test/cli/update/preflight.test.ts b/apps/kimi-code/test/cli/update/preflight.test.ts index a96d1445b..0a5690566 100644 --- a/apps/kimi-code/test/cli/update/preflight.test.ts +++ b/apps/kimi-code/test/cli/update/preflight.test.ts @@ -828,8 +828,8 @@ describe('runUpdatePreflight', () => { const track = vi.fn(); await runUpdatePreflight('0.4.0', { ...options, track }); expect(track).toHaveBeenCalledWith('update_prompted', expect.objectContaining({ - current: '0.4.0', - latest: '0.5.0', + current_version: '0.4.0', + target_version: '0.5.0', decision: 'prompt-install', source: 'npm-global', })); @@ -915,7 +915,7 @@ describe('runUpdatePreflight', () => { expect.objectContaining({ target: { version: '0.5.0' } }), ); expect(track).toHaveBeenCalledWith('update_prompted', expect.objectContaining({ - latest: '0.5.0', + target_version: '0.5.0', rollout_bucket: expect.any(Number), rollout_delay_seconds: 0, rollout_from_manifest: true, @@ -945,7 +945,7 @@ describe('runUpdatePreflight', () => { expect.objectContaining({ target: { version: '0.7.0' } }), ); expect(track).toHaveBeenCalledWith('update_prompted', expect.objectContaining({ - latest: '0.7.0', + target_version: '0.7.0', rollout_bucket: expect.any(Number), rollout_delay_seconds: 43_200, rollout_from_manifest: true, diff --git a/apps/kimi-code/test/tui/kimi-tui-startup.test.ts b/apps/kimi-code/test/tui/kimi-tui-startup.test.ts index a7c2719c7..93f8a8b83 100644 --- a/apps/kimi-code/test/tui/kimi-tui-startup.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-startup.test.ts @@ -1269,6 +1269,7 @@ describe('KimiTUI startup', () => { }); expect(harness.track).toHaveBeenCalledWith('login', { provider: 'managed:kimi-code', + method: 'oauth', already_logged_in: false, }); }); @@ -1302,6 +1303,7 @@ describe('KimiTUI startup', () => { ); expect(harness.track).toHaveBeenCalledWith('login', { provider: 'managed:kimi-code', + method: 'oauth', already_logged_in: true, }); }); diff --git a/packages/acp-adapter/src/session.ts b/packages/acp-adapter/src/session.ts index 2a8aa5e0a..a0f1e3d7c 100644 --- a/packages/acp-adapter/src/session.ts +++ b/packages/acp-adapter/src/session.ts @@ -1314,7 +1314,7 @@ export class AcpSession { // event so dashboards stay coherent. this.emitTelemetry('question_dismissed'); } else { - this.emitTelemetry('question_answered'); + this.emitTelemetry('question_answered', { answered: Object.keys(answer).length }); } return answer; } catch (err) { diff --git a/packages/acp-adapter/test/session-question-handler.test.ts b/packages/acp-adapter/test/session-question-handler.test.ts index 937ab9da3..28536a308 100644 --- a/packages/acp-adapter/test/session-question-handler.test.ts +++ b/packages/acp-adapter/test/session-question-handler.test.ts @@ -163,7 +163,7 @@ describe('AcpSession.handleQuestion', () => { expect(req.toolCall.content).toEqual([ { type: 'content', content: { type: 'text', text: '哪个口味?' } }, ]); - expect(trackCalls).toEqual([{ event: 'question_answered', properties: undefined }]); + expect(trackCalls).toEqual([{ event: 'question_answered', properties: { answered: 1 } }]); }); it('skip: q0_skip resolves to null with question_dismissed telemetry', async () => { @@ -207,7 +207,7 @@ describe('AcpSession.handleQuestion', () => { // Telemetry: degraded(multi_question) first, then answered. expect(trackCalls).toEqual([ { event: 'question_degraded', properties: { reason: 'multi_question', dropped: 2 } }, - { event: 'question_answered', properties: undefined }, + { event: 'question_answered', properties: { answered: 1 } }, ]); // log.warn fired with the dropped count. expect(warnSpy).toHaveBeenCalledWith( @@ -236,7 +236,7 @@ describe('AcpSession.handleQuestion', () => { expect(raw.permissionRequests).toHaveLength(1); expect(trackCalls).toEqual([ { event: 'question_degraded', properties: { reason: 'multi_select' } }, - { event: 'question_answered', properties: undefined }, + { event: 'question_answered', properties: { answered: 1 } }, ]); }); diff --git a/packages/agent-core/src/agent/compaction/full.ts b/packages/agent-core/src/agent/compaction/full.ts index ef5c4206b..ebd8bfe18 100644 --- a/packages/agent-core/src/agent/compaction/full.ts +++ b/packages/agent-core/src/agent/compaction/full.ts @@ -6,6 +6,7 @@ import { } from '#/errors'; import { APIEmptyResponseError, + inputTotal, isRetryableGenerateError, type GenerateResult, type Message, @@ -413,29 +414,35 @@ export class FullCompaction { tokensAfter, }; + // Telemetry keys are snake_case, but the `context.apply_compaction` + // record written below keeps its persisted camelCase field names + // (consumed by external projectors). The two channels intentionally + // diverge — don't rename the record side to match. this.agent.telemetry.track('compaction_finished', { - tokensBefore: result.tokensBefore, - tokensAfter: result.tokensAfter, + source: data.source, + tokens_before: result.tokensBefore, + tokens_after: result.tokensAfter, duration_ms: Date.now() - startedAt, - compactedCount: result.compactedCount, - retryCount, + compacted_count: result.compactedCount, + retry_count: retryCount, round, - thinkingLevel: this.agent.config.thinkingLevel, - ...usage, - ...data, + thinking_level: this.agent.config.thinkingLevel, + ...(usage === null + ? {} + : { input_tokens: inputTotal(usage), output_tokens: usage.output }), }); this.agent.context.applyCompaction(result); return result; } catch (error) { if (isAbortError(error)) return; this.agent.telemetry.track('compaction_failed', { - ...data, - tokensBefore, + source: data.source, + tokens_before: tokensBefore, duration_ms: Date.now() - startedAt, round, - retryCount, - thinkingLevel: this.agent.config.thinkingLevel, - errorType: error instanceof Error ? error.name : 'Unknown', + retry_count: retryCount, + thinking_level: this.agent.config.thinkingLevel, + error_type: error instanceof Error ? error.name : 'Unknown', }); if (isKimiError(error) && error.code === ErrorCodes.AUTH_LOGIN_REQUIRED) throw error; throw new KimiError(ErrorCodes.COMPACTION_FAILED, String(error), { cause: error }); diff --git a/packages/agent-core/src/agent/compaction/micro.ts b/packages/agent-core/src/agent/compaction/micro.ts index 912812f60..a5acca6f8 100644 --- a/packages/agent-core/src/agent/compaction/micro.ts +++ b/packages/agent-core/src/agent/compaction/micro.ts @@ -71,7 +71,7 @@ export class MicroCompaction { const previousEffect = this.measureEffect(history, previousCutoff); const rawContextTokens = estimateTokensForMessages(history); // Whole-context length before/after this cutoff change, mirroring the - // `tokensBefore`/`tokensAfter` fields on `compaction_finished` so the + // `tokens_before`/`tokens_after` fields on `compaction_finished` so the // two compaction paths can be compared on the same axis. const tokensBefore = rawContextTokens - @@ -82,15 +82,21 @@ export class MicroCompaction { effect.truncatedToolResultTokensBefore + effect.truncatedToolResultTokensAfter; this.agent.telemetry.track('micro_compaction_finished', { - ...config, - ...effect, - tokensBefore, - tokensAfter, + keep_recent_messages: config.keepRecentMessages, + min_content_tokens: config.minContentTokens, + cache_missed_threshold_ms: config.cacheMissedThresholdMs, + truncated_marker: config.truncatedMarker, + min_context_usage_ratio: config.minContextUsageRatio, + truncated_tool_result_count: effect.truncatedToolResultCount, + truncated_tool_result_tokens_before: effect.truncatedToolResultTokensBefore, + truncated_tool_result_tokens_after: effect.truncatedToolResultTokensAfter, + tokens_before: tokensBefore, + tokens_after: tokensAfter, previous_cutoff: previousCutoff, cutoff: nextCutoff, message_count: history.length, cache_age_ms: cacheAgeMs, - thinkingLevel: this.agent.config.thinkingLevel, + thinking_level: this.agent.config.thinkingLevel, }); } } diff --git a/packages/agent-core/src/agent/tool/index.ts b/packages/agent-core/src/agent/tool/index.ts index c177a7529..78173f505 100644 --- a/packages/agent-core/src/agent/tool/index.ts +++ b/packages/agent-core/src/agent/tool/index.ts @@ -566,7 +566,7 @@ export class ToolManager { ...base, outcome: 'error', duration_ms: Date.now() - startedAt, - error: error instanceof Error ? error.message : String(error), + error_type: error instanceof Error ? error.name : 'Unknown', }); throw error; } diff --git a/packages/agent-core/src/rpc/core-impl.ts b/packages/agent-core/src/rpc/core-impl.ts index 6ad2a1cd5..0d7a30509 100644 --- a/packages/agent-core/src/rpc/core-impl.ts +++ b/packages/agent-core/src/rpc/core-impl.ts @@ -1141,19 +1141,16 @@ function telemetryErrorReason(error: unknown): string { function clientTelemetryProperties(client: ClientTelemetryInfo | undefined): TelemetryProperties { if (client === undefined) return {}; - const properties: Record = {}; - addNonEmpty(properties, 'client_id', client.id); - addNonEmpty(properties, 'client_name', client.name); - addNonEmpty(properties, 'client_version', client.version); - addNonEmpty(properties, 'ui_mode', client.uiMode); - return properties; -} - -function addNonEmpty(target: Record, key: string, value: string | undefined): void { - const trimmed = value?.trim(); - if (trimmed !== undefined && trimmed.length > 0) { - target[key] = trimmed; - } + // Emit a fixed key set (null when the client did not provide a field) so + // `session_started` has a stable schema across clients, matching the harness + // producer in `kimi-harness.ts`. Other session events also inherit these as + // context properties, so they share the same stable client-attribution shape. + return { + client_id: client.id ?? null, + client_name: client.name ?? null, + client_version: client.version ?? null, + ui_mode: client.uiMode ?? null, + }; } async function resumeSessionResult( diff --git a/packages/agent-core/test/agent/compaction/full.test.ts b/packages/agent-core/test/agent/compaction/full.test.ts index 2e912d625..8adf12c6e 100644 --- a/packages/agent-core/test/agent/compaction/full.test.ts +++ b/packages/agent-core/test/agent/compaction/full.test.ts @@ -235,17 +235,14 @@ describe('FullCompaction', () => { event: 'compaction_finished', properties: expect.objectContaining({ source: 'manual', - instruction: 'Keep the important test facts.', - tokensBefore: 39, - tokensAfter: 5, + tokens_before: 39, + tokens_after: 5, duration_ms: expect.any(Number), - compactedCount: 6, - retryCount: 0, - thinkingLevel: 'off', - inputOther: 520, - output: 8, - inputCacheRead: 0, - inputCacheCreation: 0, + compacted_count: 6, + retry_count: 0, + thinking_level: 'off', + input_tokens: 520, + output_tokens: 8, }), }); await ctx.expectResumeMatches(); @@ -514,8 +511,8 @@ describe('FullCompaction', () => { event: 'compaction_finished', properties: expect.objectContaining({ source: 'manual', - tokensBefore: 25, - retryCount: 1, + tokens_before: 25, + retry_count: 1, }), }); await ctx.expectResumeMatches(); @@ -650,8 +647,8 @@ describe('FullCompaction', () => { event: 'compaction_failed', properties: expect.objectContaining({ source: 'manual', - retryCount: 4, - errorType: 'APIEmptyResponseError', + retry_count: 4, + error_type: 'APIEmptyResponseError', }), }); // No summary was ever applied; the original history is left intact. @@ -764,16 +761,16 @@ describe('FullCompaction', () => { event: 'compaction_failed', properties: expect.objectContaining({ source: 'manual', - tokensBefore: 25, + tokens_before: 25, duration_ms: expect.any(Number), round: 1, - retryCount: 0, - errorType: 'Error', + retry_count: 0, + error_type: 'Error', }), }); expect( records.find((record) => record.event === 'compaction_failed')?.properties, - ).not.toHaveProperty('tokensAfter'); + ).not.toHaveProperty('tokens_after'); await ctx.expectResumeMatches(); }); @@ -878,10 +875,10 @@ describe('FullCompaction', () => { event: 'compaction_failed', properties: expect.objectContaining({ source: 'manual', - tokensBefore: 25, + tokens_before: 25, duration_ms: expect.any(Number), - retryCount: 4, - errorType: 'APIConnectionError', + retry_count: 4, + error_type: 'APIConnectionError', }), }); await ctx.expectResumeMatches(); @@ -1212,10 +1209,10 @@ describe('FullCompaction', () => { event: 'compaction_finished', properties: expect.objectContaining({ source: 'auto', - tokensBefore: 46, - tokensAfter: 28, - compactedCount: 4, - retryCount: 0, + tokens_before: 46, + tokens_after: 28, + compacted_count: 4, + retry_count: 0, }), }); await ctx.expectResumeMatches(); @@ -1740,7 +1737,7 @@ describe('FullCompaction', () => { event: 'compaction_finished', properties: expect.objectContaining({ source: 'auto', - thinkingLevel: 'high', + thinking_level: 'high', }), }); }); diff --git a/packages/agent-core/test/agent/compaction/micro.test.ts b/packages/agent-core/test/agent/compaction/micro.test.ts index 91be825d1..edc931aaa 100644 --- a/packages/agent-core/test/agent/compaction/micro.test.ts +++ b/packages/agent-core/test/agent/compaction/micro.test.ts @@ -471,24 +471,27 @@ describe('MicroCompaction', () => { const event = singleTelemetryEvent(records, 'micro_compaction_finished'); expect(event.properties).toMatchObject({ - ...microCompaction, - truncatedMarker: DEFAULT_MARKER, + keep_recent_messages: microCompaction.keepRecentMessages, + min_content_tokens: microCompaction.minContentTokens, + cache_missed_threshold_ms: microCompaction.cacheMissedThresholdMs, + truncated_marker: DEFAULT_MARKER, + min_context_usage_ratio: microCompaction.minContextUsageRatio, previous_cutoff: 0, cutoff: 7, message_count: 9, cache_age_ms: 61 * MINUTE, - truncatedToolResultCount: 2, - truncatedToolResultTokensBefore: expect.any(Number), - truncatedToolResultTokensAfter: expect.any(Number), - tokensBefore: expect.any(Number), - tokensAfter: expect.any(Number), - thinkingLevel: 'off', - }); - expect(numberProperty(event, 'truncatedToolResultTokensBefore')).toBeGreaterThan( - numberProperty(event, 'truncatedToolResultTokensAfter'), + truncated_tool_result_count: 2, + truncated_tool_result_tokens_before: expect.any(Number), + truncated_tool_result_tokens_after: expect.any(Number), + tokens_before: expect.any(Number), + tokens_after: expect.any(Number), + thinking_level: 'off', + }); + expect(numberProperty(event, 'truncated_tool_result_tokens_before')).toBeGreaterThan( + numberProperty(event, 'truncated_tool_result_tokens_after'), ); - expect(numberProperty(event, 'tokensBefore')).toBeGreaterThan( - numberProperty(event, 'tokensAfter'), + expect(numberProperty(event, 'tokens_before')).toBeGreaterThan( + numberProperty(event, 'tokens_after'), ); expect(ctx.agent.context.messages).toHaveLength(9); @@ -533,9 +536,9 @@ describe('MicroCompaction', () => { expect(secondEvent.properties).toMatchObject({ previous_cutoff: 4, cutoff: 7, - truncatedToolResultCount: 2, - tokensBefore: expectedContextTokensBefore, - tokensAfter: estimateTokensForMessages(ctx.agent.context.messages), + truncated_tool_result_count: 2, + tokens_before: expectedContextTokensBefore, + tokens_after: estimateTokensForMessages(ctx.agent.context.messages), }); }); diff --git a/packages/node-sdk/src/kimi-harness.ts b/packages/node-sdk/src/kimi-harness.ts index 53da798e3..d503143ea 100644 --- a/packages/node-sdk/src/kimi-harness.ts +++ b/packages/node-sdk/src/kimi-harness.ts @@ -265,6 +265,11 @@ export class KimiHarness { ...sessionScoped, // Canonical fields are owned by the harness and must win over any // caller-supplied sessionStartedProperties that happen to share a key. + // `client_id` is always null here: a single-process host has no + // per-connection client id (that concept only exists for daemon clients, + // see core-impl.ts). Kept as an explicit key so both producers share the + // same session_started schema. + client_id: null, client_name: this.identity?.userAgentProduct ?? null, client_version: this.identity?.version ?? null, ui_mode: this.uiMode, diff --git a/packages/node-sdk/test/create-session-transport.test.ts b/packages/node-sdk/test/create-session-transport.test.ts index 1210719be..eb3168ead 100644 --- a/packages/node-sdk/test/create-session-transport.test.ts +++ b/packages/node-sdk/test/create-session-transport.test.ts @@ -111,6 +111,7 @@ describe('KimiHarness.createSession transport link', () => { event: 'session_started', sessionId: session.id, properties: { + client_id: null, client_name: 'kimi-code-cli', client_version: '0.0.0-test', ui_mode: 'shell', @@ -132,6 +133,7 @@ describe('KimiHarness.createSession transport link', () => { event: 'session_started', sessionId: session.id, properties: { + client_id: null, client_name: 'kimi-code-cli', client_version: '0.0.0-test', ui_mode: 'shell', @@ -169,6 +171,7 @@ describe('KimiHarness.createSession transport link', () => { event: 'session_started', sessionId: session.id, properties: { + client_id: null, client_name: 'kimi-code-cli', client_version: '0.0.0-test', ui_mode: 'print', @@ -201,6 +204,7 @@ describe('KimiHarness.createSession transport link', () => { event: 'session_started', sessionId: session.id, properties: { + client_id: null, client_name: 'kimi-code-cli', client_version: '0.0.0-test', ui_mode: 'shell', @@ -236,6 +240,7 @@ describe('KimiHarness.createSession transport link', () => { event: 'session_started', sessionId: session.id, properties: { + client_id: null, client_name: 'kimi-code-cli', client_version: '0.0.0-test', ui_mode: 'shell', @@ -255,6 +260,7 @@ describe('KimiHarness.createSession transport link', () => { event: 'session_started', sessionId: session.id, properties: { + client_id: null, client_name: 'kimi-code-cli', client_version: '0.0.0-test', ui_mode: 'shell', @@ -295,6 +301,7 @@ describe('KimiHarness.createSession transport link', () => { event: 'session_started', sessionId: session.id, properties: { + client_id: null, client_name: 'kimi-code-cli', client_version: '0.0.0-test', ui_mode: 'shell', @@ -333,6 +340,7 @@ describe('KimiHarness.createSession transport link', () => { event: 'session_started', sessionId: forked.id, properties: { + client_id: null, client_name: 'kimi-code-cli', client_version: '0.0.0-test', ui_mode: 'shell', @@ -368,6 +376,7 @@ describe('KimiHarness.createSession transport link', () => { event: 'session_started', sessionId: session.id, properties: { + client_id: null, client_name: null, client_version: null, ui_mode: 'shell',