From ec5399e0746ae8f4e3ce33202debda9a072eddd1 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:27:41 +0800 Subject: [PATCH] doc: agent note for the telemetry anonymous user id --- ...7-31-telemetry-anonymous-user-id.i18n.yaml | 6 +++ .../2026-07-31-telemetry-anonymous-user-id.md | 44 +++++++++++++++++++ ...26-07-31-telemetry-anonymous-user-id.zh.md | 44 +++++++++++++++++++ ...7-31-web-telemetry-default-mount.i18n.yaml | 4 +- .../2026-07-31-web-telemetry-default-mount.md | 2 +- ...26-07-31-web-telemetry-default-mount.zh.md | 2 +- 6 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 .agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.i18n.yaml create mode 100644 .agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.md create mode 100644 .agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.zh.md diff --git a/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.i18n.yaml new file mode 100644 index 0000000000..7bcfa9f599 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.md +2026-07-31-telemetry-anonymous-user-id.md: 3c8e3324cb418eac48cb5ae780c55bbcbaf3caa1 +2026-07-31-telemetry-anonymous-user-id.zh.md: 9ff6cf35a90d4087b4ab75987dc9244210e3d46a diff --git a/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.md b/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.md new file mode 100644 index 0000000000..3c8e3324cb --- /dev/null +++ b/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.md @@ -0,0 +1,44 @@ +# Agent Note: Telemetry anonymous user id ($DSH_HOME/.userid) and the OTel Resource user.id + +Status: implemented + +English | [中文](2026-07-31-telemetry-anonymous-user-id.zh.md) + +## Problem + +Session telemetry is mounted by default ([default-mount Note](2026-07-31-web-telemetry-default-mount.md)), but the OTel Resource carried only `service.name`/`service.version` — no user-level identity at all, so the collector could neither aggregate per user nor count active users. The only prior ruling on point was an unimplemented one to derive a user id by hashing the hostname/local IP; the dsh-sdk toolchain keeps its own anonymous id (`$DSH_HOME/telemetry.json`), but that is the launcher feed's private fact, unrelated to the OTel feed. The OTel feed needed an anonymous user identity with clean semantics. + +## Decision + +The `session-telemetry-otel` package's own module `src/user-id.ts` owns the OTel feed's user identity: `getOrCreateAnonymousUserId()` returns the bare UUID line in `$DSH_HOME/.userid` (resolved by `resolveDshHome`, `$DSH_HOME` > `~/.dsh`), minting and persisting a random UUID v4 on first use; the backend constructor carries it as the Resource's `user.id` (the OTel semconv user attribute), once per export batch. This identity belongs to the OTel feed alone; the dsh-sdk launcher telemetry keeps its own anonymous-id store (`telemetry.json`), and the two are not shared (the first cut unified both feeds through a shared util package; the user reconsidered and pulled it back — no shared package before a second real consumer exists, revisit when a feed-correlation need appears). + +| Ruling | Value | Rationale | +|---|---|---| +| Id source | Random UUID v4, never derived from the hostname, network address, or git remote | A derived id is reversible, making "anonymous" a fiction | +| Storage form | `.userid`, a bare UUID line plus newline, no JSON wrapper | Identity is a standalone fact, not something filed under one telemetry feed's file name/format | +| IO form | Synchronous IO + a process-lifetime memo keyed by resolved file path | `TelemetryOtel`'s constructor is synchronous (async would reshape plugin loading); one disk touch per process, and mid-run file deletion never affects the running process | +| Concurrent first launch | Settled by an exclusive-create (`wx`) write; the loser rereads the winner's id | Covers common concurrency (a reread landing in the winner's microsecond create-to-write window can still yield one id per process for that run, converging on the persisted value next launch — a telemetry-grade consequence, accepted) | +| Loss semantics | File deleted → next launch mints a fresh id; loss is accepted | An anonymous identity has no recovery value; recoverability demands derivation material, which conflicts with anonymity | +| Write failure | Best-effort: return the in-memory id | Telemetry is never blocked by a read-only home | +| Report position | Resource attribute, not per-record attributes | Once per batch suffices for Resource-dimension aggregation; per-record injection would touch the seam contract and grow the wire | +| semconv dependency | `@opentelemetry/semantic-conventions` is not imported | One string constant does not justify a dependency | +| Home | A module inside `session-telemetry-otel`, not a shared util package | Repo rule: split a package only for a second real consumer; the sdk launcher feed keeps its own store, and no real correlation need exists | +| Separate switch | None | Identity follows the telemetry master switch (`DSH_TELEMETRY_DISABLED`); telemetry off means nothing reports | + +## Alternatives considered + +| Rejected | One-line reason | +|---|---| +| Hostname/IP-hash-derived id (the prior ruling) | Reversible means not anonymous; the random UUID is semantically clean — the user ruled to supersede | +| user.id on every record's attributes (Claude Code's shape) | Touches the session-telemetry seam contract or injects per record, growing the wire; once per batch on the Resource already aggregates | +| A shared util package unifying both feeds (the first cut) | The only real consumer is the OTel backend; switching the sdk launcher onto it was unification for its own sake — the user reconsidered and pulled it back, to be re-extracted when a correlation need appears | +| Reusing telemetry.json instead of a new file | The file name/JSON format files the identity under the launcher feed's naming; the OTel feed's identity is a standalone fact | +| AppCLIEntry reading the id and injecting via config patch | Every surface entry needs wiring; a runtime fact inside deployment config conflates the two | +| Housing it in `@deepseek-ai/dsh-paths` | paths is pure path computation with zero IO; a persisting identity capability would pollute the package boundary | + +## Consequences + +- One `$DSH_HOME` is one stable user in the OTel feed; separate homes are separate users by construction, with no cross-home linking mechanism. +- The OTel feed and the launcher feed each hold their own id (`.userid` vs `telemetry.json`) and cannot be correlated — the direct cost of not extracting a shared package, to be unified when a real correlation need appears. +- Deleting `.userid` resets the identity (effective next launch); on an unwritable home each process holds its own in-memory id until the home becomes writable. +- The [default-mount Note](2026-07-31-web-telemetry-default-mount.md)'s identity follow-up is closed for the anonymous-user-id part by this decision; hostname/surface dimensions, the redaction rule, and the usage-metrics track remain open. diff --git a/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.zh.md b/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.zh.md new file mode 100644 index 0000000000..9ff6cf35a9 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.zh.md @@ -0,0 +1,44 @@ +# Agent Note: telemetry 匿名用户 id($DSH_HOME/.userid)与 OTel Resource user.id + +Status: implemented + +[English](2026-07-31-telemetry-anonymous-user-id.md) | 中文 + +## Problem + +session telemetry 已默认挂载([默认挂载 Note](2026-07-31-web-telemetry-default-mount.md)),但 OTel Resource 只有 `service.name`/`service.version`,没有任何用户级标识——接收端无法按用户聚合、无法数活跃用户。此前唯一相关口径是一条未实现的「hostname/本机 IP 哈希派生 user.id」裁定;dsh-sdk 工具链另有自用的匿名 id(`$DSH_HOME/telemetry.json`),但那是 launcher 回流的私有事实,与 OTel 回流无关。需要给 OTel 回流一个语义干净的匿名用户身份。 + +## Decision + +`session-telemetry-otel` 包内模块 `src/user-id.ts` 是 OTel 回流用户身份的属主:`getOrCreateAnonymousUserId()` 返回 `$DSH_HOME/.userid`(`resolveDshHome` 解析,`$DSH_HOME` > `~/.dsh`)中的裸 UUID 行,首用生成随机 UUID v4 并落盘;backend 构造时把它作为 Resource 的 `user.id`(OTel semconv 标准用户属性)随每批导出携带一次。该身份只属于 OTel 回流;dsh-sdk launcher telemetry 保留自己的匿名 id 存储(`telemetry.json`),两者不共享(初版曾做公用 util 包统一两条回流,用户复议后收回:在有第二个真实消费者之前不抽公共包,回流关联需求出现时再议)。 + +| 裁定 | 取值 | 理由 | +|---|---|---| +| id 来源 | 随机 UUID v4,绝不从 hostname/网络地址/git remote 派生 | 派生 id 可反查,「匿名」名不副实 | +| 存储形态 | `.userid` 裸 UUID 行 + 换行,无 JSON 包装 | 身份是独立事实,不挂在某条 telemetry 链路的文件命名/格式下 | +| 读写形态 | 同步 IO + 进程内按解析后文件路径 memo | `TelemetryOtel` 构造函数是同步的(async 迫使插件装载改形);一进程一次盘 IO,运行中删文件不影响本进程 | +| 并发首启 | `wx` 独占写裁决,落败方重读胜者 id | 覆盖常见并发(重读撞进胜者建档-写入微秒窗仍可能各持一 id 一次运行,下次启动收敛到落盘值——telemetry 级后果,接受) | +| 丢失语义 | 文件被删 → 下次启动换新 id,接受丢失 | 匿名身份无恢复价值;可恢复性要求派生材料,与匿名冲突 | +| 写失败 | best-effort 返回内存 id | telemetry 永不因 home 只读被阻塞 | +| 上报位置 | Resource 属性,非逐条 attributes | 每批一次即够接收端按 Resource 维度聚合;逐条注入要动 seam 契约且涨 wire 体积 | +| semconv 依赖 | 不引 `@opentelemetry/semantic-conventions` 包 | 一个字符串常量不值一个依赖 | +| 落点 | `session-telemetry-otel` 包内模块,非公共 util 包 | 仓规「有第二个真实消费者才拆包」;sdk launcher 回流保留自有存储,无现实关联需求 | +| 单独开关 | 无 | 身份跟随 telemetry 整体开关(`DSH_TELEMETRY_DISABLED`);关 telemetry 即整体不报 | + +## Alternatives considered + +| 被拒 | 一句话理由 | +|---|---| +| hostname/IP 哈希派生 id(此前口径) | 可反查即非匿名;随机 UUID 语义干净,用户裁决取代 | +| user.id 放每条 record 的 attributes(Claude Code 形态) | 要动 session-telemetry seam 契约或逐条注入,wire 体积涨;Resource 每批一次已满足聚合 | +| 公用 util 包统一两条回流(初版实现) | 唯一现实消费者是 OTel backend;sdk launcher 换用它只是为统一而统一——用户复议收回,回流关联需求出现时再抽包 | +| 复用 telemetry.json 不新建文件 | 文件名/JSON 格式把身份挂在 launcher 链路命名下;OTel 回流身份是独立事实 | +| AppCLIEntry 读好 id 经 config patch 注入 | 每个 surface 入口都要接线;config 里传运行时事实与部署配置混淆 | +| 挂进 `@deepseek-ai/dsh-paths` | paths 是纯路径计算零 IO;带持久化的身份能力会污染包边界 | + +## Consequences + +- 一个 `$DSH_HOME` 在 OTel 回流中是一个稳定用户;不同 home 在构造上就是不同用户,无跨 home 关联机制。 +- OTel 回流与 launcher 回流各有各的 id(`.userid` 与 `telemetry.json`),无法互相关联——这是「不抽公共包」的直接代价,等真实关联需求出现再统一。 +- 删除 `.userid` 即重置身份(下次启动生效);home 不可写时每进程各自持有一个内存 id 直至恢复可写。 +- [默认挂载 Note](2026-07-31-web-telemetry-default-mount.md) 的身份 follow-up 中「匿名用户 id」项由本决定关闭;hostname/surface 维度与脱敏规则、usage-metrics track 仍是待办。 diff --git a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.i18n.yaml index 97793a906a..dd15a55a21 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md -2026-07-31-web-telemetry-default-mount.md: 6c1fdaa8719ee01726b51db9a469ff659cbac476 -2026-07-31-web-telemetry-default-mount.zh.md: b447832527ba9731097cd0776060db11ee4dfc30 +2026-07-31-web-telemetry-default-mount.md: e9ec7d0cda37db44e753c9aee572763b7e24ada6 +2026-07-31-web-telemetry-default-mount.zh.md: 68b411d0668772ce81d7f323c2d286714a223ca4 diff --git a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md index 6c1fdaa871..e9ec7d0cda 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md +++ b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md @@ -35,5 +35,5 @@ The keyless integration test `apps/cli/tests/telemetry-web.e2e.ts` pins the depl ## Consequences - A developer running `dsh web` without a local collector POSTs to the production endpoint every 10s (silent failure when unreachable; no OTel diag logger is registered); local development sets `DSH_TELEMETRY_DISABLED=1` or points `DSH_TELEMETRY_OTLP_URL` locally. -- **No redaction rule is mounted yet**: exports are the raw captured copy (full user/assistant message text, tool arguments and results, the system prompt, the local `session.cwd` path). Crossing a trust boundary requires `telemetry/record` rules first — the redaction rule, identity Resource attributes (hostname / anonymous user id / surface), and the usage-metrics track are the explicit follow-ups of this decision. +- **No redaction rule is mounted yet**: exports are the raw captured copy (full user/assistant message text, tool arguments and results, the system prompt, the local `session.cwd` path). Crossing a trust boundary requires `telemetry/record` rules first — the redaction rule, the remaining identity Resource attributes (hostname / surface; the anonymous user id shipped via the [anonymous-user-id Note](2026-07-31-telemetry-anonymous-user-id.md)), and the usage-metrics track are the explicit follow-ups of this decision. - Test rigs reusing this tree (e.g. `apps/web/tests/scaffold.ts`) must explicitly disable the row, or fixture sessions stream to whatever collector the environment happens to name. diff --git a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.zh.md b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.zh.md index b447832527..68b411d066 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.zh.md @@ -35,5 +35,5 @@ Status: implemented ## Consequences - 无本地 collector 的开发者跑 `dsh web` 会对生产 endpoint 每 10s 发一次 POST(联不通则静默失败,OTel diag logger 未注册);本地开发设 `DSH_TELEMETRY_DISABLED=1` 或 `DSH_TELEMETRY_OTLP_URL` 指本地。 -- **当前零脱敏规则挂载**:导出即原始捕获副本(用户/助手消息全文、工具参数与结果、system prompt、`session.cwd` 本地路径)。跨信任边界前必须挂 `telemetry/record` 规则——脱敏规则、身份 Resource 维度(hostname/匿名 user id/surface)、使用数据 metrics 轨三件是本决策明确的后续工作。 +- **当前零脱敏规则挂载**:导出即原始捕获副本(用户/助手消息全文、工具参数与结果、system prompt、`session.cwd` 本地路径)。跨信任边界前必须挂 `telemetry/record` 规则——脱敏规则、其余身份 Resource 维度(hostname/surface;匿名 user id 已由[匿名用户 id Note](2026-07-31-telemetry-anonymous-user-id.md)落地)、使用数据 metrics 轨是本决策明确的后续工作。 - 复用这棵树的测试载具(如 `apps/web/tests/scaffold.ts`)须显式关停该行,否则 fixture 会话会流向 env 里碰巧存在的 collector。