# Agent Note: dsh web 组合默认挂载会话遥测（OTel 上报）

Status: implemented

[English](2026-07-31-web-telemetry-default-mount.md) | 中文

## Problem

遥测 seam 与 OTel backend（[revival Note](2026-07-23-session-telemetry-otel-revival.md)）自完成以来从未接入任何部署组合：没有 roster 行、没有开关、没有节奏口径，内部部署对用户会话零可观测。需要一个部署决策：哪些 surface 上报、报到哪、什么节奏、怎么关、CI 怎么隔离。

## Decision

`dsh` 共享 base（`apps/cli/config/base.cordis.yml`）默认挂载 `telemetry-otel` 行，内置生产 endpoint，因此 Web 与 headless 都会上报；原始配置命令也会先挂载该行，再应用其必需的部署 overlay。这是**内部测试期的部署立场**——有 endpoint 就上报，用户可通过环境变量退出。Web 与 headless 在 SIGINT/SIGTERM 时使用[有界、可升级的进程关闭控制器](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md)，在启动器 5 秒上限到期前，先给后端 3 秒关闭截止时间完成排空。

| 决策项 | 取值 | 理由 |
|---|---|---|
| 挂载面 | base.cordis.yml（原始配置 + Web + headless） | 所有加载共享 base 的配置树采用同一个部署立场；原始配置 overlay 决定该部署是否创建会话 |
| endpoint | `DSH_TELEMETRY_OTLP_URL`，缺省 `https://harness-telemetry.deepseeksvc.com/v1/logs` | 内部 collector；env 覆盖供本地/联调 |
| 退出开关 | `DSH_TELEMETRY_DISABLED` 非空（含 `0`/`false`）即关 | 隐私向开关取「宁关勿误开」；行级 disable 只能在 AppCLIEntry 的 patch 层做（config 无 disable 语义，且必须先于 `exporter.url` 的加载期校验生效） |
| 上报节奏 | `processor.scheduledDelayMillis: 10000`（10s/批） | 流式回流，非退出才报；崩溃至多丢最后一个未导出间隔 |
| 退出 drain 上界 | `exporter.timeoutMillis: 1000` + `maxExportBatchSize: 2048（== maxQueueSize）` + `exportTimeoutMillis: 1500` + `shutdownTimeoutMillis: 3000` | collector 不可达的常规故障会在约 1s 内放行：timeoutMillis 是单次 socket 超时与重试 deadline，使用与队列等大的单批可避免依次排空导致耗时倍增。由 DSH 管理的 3s 外层上限覆盖 SDK 先执行的无界 `forceFlush()` 等待，即传输 Promise 始终无法取得 socket 的情况。 |
| 压缩 | `compression: gzip` | 事件 body 含全文，跨机房带宽 |
| CI 隔离 | 全部 8 个 GitHub workflow 顶层 `env: DSH_TELEMETRY_DISABLED: '1'` | CI 启动 web 组合的所有通道（e2e/snapshot/built smoke）不得向生产 endpoint 泄测试会话 |

集成测试 `apps/cli/tests/telemetry-web.e2e.ts`（keyless）钉住部署级行为：测试内 OTLP collector + mock LLM，真启动 `dsh web`，断言 ledger 覆盖、seq 单调、chunk 首条投影、以及 SIGINT drain 后 ops `shutdown` 标记到达。

## Alternatives considered

**默认不挂载，部署方自行加行（SDK 立场的延续）。** 否决于当前阶段：本仓的 web/headless 组合就是内部部署本身，「上报默认开」是这个部署的产品要求；SDK 立场仍由 seam 包保持（不挂 = 零外发）。

**开关做成 config 字段而非 env patch。** 不可行：cordis 行没有 config 层的 disable 语义，且 `exporter.url` 校验在插件构造期 fail-loud，开关必须在 Loader 之前生效——AppCLIEntry patch 层是唯一落点。

**退出时 `Promise.race` 兜底超时。** 最初暂缓，是因为 SDK 参数看似已经将后端排空耗时限制在约 1.5-3s（通常 <100ms），实测 SIGINT 到退出耗时 110ms-1.1s。后来在 Linux 沙箱中复现并证明，`BatchLogRecordProcessor.shutdown()` 可能在 `exporter.forceFlush()` 中永久等待，无法进入受 `exportTimeoutMillis` 限制的完成 Promise。因此，[CLI 关闭修复](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md) 既为这一特定缺口增加 3 秒后端上限，也为整棵插件树增加 5 秒进程级上限和重复信号退出途径。

## 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/surface；匿名 user id 已由[匿名用户 id Note](2026-07-31-telemetry-anonymous-user-id.md)落地）、使用数据 metrics 轨是本决策明确的后续工作。
- 复用这棵树的测试载具（如 `apps/web/tests/scaffold.ts`）须显式关停该行，否则 fixture 会话会流向 env 里碰巧存在的 collector。
