fix(analytics): retain provider context for renderer reports

This commit is contained in:
zhc committed 2026-09-25 21:07:33 +08:00
1 parent 1a797d0d52
commit d0a7678471
5 files changed
+41 -13

No files matched your search

@@ -18,8 +18,8 @@
en: c136c4af1b72bdcb
zh: 8e287dfed42cea82
/deepseek-ai-dsh-client-product-analytics/understand-the-implementation:
en: 5fcaa1cb77e47875
zh: 434925be40f7d6ed
en: 416b7e925f0852c9
zh: c0b83bacf18e91b1
/deepseek-ai-dsh-client-product-analytics/model-experience:
en: 291751b16b8a37e7
zh: 3b38008f4364ec41
+1 -1
View File
@@ -39,7 +39,7 @@ Plugin rows use `plugin_type=plugin`, and packages use `bundle`. Installation re
<details>
<summary>Implementation internals — click to expand</summary>
Presentational components receive callbacks. Shared Client services resolve the optional Desktop analytics sender, which requires both the native preload marker and the synchronized Host policy. Native actions use the authenticated Host API; its generated Typert validator accepts only the typed event fields. The Host enriches identity immediately before queueing. It observes live compaction events without replaying Session history. No runtime invariant companion is published because collection has no independent delivery acknowledgement to compare.
Presentational components receive callbacks. Shared Client services resolve the optional Desktop analytics sender, which requires both the native preload marker and the synchronized Host policy. The sender retains its own RPC context when called by other plugins. Native actions use the authenticated Host API; its generated Typert validator accepts only the typed event fields. The Host enriches identity immediately before queueing. It observes live compaction events without replaying Session history. No runtime invariant companion is published because collection has no independent delivery acknowledgement to compare.
</details>
@@ -39,7 +39,7 @@ Electron 通过内部变量 `DSH_PRODUCT_ANALYTICS_APP_VERSION` 传递运行版
<details>
<summary>实现细节——点击展开</summary>
展示组件接收回调。共享 Client 服务读取可选的桌面埋点发送器,后者要求同时存在原生 preload 标识和 同步的 Host 采集策略。原生操作使用已认证的 Host API,其生成的 Typert 校验器只接受类型中声明的事件字段。Host 在入队前补充身份信息,并监听实时压缩事件,不重放会话历史。本包不发布运行时不变量伴随模块,因为采集没有可用于比较的独立交付确认。
展示组件接收回调。共享 Client 服务读取可选的桌面埋点发送器,后者要求同时存在原生 preload 标识和 同步的 Host 采集策略。发送器在被其他插件调用时保留自身的 RPC 上下文。原生操作使用已认证的 Host API,其生成的 Typert 校验器只接受类型中声明的事件字段。Host 在入队前补充身份信息,并监听实时压缩事件,不重放会话历史。本包不发布运行时不变量伴随模块,因为采集没有可用于比较的独立交付确认。
</details>
@@ -13,9 +13,16 @@ declare module '@deepseek-ai/cordis' {
class DesktopAnalytics extends Service {
private collecting = false
// RPC dispatch stays in the provider context when Cordis traces a consumer call.
private readonly submit: (event: ProductEvent) => Promise<void>
constructor(ctx: Context) {
super(ctx, 'productAnalytics')
this.submit = async (event) => {
try { await ctx.remote.productAnalytics.report(event) } catch (_error) {
// Missing or disconnected Remote services must not interrupt the interaction.
}
}
if (!('dshDesktop' in globalThis)) return
const stream = ctx.remote.$stream<boolean>({
name: 'product analytics policy', open: signal => ctx.remote.productAnalytics.watchPolicy(signal),
@@ -44,12 +51,6 @@ class DesktopAnalytics extends Service {
const event = { eventName: name, attributes, timestamp } as ProductEvent
void this.submit(event)
}
private async submit(event: ProductEvent): Promise<void> {
try { await this.ctx.remote.productAnalytics.report(event) } catch (_error) {
// Missing or disconnected Remote services must not interrupt the interaction.
}
}
}
/** Analytics consumes authenticated RPC and its reconnecting policy stream. */
@@ -1,4 +1,4 @@
import { Context } from '@deepseek-ai/cordis'
import { Context, Service } from '@deepseek-ai/cordis'
import { afterEach, expect, it, vi } from 'vitest'
import type { RemoteStreamOptions } from '@deepseek-ai/dsh-api-gateway/client'
import * as Analytics from '../src/client/index.ts'
@@ -28,8 +28,16 @@ async function setup(enabled = true) {
},
dispose: async () => { disposed = true; wake?.() },
}
ctx.provide('remote', { productAnalytics: { report, watchPolicy: vi.fn() }, $stream: (value: RemoteStreamOptions<boolean>) => { options = value; return stream } } as never)
ctx.provide('remote.productAnalytics', {} as never)
const remote = {
productAnalytics: { report, watchPolicy: vi.fn() },
$stream: (value: RemoteStreamOptions<boolean>) => { options = value; return stream },
}
class TracedRemote extends Service {
constructor(ctx: Context) { super(ctx, 'remote') }
$stream = remote.$stream
}
await ctx.plugin(TracedRemote)
await ctx.plugin({ apply(child: Context) { child.provide('remote.productAnalytics', remote.productAnalytics as never) } })
await ctx.plugin(Analytics)
if ('dshDesktop' in globalThis) await vi.waitFor(() => { expect(ctx.productAnalytics.enabled).toBe(enabled) })
return { ctx, report, options: () => options!,
@@ -92,3 +100,22 @@ it('fails closed on stream loss and accepts a fresh policy after reconnection',
await b.policy(true)
await b.fail()
})
it('reports from a consumer without granting it the analytics Remote namespace', async () => {
vi.stubGlobal('dshDesktop', {})
const b = await setup()
let click!: () => void
await b.ctx.plugin({
inject: ['remote'],
apply(ctx: Context) {
click = () => {
expect(ctx.get('productAnalytics')?.enabled).toBe(true)
ctx.get('productAnalytics')?.track('sidebar_menu_click', { menu_name: 'plugin' })
}
},
})
click()
expect(b.report).toHaveBeenCalledExactlyOnceWith({
eventName: 'sidebar_menu_click', timestamp: expect.any(Number) as number, attributes: { menu_name: 'plugin' },
})
})