产品分析
服务器负责每一个已连接客户端(client)的分析投递、选择退出和身份。客户端不加载 PostHog 的浏览器 SDK。客户端使用事件(event)需要一条已认证的连接,所以只是打开托管应用、并没有连上,并不算产品使用。
安装与所有者
distinct_id 沿用每个构建一直使用的那条规则:先取哈希后的 Codex 工作区,否则取 Claude 的机器 id,否则取这次安装的匿名 id。有人登录时它可能变化,而且一个 Codex Team 会共用同一个,但留着它,PostHog 的图表和历史就不会断。不要改动它;要加就加一个字段。
服务器能够保存自己的状态时,每条事件还会带上 installId:状态目录里一个随机 id 的哈希,对这次安装永远不变。如果这个文件写不进去,事件仍像以前一样,挂在 distinct_id 下发出。统计安装数就用它。一次安装属于谁,是另一件事实,以 install.owner_observed 发送。所有者在查询时于我们自己的存储中解析,因此规则可以改,并且能套用到全部历史。
所有者来源,从强到弱:先是 T3 Connect,然后是 OpenAI 和 Claude 的人员 id,再然后是客户端设备。服务器报告它自己读得到的内容:已关联的 Connect 账户,以及提供方(provider)登录。正在连接的客户端报告自己已登录的 Connect 账户和自己的设备,从而把从同一部手机、同一个浏览器或同一台桌面打开的每个环境(environment)串起来。用 OpenAI 的 user_id 声明,不要用 Codex 的 account_id(ChatGPT 工作区,整个 Team 共用),也不要用令牌上的 sub(每种登录方式各有一个)。两个 Connect 账户绝不是同一个人,哪怕它们共用一些更弱的 id。
每个 id 都由两端的同一个辅助函数做 SHA-256 哈希,因此同一个 Connect 账户在客户端和服务器上哈希相同。服务器只接受客户端发来的哈希。
归因边界
客户端维度属于这条事件(event)所在的 WebSocket 连接。如果在服务器上放一个全局的「当前客户端」,就会把同时发生的 Web、桌面和移动端使用归错。提供方执行有自己的事件,因为一个回合(turn)可能比发出请求的那条连接活得更长。
客户端维度和服务器维度要分开。一台桌面主机可以服务一部手机或一个远程浏览器,直接连接也可能跨过网络。较旧的客户端会省掉元数据。缺失的客户端值必须保持未知,不能拿服务器属性回填。遗留属性 clientType 描述的是服务器以什么方式运行;已连接的客户端用 surface。
解读事件
活跃使用报告使用 client.turn.requested。没有连接事件,因为 T3 Connect 会话每小时过期,每次重连都会再被计一次。命令(command)的 id 如果已经被接受过,那就是一次重试,因此不会再次记录。同一个身份在一段时期内可以出现在多个客户端分组里;把这些分组的人数加起来,会把用户算重。
提供方的发送次数和完成次数不必一致。provider.turn.completed 表示一个提供方回合,segment 说明是什么把它启动的:这次运行的第一次尝试(initial)、一次 steering_restart、提供方的一次 continuation(例如 Codex 的目标回合),或者一个 native_child 子智能体。createdBy、creationSource、scheduled 和 subagent 描述这次运行的起始消息,childKind 则把原生子智能体和 T3 管理的子对话线程(thread)区分开。这个事件用来做逐回合的测量,不要用来数请求次数。
provider.request.completed 表示一次运行,在执行它的那个服务器进程里、当这次运行落定时记下。每个根回合只加一次,因此继续、转向,以及被重放的提供方事件都不会多出一行。requestKind: "user" 统计人们提出了什么。原生子智能体的回合与根分开合计(childUsageStatus、childInputTokens);T3 管理的子对话线程把自己的请求报告为 t3_child。落定时还在跑的子项,之后不会再补记,摘要里会设置 childrenRunning。服务器一重启,进行中那次运行的摘要就丢了。重启后继续的运行,用 restartContinuation 和 lifecycleObserved: false 报告自己的用量;它找不回原始请求的次数或种类。提供方会话始终没有打开的运行没有摘要;提供方拒绝的那次启动会报告 providerTurns: 0。如果 Stop 正好落在一次转向重启刚刚开始的时候,这次运行会在没有摘要的情况下结束。采集是尽力而为的,不会去扫描或回填提供方的历史。
这些计数覆盖主智能体。otherInputTokens 和 otherOutputTokens 是提供方自己的账本在这个回合里、于主智能体之外记下的量:Claude 按进程统计的 modelUsage 增长,其中包含原生子智能体、压缩和辅助调用。它们需要来自同一进程的基线,所以基线未知,或者账本往回走时,这两项就缺席;在回合之间结束的后台工作,会计入下一个回合。Claude 的子智能体没有自己的回合,因此这部分不会和子回合重叠。
model、effort 和 serviceTier 是这次运行选定的内容。serviceTier 的取值是 standard、flex、fast 或 ultrafast,来自 Codex 的层级选项或 Claude 的快速模式;选择没有设置时,它就缺席。只有提供方报告了被计数的那部分工作所用的模型时,observedModel 和 mixedModels 才会出现(Codex 的对话线程设置与改道、Claude 的根消息、OpenCode 的步骤)。出现 mixedModels: false 但没有 reportVersion,表示从未检查过。模型标签来自随附的模型清单,以及 Cursor 和 Grok 的目录;像 anthropic/claude-opus-5-5 这样被路由的 id 也一样。effort 来自一个闭合列表,因此用户配置的名称不会进来。
状态和计数回答的问题不一样。usageReported: false 表示适配器(adapter)没有附上任何用量;某一驾驭层(agent harness)如果每一行都是如此,那是不受支持,不是处于空闲。partial 保留那些有效、但无法得出整回合合计的计数,例如没有报告就结束的 OpenCode 步骤、一次 OpenCode 流重连,或一段被中断的片段。明确的零会发送出去,未知的计数则保持缺席。localCommand 用来标记提供方本地的命令,例如 Claude 的 /compact,它的结果计数并不算模型工作。推理 token 是输出的子集。
用这些事件做的图表,应当表述为「每次被测量的提供方完成对应的输出 token」,比较时对齐提供方、观测到的模型、effort、交互模式(interaction mode)和结束状态,并按驾驭层、来源和状态列出符合条件的计数与已测量的计数,好让未知模型和不受支持的适配器留在分母里。算输出与输入之比时,用加总后的总量相除;如果对每个回合的比率取平均,输入很小的回合会占上风。没有 reportVersion 的行,缺了的字段保持未知;缺失的来源绝不能当成一个人。
feature.used 统计那些会启动一项功能的 WebSocket 请求。RPC 在请求成功之后,于 RPC 授权中间件中被观测;已分发的命令由分发处理程序记录,这个处理程序分得出重试。功能映射 用允许列表限定方法和命令,变体只取自闭合的契约枚举,因此路径、id 和文本永远到不了分析。共享对话线程的访客没有 WebSocket 连接,所以访客分享服务自己记录他们的加入和消息,并且不带 surface。
智能体工具事件报告驱动(driver)的种类,绝不报告提供方实例的 id:用户会给这些实例起名,所以实例 id 可能对应到某个账户或雇主。提供方维度 只在模型是供应商目录中的非自定义条目时才报告该模型。OpenCode、Pi 和 ACP 智能体从用户配置里列出模型,例如本地的 Ollama 标签,所以它们的模型不进入这里。交接设置来自工具参数里的闭合枚举和布尔值;提示词、标题和 id 一律不读。对话线程 id 只用来查找两侧的对话线程,并且不会离开服务器,这也是工具事件不带按对话线程序号的原因。一次交给多个对话线程的交接,例如 create_threads,会为每个目标记一条 mcp.tool.invoked,每条都重复这次调用的结果和耗时,所以事件条数并不是调用次数。
投递
服务器把批次发往 ingest.t3.codes 上的摄入端点。这个端点归档每一条事件,并把其中一部分转发到 PostHog。一次发送可能在端点已经存下这个批次之后才失败,所以每次重试都是一份副本。投递 在记录事件时给它一个 uuid(没有 uuid 的事件会被端点拒绝),发送失败之后退避,并在尝试几次后丢弃这个批次。没有这些限制的时候,一个卡住的批次曾经一连几天、每秒发送一次。
采集边界
分析载荷只放产品元数据和规范化的测量值。不要发送提示词、认证材料、提供方的原始载荷、用户指定的设备名称,或对话标识符。客户端元数据是尽力而为的;值不合法也不得因此拒绝连接。PostHog 的人员档案保持禁用。
Dev-runner 服务器和 CI 的归档冒烟测试会关掉遥测,中继(relay)追踪也会一起停掉,因为它们每一种都会被当成一次新安装。要从开发服务器上测试分析或中继追踪,请设置 T3CODE_TELEMETRY_ENABLED=true。