refactor(desktop): drop server_mode and fold runtime locale into track

This commit is contained in:
7Sageer 2026-07-29 13:00:48 +08:00
parent 0c41ca675a
commit 0aa8eea38b
9 changed files with 37 additions and 77 deletions

View file

@ -138,7 +138,7 @@
- [x] **埋点扩充(观测指标文档批次)**已完成desktop 专属2026-07
- 依据飞书《桌面端观测指标 & 埋点补充》:新增事件——主进程 `app_launched`launch_intentjump-list argv 判定)、`startup_timing`main_ready / window_shown / renderer_loaded / renderer_ready 四阶段,`process.uptime()` 累计)、`renderer_crashed`render-process-gone 的 reason + exit_codeElectron 新枚举 memory-eviction 折叠为 oom`startup_connect_result`ok / failure_phase / retry_count / error_classconnect.ts 失败分类纯函数)、`startup_failure_screen_shown`renderer `session_created`kind new/resumed + source见下`notification_shown/clicked`kind 由通知 tag 前缀映射)、`onboarding_completed/abandoned`wizard 记时step1 skip=abandoned、step2 skip=completed`search_opened/executed`TranscriptSearch仅查询驱动上报MutationObserver 流式刷新不报,结果数分桶)、`logout``plan_usage_card_viewed`PlanUsageCard 加 active prop——Account 面板是 v-showonMounted 不等于曝光usage_bucket 取各行最严重级别)、`telemetry_consent_changed``renderer_error`trace.ts 的 window error/unhandledrejection只报 Error.name`connection_lost/restored`useKimiWebClient 连接状态迁移,首次连接前的断开不报)、`workspace_added/removed`workspace_count 为操作后总数)。
- 参数修订:`app_crashed` + process(main/gpugpu 来自 child-process-gone)+app_uptime_ms`update_status_changed` version→to_version + from_version/prev_state/error_class`window_lifecycle` + reason(quit/close_to_tray/deactivate)/visible_duration_msclose→hide 路径=close_to_tray、minimize=deactivate、before-quit=quit`approval_decision` + request_idApprovalCard 加可选 prop父级透传 approvalId`onboarding_step` 改为**离开步骤时**上报 + step_index/total_steps/duration_ms`oauth_login_step` + method/duration_ms/error_classstart_failed/poll_failed/expired/cancelled`settings_changed` + source_panelLanguageSwitcher 与 useUpdateStatus.setAutoDownload 需调用方传参区分面板);`attachment_added` + size_bucket/count`ui_element_toggled` + sample_rateliteral 1未真实采样`system_metrics` + window_count 与 session_count后者经内嵌 core 的 `ISessionIndex.countActive` 进程内查询external 模式省略)。
- 公共参数注入:`telemetry.ts` 的 track impl 包装给每个事件合并 server_modeconnect.ts 解析后写 `runtime-context.ts` 新模块,避免循环依赖)/ app_uptime_ms / days_since_installdevice_id 文件 birthtime/ locale`kimi:locale` 推送经 tray.ts 落入 runtime-context/ themenativeTheme事件自带字段优先。
- 公共参数注入:`telemetry.ts` 的 track impl 包装给每个事件合并 app_uptime_ms / days_since_installdevice_id 文件 birthtime/ locale`kimi:locale` 推送经 tray.ts 落入 `track.ts`/ themenativeTheme事件自带字段优先。server_mode 与 `runtime-context.ts` 已于 2026-07 移除:外部 server 仅 dev 联调、生产恒 embedded该维度无分析价值locale 状态并入 `track.ts`
- **启动事件缓冲**`track.ts` 在 impl 安装前缓冲事件(上限 200 丢最旧),`setDesktopTrackImpl` 时回放——telemetry 要等内嵌 server 就绪才接线,此前 app_launched/startup_timing 全丢window_lifecycle 的原 replayWindowLifecycle 随之删除(缓冲覆盖)。外部 server 模式KIMI_SERVER_URLdev 联调用)仍无 telemetry——appender 挂在内嵌 core 上,生产无影响。注意口径:回放事件的管道时间戳与 super property `app_uptime_ms` 取**接线时刻**(非事件发生时刻,偏移为 shell-env 探测 + server 启动耗时;`startup_timing` 自带的 `duration_ms` 不受影响)。
- `session_created` 的 source 归因:`lib/session-intent.ts`**不同步 web**,同 track.ts 先例)模块级 pending intent入口点runShortcutAction 按 source 映射 / jump-list 回调 / tray / 通知 onclick / 搜索框选择 / 斜杠命令)先 set`createDraftSession`kind:new`selectSession`kind:resumedconsumecreateDraftSession 内部 selectSession 传 skipTrack 去重;**自动恢复路径全部 skipTrack**(首载 deep-link/自动选择、popstate 死链兜底、归档/会话消失兜底)——漏斗只计用户主动打开。
- 未做(需 kimi-code core / 服务端 / 产品 UI 先行upgrade_clicked 与 limit_reached_shown无升级/限额 UIManagedUsageResult 无 plan_type、user_id / install_id / ingest_ts / is_backfilled管道侧、consent 热切换(维持重启生效——由此 `telemetry_consent_changed{enabled:true}` 永不送达consent 关闭时管线不接线,事件只进内存缓冲并随退出丢弃;`enabled:false` 方向正常送达)。

View file

@ -13,7 +13,6 @@ import { isOnboarded, isVibrancyEnabled } from './ui-state';
import { resolveConnectTarget } from './connect-target';
import { dataUrl, errorHtml } from './screens';
import { log, redactUrlForLog } from './log';
import { setServerMode } from './runtime-context';
import { trackDesktopEvent } from './track';
import type { StartupConnectResultEvent } from './telemetry-events';
import { DESKTOP_PRODUCT_NAME } from '../shared/identity';
@ -136,7 +135,6 @@ async function connectOnce(win: BrowserWindow): Promise<void> {
// builds always use `app://renderer`.
const devBase = app.isPackaged ? undefined : rendererDevBase(process.env['KIMI_RENDERER_DEV_URL']);
const target = resolveConnectTarget(process.env['KIMI_SERVER_URL'], readServerToken);
setServerMode(target.external ? 'external' : 'embedded');
if (target.external) {
({ origin, token } = target);
// Redact: KIMI_SERVER_URL may carry basic-auth userinfo, which must not

View file

@ -1,25 +0,0 @@
// Module-level runtime context, merged into every telemetry event as super
// properties (telemetry.ts). Kept dependency-free so connect.ts / tray.ts can
// write it without pulling the telemetry wiring module (agent-core-v2) into
// their load-time graph.
export type ServerMode = 'embedded' | 'external';
let serverMode: ServerMode | undefined;
let locale: string | undefined;
export function setServerMode(mode: ServerMode | undefined): void {
serverMode = mode;
}
export function getServerMode(): ServerMode | undefined {
return serverMode;
}
export function setRuntimeLocale(next: string | undefined): void {
locale = next;
}
export function getRuntimeLocale(): string | undefined {
return locale;
}

View file

@ -16,9 +16,8 @@ import { resolveKimiHome } from '@moonshot-ai/kimi-code-sdk';
import { DESKTOP_PRODUCT_NAME, DESKTOP_UI_MODE } from '../shared/identity';
import { log } from './log';
import { getRuntimeLocale, getServerMode } from './runtime-context';
import { startDesktopSystemMetrics, stopDesktopSystemMetrics } from './system-metrics';
import { setDesktopTrackImpl } from './track';
import { getRuntimeLocale, setDesktopTrackImpl } from './track';
export interface DesktopTelemetryHandle {
/** Emits `exit`, flushes the buffer, stops periodic flush. Idempotent. */
@ -82,8 +81,6 @@ function withSuperProperties(properties: TelemetryProperties | undefined): Telem
app_uptime_ms: Math.round(process.uptime() * 1000),
theme: nativeTheme.shouldUseDarkColors ? 'dark' : 'light',
};
const serverMode = getServerMode();
if (serverMode !== undefined) injected['server_mode'] = serverMode;
const daysSinceInstall = readDaysSinceInstall();
if (daysSinceInstall !== undefined) injected['days_since_install'] = daysSinceInstall;
const locale = getRuntimeLocale();

View file

@ -1,6 +1,8 @@
// Host telemetry facade. Events fired before embedded telemetry is wired (app
// launch, startup phases) are buffered and replayed once wiring completes;
// without an impl the buffer is the only place they exist, so it stays small.
// Also holds the runtime locale (renderer-pushed via tray.ts) that
// telemetry.ts merges into every event as the `locale` super property.
import type { TelemetryProperties } from '@moonshot-ai/agent-core-v2';
@ -44,3 +46,13 @@ export function trackDesktopEvent<K extends DesktopEventName>(
}
impl(event, props);
}
let locale: string | undefined;
export function setRuntimeLocale(next: string | undefined): void {
locale = next;
}
export function getRuntimeLocale(): string | undefined {
return locale;
}

View file

@ -3,8 +3,7 @@ import { join } from 'node:path';
import { app, Menu, nativeImage, Tray } from 'electron';
import type { MenuItemConstructorOptions } from 'electron';
import { trackDesktopEvent } from './track';
import { setRuntimeLocale } from './runtime-context';
import { setRuntimeLocale, trackDesktopEvent } from './track';
import { setTaskbarAttention } from './taskbar';
// System tray (macOS menu-bar / Windows notification area). Desktop-only — the

View file

@ -413,19 +413,6 @@ describe('startup_connect_result', () => {
expect.anything(),
);
});
it('records the server mode in the runtime context', async () => {
const { connect } = await importConnect();
const { getServerMode } = await import('../../src/main/runtime-context');
mocks.startDesktopServer.mockResolvedValue(fakeHandle());
await connect(fakeWindow() as unknown as BrowserWindow);
expect(getServerMode()).toBe('embedded');
process.env['KIMI_SERVER_URL'] = 'http://127.0.0.1:58627';
await connect(fakeWindow() as unknown as BrowserWindow);
expect(getServerMode()).toBe('external');
});
});
describe('classifyConnectFailure', () => {

View file

@ -38,13 +38,11 @@ import {
resetDaysSinceInstallCacheForTests,
wireDesktopTelemetry,
} from '../../src/main/telemetry';
import { setDesktopTrackImpl, trackDesktopEvent } from '../../src/main/track';
import {
getRuntimeLocale,
getServerMode,
setDesktopTrackImpl,
setRuntimeLocale,
setServerMode,
} from '../../src/main/runtime-context';
trackDesktopEvent,
} from '../../src/main/track';
const EXISTING_DEVICE = { deviceId: 'device-1', firstLaunch: false } as const;
@ -272,7 +270,6 @@ describe('super properties injection', () => {
beforeEach(async () => {
createCloudAppenderMock.mockReset();
nativeThemeMock.shouldUseDarkColors = false;
setServerMode(undefined);
setRuntimeLocale(undefined);
resetDaysSinceInstallCacheForTests();
homeDir = await mkdtemp(join(tmpdir(), 'kimi-telemetry-'));
@ -281,7 +278,6 @@ describe('super properties injection', () => {
afterEach(() => {
setDesktopTrackImpl(null);
setServerMode(undefined);
setRuntimeLocale(undefined);
vi.unstubAllEnvs();
});
@ -295,7 +291,6 @@ describe('super properties injection', () => {
await writeFile(join(homeDir, 'device_id'), 'device-1');
wiredTrack();
const { core, telemetryService } = makeCore(undefined);
setServerMode('embedded');
setRuntimeLocale('zh');
const handle = await wireDesktopTelemetry(core as never, EXISTING_DEVICE);
@ -304,7 +299,6 @@ describe('super properties injection', () => {
trackDesktopEvent('window_lifecycle', { action: 'shown' });
expect(telemetryService.track).toHaveBeenCalledWith('window_lifecycle', {
action: 'shown',
server_mode: 'embedded',
locale: 'zh',
theme: 'light',
days_since_install: 0,
@ -322,8 +316,8 @@ describe('super properties injection', () => {
});
it('omits context values that are unset or unreadable', async () => {
// No device_id file in homeDir → days_since_install is dropped; server_mode
// and locale stay unset.
// No device_id file in homeDir → days_since_install is dropped; locale
// stays unset.
wiredTrack();
const { core, telemetryService } = makeCore(undefined);
@ -332,7 +326,6 @@ describe('super properties injection', () => {
trackDesktopEvent('global_shortcut_invoked', {});
const properties = telemetryService.track.mock.calls.at(-1)?.[1] as Record<string, unknown>;
expect(properties['server_mode']).toBeUndefined();
expect(properties['locale']).toBeUndefined();
expect(properties['days_since_install']).toBeUndefined();
expect(properties['app_uptime_ms']).toEqual(expect.any(Number));
@ -347,30 +340,14 @@ describe('super properties injection', () => {
nativeThemeMock.shouldUseDarkColors = true;
const handle = await wireDesktopTelemetry(core as never, EXISTING_DEVICE);
setServerMode('external');
setRuntimeLocale('en');
trackDesktopEvent('global_shortcut_invoked', {});
expect(telemetryService.track).toHaveBeenCalledWith(
'global_shortcut_invoked',
expect.objectContaining({ server_mode: 'external', locale: 'en', theme: 'dark' }),
expect.objectContaining({ locale: 'en', theme: 'dark' }),
);
await handle!.shutdown();
});
});
describe('runtime-context', () => {
it('stores server_mode and locale, defaulting to undefined', () => {
expect(getServerMode()).toBeUndefined();
expect(getRuntimeLocale()).toBeUndefined();
setServerMode('embedded');
setRuntimeLocale('zh');
expect(getServerMode()).toBe('embedded');
expect(getRuntimeLocale()).toBe('zh');
setServerMode(undefined);
setRuntimeLocale(undefined);
expect(getServerMode()).toBeUndefined();
expect(getRuntimeLocale()).toBeUndefined();
});
});

View file

@ -1,7 +1,12 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { asRendererTrackEvent } from '../../src/main/renderer-track-validation';
import { setDesktopTrackImpl, trackDesktopEvent } from '../../src/main/track';
import {
getRuntimeLocale,
setDesktopTrackImpl,
setRuntimeLocale,
trackDesktopEvent,
} from '../../src/main/track';
beforeEach(() => {
setDesktopTrackImpl(null);
@ -78,6 +83,16 @@ describe('trackDesktopEvent', () => {
});
});
describe('runtime locale', () => {
it('stores the pushed locale, defaulting to undefined', () => {
expect(getRuntimeLocale()).toBeUndefined();
setRuntimeLocale('zh');
expect(getRuntimeLocale()).toBe('zh');
setRuntimeLocale(undefined);
expect(getRuntimeLocale()).toBeUndefined();
});
});
describe('asRendererTrackEvent', () => {
it('accepts action_invoked with a valid source', () => {
expect(