diff --git a/apps/desktop/src/account-backend.ts b/apps/desktop/src/account-backend.ts index bb79bbb526..770d3e4e9d 100644 --- a/apps/desktop/src/account-backend.ts +++ b/apps/desktop/src/account-backend.ts @@ -67,10 +67,13 @@ export interface DesktopAccountBackend { * @param listener - state recipient. * @param failed - stream failure recipient. * @param expired - live credential-expiry recipient. - * @param policy - optional Desktop collection-policy recipient; disconnected streams publish false. + * @param onAnalyticsEnabledChanged - optional Desktop collection-policy recipient; disconnected streams publish false. * @returns stream disposer. */ - watch(listener: (state: AccountView) => void, failed: () => void, expired: () => void, policy?: (enabled: boolean) => void): () => void + watch( + listener: (state: AccountView) => void, failed: () => void, expired: () => void, + onAnalyticsEnabledChanged?: (enabled: boolean) => void, + ): () => void } /** @@ -87,21 +90,21 @@ export function desktopAccountBackend(origin: string, invoke: AccountInvoke, coo state: () => call('getState'), start: client => call('startSignIn', { client, callbackOrigin: new URL(origin).origin, loginSource: 'desktop' }), cancel: attemptId => call('cancelSignIn', { attemptId }), signOut: client => call('signOut', { client }), - watch(listener, failed, expired, policy) { + watch(listener, failed, expired, onAnalyticsEnabledChanged) { let closed = false let socket: WebSocket | undefined let retry: ReturnType | undefined const connect = (): void => { const streamId = randomUUID() const expiryStreamId = randomUUID() - const policyStreamId = randomUUID() + const analyticsPolicyStreamId = randomUUID() void cookies().then((cookie) => { if (closed) return const url = new URL(REMOTE_STREAM_MUX_PATH, origin) url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:' socket = new WebSocket(url, { headers: { cookie, origin }, maxPayload: 65_536 }) socket.on('open', () => { - if (policy !== undefined) socket?.send(JSON.stringify({ type: 'open', streamId: policyStreamId, endpoint: 'productAnalytics/watchPolicy', payload: { args: {} } })) + if (onAnalyticsEnabledChanged !== undefined) socket?.send(JSON.stringify({ type: 'open', streamId: analyticsPolicyStreamId, endpoint: 'productAnalytics/watchPolicy', payload: { args: {} } })) socket?.send(JSON.stringify({ type: 'open', streamId: expiryStreamId, endpoint: 'account/watchExpiry', payload: { args: {} } })) socket?.send(JSON.stringify({ type: 'open', streamId, endpoint: 'account/watch', payload: { args: {} } })) }) @@ -109,9 +112,9 @@ export function desktopAccountBackend(origin: string, invoke: AccountInvoke, coo try { const bytes = Array.isArray(data) ? Buffer.concat(data) : Buffer.isBuffer(data) ? data : Buffer.from(data) const frame = parseRemoteStreamServerMessage(bytes.toString('utf8')) - if (frame.streamId === policyStreamId) { - if (frame.type === 'item' && typeof frame.value === 'boolean') policy?.(frame.value) - else policy?.(false) + if (frame.streamId === analyticsPolicyStreamId) { + if (frame.type === 'item' && typeof frame.value === 'boolean') onAnalyticsEnabledChanged?.(frame.value) + else onAnalyticsEnabledChanged?.(false) return } if (frame.streamId === expiryStreamId && frame.type === 'item' && frame.value === 'session-expired') { expired(); return } @@ -121,11 +124,11 @@ export function desktopAccountBackend(origin: string, invoke: AccountInvoke, coo } catch { socket?.close() } }) socket.on('error', () => { socket?.close() }) - socket.on('close', () => { if (!closed) { policy?.(false); failed(); retry = setTimeout(connect, 1000) } }) - }).catch(() => { if (!closed) { policy?.(false); failed(); retry = setTimeout(connect, 1000) } }) + socket.on('close', () => { if (!closed) { onAnalyticsEnabledChanged?.(false); failed(); retry = setTimeout(connect, 1000) } }) + }).catch(() => { if (!closed) { onAnalyticsEnabledChanged?.(false); failed(); retry = setTimeout(connect, 1000) } }) } connect() - return () => { closed = true; policy?.(false); clearTimeout(retry); socket?.close() } + return () => { closed = true; onAnalyticsEnabledChanged?.(false); clearTimeout(retry); socket?.close() } }, } } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 9f4e778d4f..eb75980ad7 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -45,7 +45,7 @@ import { DesktopUpdateSchedule, resolveDesktopUpdateScheduleConfig } from './upd import { desktopUpdateErrorSummary, presentDesktopUpdate } from './update-presentation.ts' import { desktopErrorState } from './startup-error.ts' import { DesktopMandatoryUpdatePolicy, resolveDesktopPolicyConfig, type DesktopPolicyState } from './mandatory-update-policy.ts' -import { desktopClientMetadata } from './client-metadata.ts' +import { desktopClientMetadata, desktopClientVersion } from './client-metadata.ts' import { DesktopMandatoryUpdateWindow } from './mandatory-update-window.ts' import { DesktopPolicyTestAuth } from './policy-test-auth.ts' import { DesktopUpdateDialog, type UpdateDialogOptions } from './update-dialog.ts' @@ -417,7 +417,7 @@ async function main(): Promise { const backend = new DesktopBackendController((onFailure) => { const hostInspectPort = developmentHostInspectPort(development) const host = new DesktopHostProcess(resources.node, resources.dsh, activeProject, - hostInspectPort, { ...process.env, DSH_PRODUCT_ANALYTICS_APP_VERSION: app.getVersion() }, onFailure, + hostInspectPort, { ...process.env, DSH_CLIENT_VERSION: desktopClientVersion() }, onFailure, primaryRuntime, resources, (next) => { platformView.setSession(next) }) return { diff --git a/apps/desktop/tests/account-backend.spec.ts b/apps/desktop/tests/account-backend.spec.ts index 148c7e0a7d..b9cee7b61a 100644 --- a/apps/desktop/tests/account-backend.spec.ts +++ b/apps/desktop/tests/account-backend.spec.ts @@ -49,12 +49,14 @@ it('receives live expiry separately from account snapshots', async () => { if (typeof address === 'string' || address === null) throw new Error('missing server address') const connected = once(server, 'connection') const expired = vi.fn() - const policy = vi.fn() + const onAnalyticsEnabledChanged = vi.fn() const snapshot = { status: 'signed-out', attempt: null, links: { usageUrl: 'https://platform.deepseek.com/usage', topUpUrl: 'https://platform.deepseek.com/top_up' } } const received = Promise.withResolvers() const backend = desktopAccountBackend(`http://127.0.0.1:${address.port}`, () => Promise.resolve(snapshot), () => Promise.resolve('')) - const stop = backend.watch((value) => { expect(value).toEqual(snapshot); received.resolve(undefined) }, vi.fn(), expired, policy) + const stop = backend.watch( + (value) => { expect(value).toEqual(snapshot); received.resolve(undefined) }, vi.fn(), expired, onAnalyticsEnabledChanged, + ) onTestFinished(stop) await connected // Logical streams share the socket; observe their opening frames before sending data. @@ -71,13 +73,13 @@ it('receives live expiry separately from account snapshots', async () => { socket.send(JSON.stringify({ type: 'item', streamId: streams.get('account/watch'), value: snapshot })) await received.promise socket.send(JSON.stringify({ type: 'item', streamId: streams.get('productAnalytics/watchPolicy'), value: true })) - await vi.waitFor(() => { expect(policy).toHaveBeenLastCalledWith(true) }) + await vi.waitFor(() => { expect(onAnalyticsEnabledChanged).toHaveBeenLastCalledWith(true) }) socket.send(JSON.stringify({ type: 'item', streamId: streams.get('productAnalytics/watchPolicy'), value: false })) - await vi.waitFor(() => { expect(policy).toHaveBeenLastCalledWith(false) }) + await vi.waitFor(() => { expect(onAnalyticsEnabledChanged).toHaveBeenLastCalledWith(false) }) socket.send(JSON.stringify({ type: 'item', streamId: streams.get('productAnalytics/watchPolicy'), value: true })) - await vi.waitFor(() => { expect(policy).toHaveBeenLastCalledWith(true) }) + await vi.waitFor(() => { expect(onAnalyticsEnabledChanged).toHaveBeenLastCalledWith(true) }) socket.send(JSON.stringify({ type: 'item', streamId: streams.get('productAnalytics/watchPolicy'), value: 'invalid policy' })) - await vi.waitFor(() => { expect(policy).toHaveBeenLastCalledWith(false) }) + await vi.waitFor(() => { expect(onAnalyticsEnabledChanged).toHaveBeenLastCalledWith(false) }) expect(expired).not.toHaveBeenCalled() socket.send(JSON.stringify({ type: 'item', streamId: streams.get('account/watchExpiry'), value: 'session-expired' })) await vi.waitFor(() => { expect(expired).toHaveBeenCalledOnce() }) diff --git a/apps/desktop/tests/main-startup.spec.ts b/apps/desktop/tests/main-startup.spec.ts index f1e14c314d..abce2c0bbc 100644 --- a/apps/desktop/tests/main-startup.spec.ts +++ b/apps/desktop/tests/main-startup.spec.ts @@ -173,7 +173,7 @@ const harness = await vi.hoisted(async () => { }) let accountListener: ((state: AccountView) => void) | undefined let analyticsEnabled = true - let policyListener: ((enabled: boolean) => void) | undefined + let analyticsEnabledListener: ((enabled: boolean) => void) | undefined const analytics = vi.fn(async (_event: unknown) => {}) const nativeTheme = { themeSource: 'system', shouldUseDarkColors: false } const trays: FakeTray[] = [] @@ -194,13 +194,14 @@ const harness = await vi.hoisted(async () => { platformCloseAndWait, get analyticsEnabled() { return analyticsEnabled }, - set analyticsEnabled(value: boolean) { analyticsEnabled = value; policyListener?.(value) }, + set analyticsEnabled(value: boolean) { analyticsEnabled = value; analyticsEnabledListener?.(value) }, watchAccount: ( - listener: (state: AccountView) => void, _failed: () => void, _expired: () => void, policy?: (enabled: boolean) => void, + listener: (state: AccountView) => void, _failed: () => void, _expired: () => void, + onAnalyticsEnabledChanged?: (enabled: boolean) => void, ) => { - policyListener = policy + analyticsEnabledListener = onAnalyticsEnabledChanged accountListener = listener - return () => { accountListener = undefined; policyListener = undefined } + return () => { accountListener = undefined; analyticsEnabledListener = undefined } }, publishAccount(state: AccountView) { accountListener?.(state) }, ipcOn: vi.fn<(channel: string, listener: (event: { sender: unknown; senderFrame: unknown }, ...args: unknown[]) => void) => void>(), @@ -2064,7 +2065,7 @@ describe('desktop main startup', () => { profile: 'desktop-test-profile', }) expect(harness.hosts[0]!.environment).not.toBe(process.env) - expect(harness.hosts[0]!.environment?.DSH_PRODUCT_ANALYTICS_APP_VERSION).toBe('1.0.0') + expect(harness.hosts[0]!.environment?.DSH_CLIENT_VERSION).toBe('1.2.3') expect(harness.analytics).toHaveBeenCalledExactlyOnceWith({ eventName: 'desktop_app_launch', timestamp: Date.now(), attributes: {} }) expect(harness.hosts[0]!.start).toHaveBeenCalledTimes(1) expect(harness.windows).toHaveLength(1) diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index 757c0922ae..cf2d277132 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -50,7 +50,7 @@ exportTimeoutMillis: 1500 shutdownTimeoutMillis: 2000 serviceName: deepseek-harness-desktop - serviceVersion: !!js process.env.DSH_PRODUCT_ANALYTICS_APP_VERSION + serviceVersion: !!js process.env.DSH_CLIENT_VERSION endpoint: !!js process.env.DSH_PRODUCT_ANALYTICS_OTLP_URL - id: product-analytics @@ -58,7 +58,7 @@ disabled: !!js "ctx.get('profileContext')?.name !== 'desktop'" config: enabled: true - appVersion: !!js process.env.DSH_PRODUCT_ANALYTICS_APP_VERSION + appVersion: !!js process.env.DSH_CLIENT_VERSION # Host-owned opt-in sampled when a new Web session receives its preset # delegation tools. The Plugins page edits this settings namespace. diff --git a/packages/bundle/web-app/tests/product-analytics.spec.ts b/packages/bundle/web-app/tests/product-analytics.spec.ts index fbafdaae4d..178d775d6f 100644 --- a/packages/bundle/web-app/tests/product-analytics.spec.ts +++ b/packages/bundle/web-app/tests/product-analytics.spec.ts @@ -20,7 +20,7 @@ it.each(['desktop', 'web'])('limits collection and its shutdown to the Desktop l onTestFinished(async () => { const closed = once(server, 'close'); server.close(); server.closeAllConnections(); await closed }) const address = server.address() if (address === null || typeof address === 'string') throw new Error('missing collector address') - vi.stubEnv('DSH_PRODUCT_ANALYTICS_APP_VERSION', 'test-version') + vi.stubEnv('DSH_CLIENT_VERSION', 'test-version') vi.stubEnv('DSH_PRODUCT_ANALYTICS_OTLP_URL', `http://127.0.0.1:${address.port}/logs`) const ctx = new Context() onTestFinished(() => ctx.fiber.dispose()) @@ -68,6 +68,9 @@ it.each(['desktop', 'web'])('limits collection and its shutdown to the Desktop l expect(identity).not.toHaveBeenCalled() } } + const emit = vi.spyOn(ctx.productTelemetry, 'emit') + await analytics.report({ eventName: 'desktop_app_launch', timestamp: 1, attributes: {} }) + expect(emit).toHaveBeenCalledWith(expect.objectContaining({ attributes: { app_version: 'test-version' } })) lifetime.abort() await policy.return?.() ctx.productTelemetry.emit({ eventName: 'desktop_upgrade_install_restart_click', body: 'upgrade', timestamp: Date.now() }) diff --git a/packages/client/product-analytics/README.i18n.yaml b/packages/client/product-analytics/README.i18n.yaml index dad13cf815..200ba4f9e9 100644 --- a/packages/client/product-analytics/README.i18n.yaml +++ b/packages/client/product-analytics/README.i18n.yaml @@ -15,8 +15,8 @@ en: 2fee9973f19f9401 zh: 8ab4b55413cb8408 /deepseek-ai-dsh-client-product-analytics/use-this-package: - en: c136c4af1b72bdcb - zh: 8e287dfed42cea82 + en: c8c05edf30b3959d + zh: 39c2147cf01d43b5 /deepseek-ai-dsh-client-product-analytics/understand-the-implementation: en: 416b7e925f0852c9 zh: c0b83bacf18e91b1 diff --git a/packages/client/product-analytics/README.md b/packages/client/product-analytics/README.md index 33ad1d849f..84c67e30e2 100644 --- a/packages/client/product-analytics/README.md +++ b/packages/client/product-analytics/README.md @@ -27,9 +27,9 @@ The renderer and Electron subscribe to the Host policy through the existing auth Common fields are `device_id`, `user_id`, `os_version`, and `app_version`. Device identity reuses the existing login record without generating one. The Host reads credential-free device, account, and OS fields through `deepseekAccount.getDeviceIdentity()`. Missing values are omitted; API keys, account tokens, prompts, and responses are never event fields. -Electron supplies internal `DSH_PRODUCT_ANALYTICS_APP_VERSION` from its running version; the exporter requires it. This variable belongs to the Desktop launcher-to-Host protocol. Desktop export, batch, and shutdown deadlines are 1000, 1500, and 2000 milliseconds, below the Host exit budget. +Electron forwards its build-inlined `DSH_CLIENT_VERSION` to the Host; analytics and the exporter reuse this client version. The exporter requires it. Desktop export, batch, and shutdown deadlines are 1000, 1500, and 2000 milliseconds, below the Host exit budget. -The [event types](src/events.ts) own names and allowed fields. Views count visible page entry, including reshown native welcome windows; transient onboarding loading does not repeat the same page impression, and closing an onboarding popup emits nothing. Funded Continue uses `next`. Message submission retains its original occurrence timestamp and captures `submit_source`, `submit_type`, model, explicit effort, and `run_mode` before asynchronous command adjudication; only the ordinary message path reports before reference serialization, so handled and claimed commands are excluded. Later serialization, attachment, or send failures do not retract this attempt; queue execution does not count again. Attachment-only submissions follow the same rule. Programmatic submissions omit `submit_source`; keyboard and button gestures use `enter` and `click`. Plan takes precedence over an active goal. Capture and reporting failures cannot interrupt submission. A blank first-send Session has no `session_id`. Model and plugin switches report only accepted changes. Fork events carry the created child ID and the source IDs, before the optional child-title update; failed creation emits nothing. +The [event types](src/events.ts) own names and allowed fields. Authentication events cover the native welcome page only; workspace login after API-key entry is excluded. Views count visible page entry, including reshown native welcome windows; transient onboarding loading does not repeat the same page impression, and closing an onboarding popup emits nothing. Funded Continue uses `next`. Message submission retains its original occurrence timestamp and captures `submit_source`, `submit_type`, model, explicit effort, and `run_mode` before asynchronous command adjudication; only the ordinary message path reports before reference serialization, so handled and claimed commands are excluded. Later serialization, attachment, or send failures do not retract this attempt; queue execution does not count again. Attachment-only submissions follow the same rule. Programmatic submissions omit `submit_source`; keyboard and button gestures use `enter` and `click`. Plan takes precedence over an active goal. Capture and reporting failures cannot interrupt submission. A blank first-send Session has no `session_id`. Model and plugin switches report only accepted changes. Fork events carry the created child ID and the source IDs, before the optional child-title update; failed creation emits nothing. Plugin rows use `plugin_type=plugin`, and packages use `bundle`. Installation reports `result_status=success|failed|cancelled|unknown` independently from `error_reason` and keeps `is_success`. Duration is milliseconds from the install click through the terminal result, including validation, inspection, and internal registry retries. `input_value` retains registry package identifiers and plain versions only; Git inputs become `[git]`, other URLs `[url]`, and paths or unrecognized inputs `[path-or-other]`. Both click and result use this redacted value. Reopening a hidden install dialog counts another `plugin_add_button_click`. `is_builtin` means installation-supplied (`installed=false`), not whether a profile explicitly depends on the package. Explicit build-approval retry starts a new attempt. Restart-required is success. A temporary disconnect retains the pending operation; only an absent recovery result becomes `unknown`. diff --git a/packages/client/product-analytics/README.zh.md b/packages/client/product-analytics/README.zh.md index cdd60bb58b..f997f49dc3 100644 --- a/packages/client/product-analytics/README.zh.md +++ b/packages/client/product-analytics/README.zh.md @@ -27,9 +27,9 @@ kind: "package-reference" 公共字段为 `device_id`、`user_id`、`os_version` 和 `app_version`。设备身份复用现有登录记录,不会生成新标识。Host 通过 `deepseekAccount.getDeviceIdentity()` 读取不含凭据的设备、账户和操作系统字段。缺失值会省略;API key、账户令牌、提示词和模型回复都不是事件字段。 -Electron 通过内部变量 `DSH_PRODUCT_ANALYTICS_APP_VERSION` 传递运行版本;导出器要求该值存在。该变量属于桌面启动器与 Host 之间的协议。桌面导出、批次与关闭期限分别为 1000、1500 和 2000 毫秒,均短于 Host 退出预算。 +Electron 将构建内联的 `DSH_CLIENT_VERSION` 传给 Host;埋点与导出器复用这一客户端版本,导出器要求该值存在。桌面导出、批次与关闭期限分别为 1000、1500 和 2000 毫秒,均短于 Host 退出预算。 -[事件类型](src/events.ts)定义名称和允许的字段。页面曝光按实际进入可见页面计数,包括重新显示的原生欢迎窗口;onboarding 短暂进入加载状态不会重复计算同一页面曝光,关闭 onboarding 弹窗不产生事件。有余额时的继续按钮使用 `next`。消息提交保留最初发生时间,并在异步命令裁决前采集 `submit_source`、`submit_type`、模型、显式思考强度和 `run_mode`;仅普通消息路径在引用序列化之前上报,已处理或认领的命令不计入。后续序列化、附件处理或发送失败不撤销此次计数,排队后的执行也不重复计数;纯附件提交遵循相同规则。程序化提交省略 `submit_source`;键盘与按钮操作分别使用 `enter` 和 `click`。计划模式优先于活跃目标。采集和上报异常不会中断提交。首次发送的空白会话省略 `session_id`。模型与插件切换仅在变更被接受后上报。分叉事件携带已创建的子会话 ID 和来源 ID,在可选的子会话标题更新前上报;创建失败不产生事件。 +认证事件仅覆盖原生欢迎页;通过 API Key 进入工作区后再登录的场景不在采集范围内。[事件类型](src/events.ts)定义名称和允许的字段。页面曝光按实际进入可见页面计数,包括重新显示的原生欢迎窗口;onboarding 短暂进入加载状态不会重复计算同一页面曝光,关闭 onboarding 弹窗不产生事件。有余额时的继续按钮使用 `next`。消息提交保留最初发生时间,并在异步命令裁决前采集 `submit_source`、`submit_type`、模型、显式思考强度和 `run_mode`;仅普通消息路径在引用序列化之前上报,已处理或认领的命令不计入。后续序列化、附件处理或发送失败不撤销此次计数,排队后的执行也不重复计数;纯附件提交遵循相同规则。程序化提交省略 `submit_source`;键盘与按钮操作分别使用 `enter` 和 `click`。计划模式优先于活跃目标。采集和上报异常不会中断提交。首次发送的空白会话省略 `session_id`。模型与插件切换仅在变更被接受后上报。分叉事件携带已创建的子会话 ID 和来源 ID,在可选的子会话标题更新前上报;创建失败不产生事件。 插件条目使用 `plugin_type=plugin`,插件包使用 `bundle`。安装通过独立于 `error_reason` 的 `result_status=success|failed|cancelled|unknown` 表达结果,并保留 `is_success`。耗时单位为毫秒,从点击安装开始,到最终结果结束,包含校验、检查输入和内部源重试。`input_value` 仅保留 registry 包标识和普通版本;Git 输入记为 `[git]`,其他 URL 记为 `[url]`,路径及无法识别的输入记为 `[path-or-other]`。点击和结果均使用脱敏后的值。重新打开已隐藏的安装弹窗会再次计为 `plugin_add_button_click`。`is_builtin` 表示由安装供应(`installed=false`),不表示 profile 是否显式依赖该包。显式批准构建后的重试开启新的一次尝试。需要重启视为成功。临时断线保留待定操作;只有恢复查询确认没有结果时才成为 `unknown`。 diff --git a/packages/client/ui-settings-account/README.i18n.yaml b/packages/client/ui-settings-account/README.i18n.yaml index 645a98ab43..cb0531a5d8 100644 --- a/packages/client/ui-settings-account/README.i18n.yaml +++ b/packages/client/ui-settings-account/README.i18n.yaml @@ -6,8 +6,8 @@ en: 1fcba8f273b661fb zh: 14e1236f84ae39d7 /deepseek-ai-dsh-client-ui-settings-account: - en: 311be03fcfd8ccb8 - zh: e65daffe9234c40a + en: 9ff7ac2d70f70b59 + zh: ded522c4cfd6e040 /deepseek-ai-dsh-client-ui-settings-account/summary: en: 9ff357621753be69 zh: 3f7ede82acc9fa31 diff --git a/packages/client/ui-settings-account/README.md b/packages/client/ui-settings-account/README.md index b9f38dacfa..a417443c7b 100644 --- a/packages/client/ui-settings-account/README.md +++ b/packages/client/ui-settings-account/README.md @@ -7,7 +7,7 @@ kind: "package-reference" English | [中文](README.zh.md) -Desktop product events use the optional [product analytics service](../product-analytics/README.md); ordinary Web interactions are excluded. +Desktop product events use the optional [product analytics service](../product-analytics/README.md); ordinary Web interactions are excluded. Workspace login dialogs emit no authentication events; native welcome owns authentication analytics. Server-expired account credentials clear the displayed account details and emit a localized sign-in reminder toast once; account snapshot replay does not repeat it. diff --git a/packages/client/ui-settings-account/README.zh.md b/packages/client/ui-settings-account/README.zh.md index 6edaab61a1..2d357ee54c 100644 --- a/packages/client/ui-settings-account/README.zh.md +++ b/packages/client/ui-settings-account/README.zh.md @@ -7,7 +7,7 @@ kind: "package-reference" [English](README.md) | 中文 -桌面端产品事件使用可选的[产品埋点服务](../product-analytics/README.zh.md),不包含普通 Web 交互。 +桌面端产品事件使用可选的[产品埋点服务](../product-analytics/README.zh.md),不包含普通 Web 交互。工作区内的登录弹窗不发送认证事件;认证埋点由原生欢迎页负责。 账号凭据被服务端判定失效后,清空显示的账号详情,并弹出一次本地化的重新登录提示 toast;账号快照重放不会重复提示。 diff --git a/packages/client/ui-settings-account/src/client/AccountMenu.tsx b/packages/client/ui-settings-account/src/client/AccountMenu.tsx index 822cb7fc20..ea5e9d4a6b 100644 --- a/packages/client/ui-settings-account/src/client/AccountMenu.tsx +++ b/packages/client/ui-settings-account/src/client/AccountMenu.tsx @@ -22,7 +22,7 @@ export type AccountMenuProps = PropsRuntime<'settings.launcher'> & PropsLocale<' export function AccountMenu({ subscribeSessionExpired, subscribeModelSignInRequired, wide, settingsShortcut, openSettings, openOnboarding, settingsOpen, useAccount, useTheme, signOut, hasRunningAccountTasks, refreshAccount, bonusNoticeShown, bonusNoticeDismissed, - contactUs, showLogin, start, cancel, t, track, + contactUs, showLogin, start, cancel, t, }: AccountMenuProps) { const anchor = useRef(null) // The launcher outlives the panel, so a false-to-true edge is one Settings entry: @@ -82,7 +82,7 @@ export function AccountMenu({ else if (id === 'signin') beginSignIn() else void requestSignOut() }} /> - {account.loginVisible && !account.onboarding && { showLogin(false) }} useApiKey={() => { showLogin(false); openOnboarding('deepseek-official') }} />} {signedIn && signOutImpact !== undefined && & PropsLocale<'settings.account'> & InjectFace) { const account = useAccount(value => value) const colorScheme = useTheme(snapshot => snapshot.active.colorScheme) useEffect(() => { setOnboarding(true); return () => { setOnboarding(false) } }, [setOnboarding]) useEffect(() => { if (account.view?.status === 'credential-stored') complete() }, [account.view?.status, complete]) if (!account.view || account.view.status === 'credential-stored') return null - return { showLogin(false); complete() }} useApiKey={() => { showLogin(false); useApiKey() }} /> } diff --git a/packages/client/ui-settings-account/src/client/AccountSection.tsx b/packages/client/ui-settings-account/src/client/AccountSection.tsx index 60a326d9d7..ba6530bef1 100644 --- a/packages/client/ui-settings-account/src/client/AccountSection.tsx +++ b/packages/client/ui-settings-account/src/client/AccountSection.tsx @@ -1,5 +1,4 @@ /** Account settings renders safe Host state and explicit login actions. */ -import type { TrackProductEvent } from '@deepseek-ai/dsh-client-product-analytics/client' import { Big } from 'big.js' import { useEffect, useRef, useState, type MouseEvent } from 'react' import { Button, IconRightUpOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives' @@ -34,8 +33,6 @@ export interface AccountSnapshot { /** Host operations injected into the Cordis-free account component. */ export interface AccountSectionInjected { - /** Desktop-only event collection supplied by the application. */ - track?: TrackProductEvent /** Subscribe to live credential-expiry notifications. * @param listener - callback after the current credential is removed. * @returns listener cleanup. diff --git a/packages/client/ui-settings-account/src/client/SignInDialog.tsx b/packages/client/ui-settings-account/src/client/SignInDialog.tsx index edc2c4d4a5..f07b505d8a 100644 --- a/packages/client/ui-settings-account/src/client/SignInDialog.tsx +++ b/packages/client/ui-settings-account/src/client/SignInDialog.tsx @@ -1,6 +1,5 @@ /** Account authorization dialog; errors allow retry after cancelling any active attempt. */ -import type { TrackProductEvent } from '@deepseek-ai/dsh-client-product-analytics/client' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useState } from 'react' import { Button, IconCloseOutlineRegular, IconLoadingOutlineRegular, Modal } from '@deepseek-ai/dsh-client-ui-primitives' import type { AccountSnapshot } from './AccountSection.tsx' import type { SignInAttemptId } from '@deepseek-ai/dsh-deepseek-account/types' @@ -9,8 +8,7 @@ import { authorizeUrlWithTheme } from './authorize-url.ts' import css from './SignInDialog.module.css' /** @param props - safe account state, localized copy, and user actions. @returns login dialog. */ -export function SignInDialog({ account, colorScheme, start, cancel, close, useApiKey, t, track }: { - track?: TrackProductEvent | undefined +export function SignInDialog({ account, colorScheme, start, cancel, close, useApiKey, t }: { account: AccountSnapshot /** Resolved scheme of the active Desktop theme; the copied link carries it. */ colorScheme: 'light' | 'dark' @@ -20,8 +18,6 @@ export function SignInDialog({ account, colorScheme, start, cancel, close, useAp useApiKey: () => void t: (key: AccountKey) => string }) { - const viewed = useRef(false) - useEffect(() => { if (!viewed.current) { viewed.current = true; track?.('auth_page_view', {}) } }, [track]) const [busy, setBusy] = useState(false) const [failed, setFailed] = useState(false) const [copyResult, setCopyResult] = useState<{ messageKey: 'copiedLink' | 'copyFailed' } | null>(null) @@ -80,9 +76,9 @@ export function SignInDialog({ account, colorScheme, start, cancel, close, useAp
+ onClick={active ? dismiss : useApiKey}>{t(active ? 'cancel' : 'addApiKey')}
diff --git a/packages/client/ui-settings-account/src/client/index.ts b/packages/client/ui-settings-account/src/client/index.ts index fab9001aaf..a2d1cb38bf 100644 --- a/packages/client/ui-settings-account/src/client/index.ts +++ b/packages/client/ui-settings-account/src/client/index.ts @@ -156,7 +156,6 @@ export function apply(ctx: Context): void { if (reason === 'returned' && page === 'top-up') void refreshAfterReturn(refreshing, reRead) }) const operations: AccountSectionInjected = { - track: (name, attributes) => ctx.get('productAnalytics')?.track(name, attributes), subscribeSessionExpired: listener => ctx.remote.$on('deepseek-account/session-expired', listener), subscribeModelSignInRequired: listener => ctx.remote.$on('deepseek-account/model-sign-in-required', listener), ...nativePlatform === undefined ? {} : { openPlatformPage: platformPageOpener(refreshAccount) }, diff --git a/packages/client/ui-settings-account/tests/apply.client.spec.ts b/packages/client/ui-settings-account/tests/apply.client.spec.ts index a442d71cf4..393d5232ec 100644 --- a/packages/client/ui-settings-account/tests/apply.client.spec.ts +++ b/packages/client/ui-settings-account/tests/apply.client.spec.ts @@ -273,8 +273,7 @@ it('re-reads profile and balance but no bonus for the onboarding recharge return const actions = operations(c) const track = vi.fn() c.ctx.provide('productAnalytics', { track } as never) - actions.track?.('auth_page_view', {}) - expect(track).toHaveBeenCalledWith('auth_page_view', {}) + expect(actions).not.toHaveProperty('track') const onboarding = injectedOf(c.ctx.slots.entries('shell.overlay') .find(entry => entry.options.id === 'desktop-onboarding')!) as DesktopOnboardingInjected onboarding.track?.('onboarding_page_view', { page_name: 'onboarding_recharge' }) diff --git a/packages/client/ui-settings-account/tests/sign-in-dialog.client.spec.tsx b/packages/client/ui-settings-account/tests/sign-in-dialog.client.spec.tsx index 9eebc21673..c03b3613d8 100644 --- a/packages/client/ui-settings-account/tests/sign-in-dialog.client.spec.tsx +++ b/packages/client/ui-settings-account/tests/sign-in-dialog.client.spec.tsx @@ -179,11 +179,15 @@ it('keeps an outstanding start open until the request settles', async () => { expect(screen.getByRole('button', { name: en.signIn }).hasAttribute('disabled')).toBe(false) }) - -it('reports entry once across rerenders with a supplied sender', () => { +// Workspace login remains outside native welcome analytics even if a caller forwards a sender. +it('does not report workspace login views or login choices', async () => { const track = vi.fn() - const props = dialogProps(null) - const view = render() - view.rerender( { track(name, attributes) }} colorScheme="light" />) - expect(track).toHaveBeenCalledExactlyOnceWith('auth_page_view', {}) + const props = { ...dialogProps(null), track } + const view = render() + view.rerender() + await act(async () => { fireEvent.click(screen.getByRole('button', { name: en.signIn })) }) + expect(props.start).toHaveBeenCalledOnce() + await act(async () => { fireEvent.click(screen.getByRole('button', { name: en.addApiKey })) }) + expect(props.useApiKey).toHaveBeenCalledOnce() + expect(track).not.toHaveBeenCalled() }) diff --git a/packages/host/product-telemetry-otel/README.i18n.yaml b/packages/host/product-telemetry-otel/README.i18n.yaml index f02cccd867..6b65776e76 100644 --- a/packages/host/product-telemetry-otel/README.i18n.yaml +++ b/packages/host/product-telemetry-otel/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 9aa34fb1dc95d8cf /deepseek-ai-dsh-host-product-telemetry-otel/use-this-package: - en: a62d2f83bd8b4186 - zh: f439a20ac0c4858e + en: d2356d7abffcc0ce + zh: 13250317296816ce /deepseek-ai-dsh-host-product-telemetry-otel/understand-the-implementation: en: d7807324fdeb2b42 zh: 1bfe1e943e513500 diff --git a/packages/host/product-telemetry-otel/README.md b/packages/host/product-telemetry-otel/README.md index 3ea8563ee8..09511b5094 100644 --- a/packages/host/product-telemetry-otel/README.md +++ b/packages/host/product-telemetry-otel/README.md @@ -25,7 +25,7 @@ Send selected product usage events to an OTLP/HTTP collector. Events carry a nam ## Use this package -Mount the plugin in a Cordis composition with the application identity; override the collector endpoint when needed. The Desktop composition mounts it when [product analytics](../../client/product-analytics/README.md) is enabled; ordinary Web does not. The standalone example below reads `DSH_APP_VERSION`; Desktop supplies `DSH_PRODUCT_ANALYTICS_APP_VERSION` through its native launcher. Both must name the running release; the schema rejects an absent version. +Mount the plugin in a Cordis composition with the application identity; override the collector endpoint when needed. The Desktop composition mounts it when [product analytics](../../client/product-analytics/README.md) is enabled; ordinary Web does not. The standalone example below reads `DSH_APP_VERSION`; Desktop supplies `DSH_CLIENT_VERSION` through its native launcher. Both must name the running release; the schema rejects an absent version. ```yaml - name: '@deepseek-ai/dsh-otel' diff --git a/packages/host/product-telemetry-otel/README.zh.md b/packages/host/product-telemetry-otel/README.zh.md index e2674730da..13fa44eee7 100644 --- a/packages/host/product-telemetry-otel/README.zh.md +++ b/packages/host/product-telemetry-otel/README.zh.md @@ -25,7 +25,7 @@ kind: "package-reference" ## 使用本包 -在 Cordis 组合中挂载插件并提供应用标识;需要时可覆盖接收地址。桌面组合在开启[产品埋点](../../client/product-analytics/README.zh.md)时挂载本插件;普通 Web 不挂载。下方独立示例读取 `DSH_APP_VERSION`;桌面端由原生启动器提供 `DSH_PRODUCT_ANALYTICS_APP_VERSION`。两者均须为运行中的发布版本;缺少版本时 schema 会拒绝配置。 +在 Cordis 组合中挂载插件并提供应用标识;需要时可覆盖接收地址。桌面组合在开启[产品埋点](../../client/product-analytics/README.zh.md)时挂载本插件;普通 Web 不挂载。下方独立示例读取 `DSH_APP_VERSION`;桌面端由原生启动器提供 `DSH_CLIENT_VERSION`。两者均须为运行中的发布版本;缺少版本时 schema 会拒绝配置。 ```yaml - name: '@deepseek-ai/dsh-otel'