telemetry
插件分析:通过 $.telemetry 的两个事件 hook,让插件记录事件,或把功能使用标记为一条 first-party 记录,并使用工作阶次自己的凭据分批发送。
关于这个 mod
telemetry
这是一个作为插件提供的插件分析 mod。它对 telemetry.log 和 telemetry.mark 的 hook 实现这两个事件,建立在 engine.create 步骤传入的那些名词之上;上方的门只服务于 Claude Code 内置的插件:由用户安装或管理员列出的插件发起的调用会带着原因被拒绝(主机会为每次调用盖上发起它的插件、next.origin,门则读取其层级)。如果某个引擎没有自己的 $.telemetry,同一步骤会把这个名词补上,因此这些调用在那里也存在。条目用 to 标明去向:anthropic(默认值)是这个 mod 的去向;collector 则表示操作员配置的 telemetry collector,会原样传给下层,而 to 永远不会成为记录的一部分。 $.telemetry.log({ event, props }) 会把一个事件排入队列,作为一条 first-party 记录 tengu_plugin_<event>;$.telemetry.mark({ feature, kind, reason?, props? }) 会像 CLI 自己的功能事件那样,把一次功能使用标记为 tengu_feature_<kind>,并在旁边放上 feature_name 和这次标记的属性。两者都会在记录排入队列后完成。记录会分批发出:第一条记录排队几秒后,用工作阶次自己的凭据向事件记录接收端发出一次 POST(每个批次都会解析 $.session.authorize());等待中的数量达到 100 时立即发送,工作阶次结束时也会发送。如果接收端因服务器错误、超时或速率限制拒绝批次,会再试一次。没有 first-party 凭据的工作阶次,或仍被接收端拒绝的批次,会被丢弃;每种结果都会在 debug log 中占一行。
每条记录都带有 CLI 自己的记录会带的内容,这些内容通过 $ 在每个工作阶次中收集一次:事件 ID、安装的设备 ID、CLI 全局配置中已登录账户的 ID、工作阶次 ID、模型、客户端类型、入口点和交互性,以及 env 区块(一次 uname 探测得到的平台和架构、终端、shell、套件管理器和运行时、CI 与 GitHub Actions、远程容器、部署、Linux 发行版和内核、WSL、工作目录的版本控制,以及通过 $.session.version() 取得的引擎版本、基础版本和构建时间;引擎不回答时留空),并在记录的属性旁放上仓库的远程 hash。只有引擎知道的内容(运行时版本、进程记忆体、请求的 betas、订阅层级、调用代理)不在 $ 上,因此这些栏位保持空白。
只要 CLI 自己的分析功能被关闭,它就不会发送任何内容:处于 DISABLE_TELEMETRY、CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC 或 DO_NOT_TRACK 之下时、测试运行时、使用主机不管理的任何第三方提供者(Bedrock、Vertex、Foundry 等)时、位于云端 gateway(环境开关或受管理策略的登录 pins)时,以及位于拥有自己 OAuth URL 的部署中时,都会停止发送。每个批次发送前都会通过 $.env 和 $.settings 重新读取这些设置,因此工作阶次后来切换到第三方提供者或 gateway 后就不会再发送;如果无法读取开关,也不会发送。USER_TYPE 表示对应情况时,记录的 user_type 是 ant,否则是 external。
记录中不会出现自由格式内容。事件名称和每个属性键都是 snake_case token;值只能是有限数字、布尔值或 Choice(与可选列表一起命名的字符串),log 和 mark 都一样。mark 接受 ok、sad 或 bad;后两者必须提供 reason,前者则不接受 reason。违反规则的条目会在排队前被拒绝。对于环境变量,如果变量的值是秘密,或包含人员或主机名称,只会读取它是否已设置,不会读取其他内容;shell 则是来自封闭清单的 basename。
hooks/register.ts 是模块;types/index.d.ts 是这个名词的契约,也是这个 mod 的 hook、调用该名词的 mod,以及回应它的测试共同读取的 $.telemetry 唯一声明。
它 hook 的内容
telemetry.* 是门:内置层级(或引擎)的调用会继续,其他调用会被拒绝,门本身抛出异常时也会拒绝。 telemetry.log 和 telemetry.mark 位于门下:检查条目并将记录排队,hook 会回应 { value };如果条目违反规则,则回应带有原因的 { deny },让调用方的 promise 以该名称拒绝。engine.create:sender 建立在 await next(e) 之上,该步骤向上交出 { ...{ telemetry }, ...beneath };beneath 会最后展开,因此下层已有自己的 telemetry 时保留它,没有时才加入这个 mod 的 telemetry;下层没有任何内容会被替换。 session.start 用于得知用户是否在提示词前;session.end 用于发送仍在等待的内容。
它在 $ 上调用的内容
session.authorize、session.id、session.model、session.surfaces、session.cwd、session.repo、settings.read、env.get(开关和描述用的变量,按字面名称读取)、fs.read、fs.list、fs.exists、process.run(一次执行 uname 和 command -v 的 sh -c)、clock.after、clock.sleep、http.fetch 和 ui.log(写入 debug log),都通过 fold 交给它的介面调用。
运行位置与服务对象
这个插件由 CLI 本身载入,在自身分析功能开启的每个构建中运行,其他地方都不运行;它服务于 CLI 内置的插件,拒绝其他所有调用方。它不是设计来通过 --plugin-dir 安装或载入的;文件夹带有清单,是为了让它像其他插件一样被读取,而不是为了独立运行。内置插件在这个插件不存在时调用 $.telemetry,会找不到该名词;如果引擎有自己的名词,则会找到一个不会排队任何内容的版本。无论哪种情况,都是 "no analytics here"。
安装
请先查看作者 README 确认 marketplace 和插件名称;命令可能随仓库结构改变。
claude plugin marketplace add anthropics/claude-code claude plugin install telemetry
原文 / README
telemetry
Plugin analytics as a plugin: its hooks on telemetry.log and
telemetry.mark are what those two events do, built over the nouns its
engine.create step is handed, and a gate above them serves the plugins
built into Claude Code alone: a call from a plugin a person installed or an
administrator listed is refused with a reason (the host stamps every call
with the plugin that raised it, next.origin, and the gate reads its tier).
On an engine that has no $.telemetry of its own the same step adds the
noun, so the calls exist there too. An entry names where it goes with to:
anthropic, the default, is this mod's; one for collector, the telemetry
collector an operator configured, is passed on beneath untouched, and to
is never part of a row.
$.telemetry.log({ event, props }) queues one event as one first-party
row, tengu_plugin_<event>; $.telemetry.mark({ feature, kind, reason?, props? }) marks one use of a feature as the CLI's own feature events do,
tengu_feature_<kind> with a feature_name and the mark's properties
beside it. Both resolve once the row is queued. Rows go out in batches: one
POST to the event-logging ingest with the session's own credential
($.session.authorize(), resolved for each batch) a few seconds after the
first row was queued, at once when a hundred wait, and when the session
ends; a batch the ingest refuses with a server error, a timeout or a rate
limit is tried once more. A session with no first-party credential, or an
ingest that still refuses, drops the batch; each outcome is one line in the
debug log.
Each row carries what the CLI's own rows carry, gathered through $ once
a session: an event id, the install's device id and the signed-in account's
ids from the CLI's global config, the session's id, model, client type,
entrypoint and interactivity, and an env block (platform and
architecture from one uname probe, terminal, shell, package managers and
runtimes, CI and GitHub Actions, the remote container, the deployment, the
Linux distribution and kernel, WSL, the working directory's version
control, and the engine's version, base version and build time from
$.session.version(), left empty on an engine that does not answer it),
with the repository's remote hash beside the row's properties. What the
engine alone knows (its runtime's version, the process's memory, the
request's betas, the subscription tier, the calling agent) is not on $,
and those columns stay empty.
It sends nothing wherever the CLI's own analytics are off: under
DISABLE_TELEMETRY, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC or
DO_NOT_TRACK, in a test run, on any third-party provider (Bedrock,
Vertex, Foundry and kin) the host does not manage, on a cloud gateway
(the environment's switch or the managed policy's login pins), and on a
deployment with its own OAuth URL. Each is read through $.env and
$.settings before every batch, so a session that has since moved to a
third-party provider or a gateway sends nothing more; when the switches
cannot be read, nothing is sent either. The row's user_type is ant
when USER_TYPE says so, else external.
Nothing free-form reaches a row. An event name and every property key is a
snake_case token; a value is a finite number, a boolean, or a Choice (a
string named together with the list it is chosen from), under log and
mark alike; mark takes ok, sad or bad, with a reason required on
the last two and refused on the first. An entry that breaks a rule is
refused before anything is queued. Of the environment, a variable whose
value is a secret, or names a person or a host, is read for whether it is
set and nothing more; a shell is its basename from a closed list.
hooks/register.ts is the module; types/index.d.ts is the noun's contract,
the one declaration of $.telemetry that this mod's hooks, a mod calling the
noun and a test answering it all read.
What it hooks
telemetry.*, the gate: a caller in the built-in tier (or the engine) goes
on, any other is refused, and a gate that throws refuses too.
telemetry.log and telemetry.mark, beneath the gate: the entry is checked
and its row queued, the hook answering { value }, or { deny } with the
reason for an entry that breaks a rule, so the caller's promise rejects
naming it. engine.create: the sender is built over await next(e), and
the step hands up { ...{ telemetry }, ...beneath }: what is beneath is
spread last, so its own telemetry stands where it has one and this mod's
is added where it has none; nothing beneath is replaced.
session.start, to learn whether a person is at the prompt; session.end,
to send what still waits.
What it calls on $
session.authorize, session.id, session.model, session.surfaces,
session.cwd, session.repo, settings.read, env.get (the switches and
the describing variables, by literal name), fs.read, fs.list,
fs.exists, process.run (one sh -c of uname and command -v),
clock.after, clock.sleep, http.fetch and ui.log (to the debug log),
each on the interface the fold handed it.
Where it runs, whom it serves
This plugin is seated by the CLI itself, on every build whose own analytics
are on, and nowhere else; it serves the plugins bundled with the CLI and
refuses every other caller. It is not meant to be installed or loaded with
--plugin-dir; the folder has a manifest so it reads like every other
plugin, not so it can stand alone. A built-in that calls $.telemetry
where this one is absent finds no such noun, or, on an engine with the noun
of its own, one whose calls queue nothing; either way that is "no analytics
here".

