拆 browser-use 的遥测与观测:收什么字段、怎么接外部平台、想关掉动哪一处

2026-07-30

本文基于 browser-use 仓库 commit f0aa3a8(2026-07-27)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/browser-use/browser-use 最新代码与文档为准。

**这个项目里的「匿名遥测」,匿名的是你这台机器的身份,不是任务的内容。**设备 id 确实是哈希出来的,但同一条上报事件里躺着 task 原文、urls_visitedfinal_result_responseerror_message。你如果只看 README 里那句「anonymized telemetry」就放心地把它跑在内网数据上,那是把两件事当成了一件事。这篇把遥测和观测这两条线分开拆:遥测是项目方往自己的分析后台收数据,观测是留给你往自己的平台接 trace。两条线的开关、位置、粒度完全不同。

站内已有的 Agent 可观测性与日志怎么设计 谈的是通用做法,日志里的敏感信息处理 谈脱敏思路,pi 用 hooks 做可观测 是另一个项目的另一种取向;这篇不重复方法论,只对着 browser-use 这一份代码逐字段核实。

一、遥测这条线解决的是项目方的问题,不是你的问题

先把动机说清,后面的设计取向才讲得通。开源 Agent 项目最缺的不是 star,是「真实任务上到底哪一步崩了」。维护者拿不到用户的失败样本,就只能等 issue。browser_use/telemetry/service.py 里的 ProductTelemetry 就是为这件事存在的:它把一次 Agent 运行压成一条事件,发到项目方的 PostHog 项目里。

实现上没有玄机。POSTHOG_PROJECT_API_KEYPOSTHOG_HOST 是明文常量写在文件顶部的,host 指向 https://eu.i.posthog.com。客户端初始化时传了两个值得注意的参数:disable_geoip=False(不禁用 IP 归属地推断)和 enable_exception_autocapture=True(自动捕获异常)。后者意味着上报面比 views.py 里显式声明的那些字段更宽——异常自动捕获会带上堆栈,而堆栈里通常有文件路径。

ProductTelemetry 上挂了 @singleton(实现在 browser_use/utils.py,一个用闭包缓存实例的小函数)。这个细节后面会变成一个真实的坑:开关是在 __init__ 里读的,进程内第一次实例化就定型了。

身份这块的降级链条写得挺克制,get_or_create_device_id() 按顺序试四层:环境变量 BROWSER_USE_DEVICE_ID、配置目录里的持久化文件、机器指纹、随机 id。持久化文件路径是 CONFIG.BROWSER_USE_CONFIG_DIR / 'device_id',写入用的是先写 .{pid}.tmpos.replace 的原子替换,避免多进程同时启动时读到半截内容。机器指纹 _machine_fingerprint()uuid.getnode()socket.gethostname() 拼串做 sha256、截前 32 位、加 bu_ 前缀;它还判了一件对的事——如果 getnode() 的 multicast 位被置上,说明取 MAC 失败返回的是随机数,这时候指纹没有稳定性意义,直接返回 None 让位给随机 id。

对你意味着什么:这个 id 不指向你的身份,但它稳定指向「这台机器」。如果你的配置目录不做持久化,每次运行都是新 id;反过来想让多实例聚成一个来源,就显式设 BROWSER_USE_DEVICE_ID

二、一条 agent_event 里到底有什么

事件定义在 browser_use/telemetry/views.py,一共三类,都继承 BaseTelemetryEvent。基类的 propertiesasdict(self) 把整个 dataclass 摊平(只排除 name),再补一个 is_docker 字段——也就是说,字段是「声明即上报」,加一个 dataclass 字段就等于加一个上报项,没有白名单那道闸。

AgentTelemetryEvent(事件名 agent_event)的字段按注释分四段:启动信息、步骤信息、结束信息、判定信息。发送点在 browser_use/agent/service.py_log_agent_event(),整个 run 结束时发一条,不是每步一条。里面有几处处理值得看:

  • cdp_url 不是原样上报的,取的是 urlparse(...).hostname,只留主机名。
  • action_history 是把每步的 model_output.action 逐个 model_dump(exclude_unset=True) 后塞进去的,也就是模型选了哪个动作、参数是什么,全在里面。
  • task 是原样的任务字符串,final_result_responsehistory.final_result() 的 json 序列化结果,urls_visited 是访问过的 URL 序列。
  • judge_* 五个字段来自 history.judgement(),其中有一个是 judge_reached_captcha。项目方显然清楚 Agent 会撞上验证码,并且在统计它。

另外两类事件面向 MCP。MCPClientTelemetryEventmcp_client_event)在 browser_use/mcp/client.pyfinally 块里发,action'connect''disconnect''tool_call',带 server_namecommandtools_discoveredtool_nameduration_secondsMCPServerTelemetryEventmcp_server_event)的字段更少,但多一个 parent_process_cmdline——父进程命令行。做 MCP 集成的人自己判断这条能不能接受。

组成部分它负责什么仓库位置你什么时候会碰到它
ProductTelemetry单例客户端,判断开关、发事件、flushbrowser_use/telemetry/service.py想确认到底发不发、发去哪
事件 dataclass定义三类事件的字段与事件名browser_use/telemetry/views.py合规评估时逐字段过一遍
_log_agent_event()run 结束时组装 agent_eventbrowser_use/agent/service.py想知道字段值从哪来
MCP 侧上报连接、断开、工具调用三类动作browser_use/mcp/client.pybrowser_use/mcp/server.py自建 MCP server 接进来时
observe / observe_debug可选的 trace 装饰器,无依赖时退化为空壳browser_use/observability.py想接自己的观测平台
动作级 span每个动作名一个 TOOL 类型 spanbrowser_use/tools/service.py想看单个动作耗时与参数
第三方接入示例用 traceloop 的 SDK 起 OpenTelemetryexamples/observability/openLLMetry.py照着接自己的 collector
开关与默认值环境变量读取与默认值判定browser_use/config.py关遥测、关云同步

三、装饰式观测:真的接上还是接了个空壳

browser_use/observability.py 这个文件只有两百行,但它是这个项目里少见的「设计取向很明确」的一块。它导出 observeobserve_debug 两个装饰器,参数签名一致:nameignore_inputignore_outputmetadataspan_typeLiteral['DEFAULT', 'LLM', 'TOOL']),加 **kwargs 兜底。

真正的分支在文件顶部:它 try 一下 from lmnr import observe as _lmnr_observe,成功就把 _LMNR_AVAILABLE 置真,失败(捕的是 ImportErrorTypeError)就走 _create_no_op_decorator。空壳装饰器接受完全相同的参数,只是原封不动地转发调用,且区分了协程函数和同步函数两种包装。这个选择的代价和好处都很清楚:核心代码可以在成百个位置无脑撒装饰器而不引入硬依赖,但装饰器本身不提供任何抽象层——它绑定的是 lmnr(Laminar)一家的 observe 接口,不是一个通用协议。

两个装饰器的差别有两处。一是 tagsobserve['observe', 'observe_debug']observe_debug 只打 ['observe_debug'],源码里那句注释还提醒 tags 需要先在 Laminar 上创建好。二是生效条件:observe 只要 lmnr 可用就生效,observe_debug 额外要求 _is_debug_mode() 为真。

这里有个必须点出来的实现与文档不一致:observe_debug 的 docstring 写着 debug 模式由 DEBUGBROWSER_USE_DEBUG 环境变量或 root 日志级别决定,而 _is_debug_mode() 的实现只读一个东西——LMNR_LOGGING_LEVEL 是否等于 debug。你照 docstring 设变量,什么都不会发生。文件还提供了 is_lmnr_available()is_debug_mode()get_observability_status() 三个查询函数,最后那个返回含 lmnr_availabledebug_modeobserve_activeobserve_debug_active 的字典,接平台时先打印它比猜要快。

埋点密度可以自己数。agent/service.py 里有 @observe(name='agent.run')@observe(name='agent.step') 这两个主干 span,get_next_actionget_model_outputobserve_debugbrowser/session.py 上有 browser_session_startget_browser_state_summarytake_screenshotget_element_coordinates 等;dom/service.pyget_dom_treeget_serialized_dom_treebrowser/watchdogs/ 那 14 个 watchdog 里也散着 click_element_eventscreenshot_event_handler 这类命名。绝大多数 observe_debug 都带着 ignore_input=True, ignore_output=True,意思是常态下 span 里只有名字和时序,没有内容。

有一处写法很妙,值得单独说:LLM 调用超时时,_get_next_actionexcept TimeoutError 分支里现场定义一个空函数 _log_model_input_to_lmnr,用 @observe(name='_llm_call_timed_out_with_input') 包住再 await 一次,函数体是 pass。它什么也不做,唯一目的是让超时时的 input_messages 作为这个 span 的 input 落进 trace。超时最难查的就是「当时喂进去的是什么」,这招用装饰器的输入捕获顺手解决了。

动作级 span 走的是另一条路。browser_use/tools/service.py 直接 try import Laminar 类,执行每个动作时用 Laminar.start_as_current_span(name=action_name, input={'action': ..., 'params': ...}, span_type='TOOL') 开 span,import 失败就换成 contextlib.nullcontext()。注意这里 input 是显式传的,动作名和参数会进 trace——和上一段那些 ignore_input=True 的埋点不是一个口径。

四、往外接平台:接口其实是「别人的 SDK 自己去 patch」

examples/observability/openLLMetry.py 这个示例只有二十几行,信息量集中在它没做的事上。它 try import traceloop.sdkTraceloop,失败就打印一句提示并 exit(1);成功就读 TRACELOOP_API_KEY,调 Traceloop.init(api_key=api_key, disable_batch=True),然后跑一个普通的 Agent。全程没有向 browser-use 注册任何回调、没有传任何参数——接入完全靠外部 SDK 自己去做 instrumentation。

skills/open-source/references/monitoring.md 里给的另外两条路子也是同一个形状:Laminar 是 pip install lmnrLaminar.initialize()(配 LMNR_PROJECT_API_KEY),OpenLIT 是 openlit.init(),需要自建 collector 就写 openlit.init(otlp_endpoint="http://your-collector:4318")。成本这块另有一套东西,Agent(..., calculate_cost=True) 打开后可以从 history.usageagent.token_cost_service.get_usage_summary() 取;_log_agent_event() 里的 token 字段就是从 token_cost_service.get_usage_tokens_for_model() 拿的。要把这些数字变成团队口径,可以对着 token 用量统计怎么做 那套方法组织。

这种「不定义自己的观测接口、让外部 SDK 去 patch」的取向,好处是项目侧零维护成本,代价是接入的稳定性挂在第三方 SDK 对内部函数的适配上——上游函数改名或搬家,你的 trace 就可能悄悄少一层,而且不会报错。真接进生产前,先跑一个已知会失败的任务,确认失败那一步在你的平台上看得见。

五、边界与代价:它明确不管的事

放弃了什么,得摆明白。

遥测粒度是「一次运行一条」,不是运维数据。 agent_event 在 run 收尾时才发。进程被 kill -9、机器断电、容器被驱逐,这条事件就不存在。agent/service.py 里确实为强制退出注册了回调调 telemetry.flush()(还有个 _force_exit_telemetry_logged 标志位防重复),但那只覆盖它能拦到的中断信号。拿它当监控用是走错门了。

常态下的 span 是空的。 大量 observe_debug 埋点写死了 ignore_input=True, ignore_output=True,非 debug 档你只能看到调用树和耗时。想看内容就得进 debug 档,而 debug 档的内容会连着页面文本、DOM 序列化结果、截图一起进你的观测平台——这就从「不够看」直接跳到「什么都看得见」,中间没有档位。

它不管脱敏。 sensitive_data 那套机制解决的是另一个问题:占位符替换发生在动作执行时(tools/registry/service.pyexecute_action 路径里调 _replace_sensitive_data),所以历史里存的动作参数是占位符而不是真值。但 urls_visitedfinal_result_responseerror_message 这些来自页面和运行结果的字段没有这层保护。examples/features/secure.py 的注释也直说了:默认会把截图传给 LLM,截图里可能有你的信息,不想传就 use_vision=False

它不管目标站点那一侧的任何事。 这类工具驱动的是真实浏览器,可能带着你已登录的会话去操作真实账号。目标站点的使用条款、验证码与反自动化机制、账号被判定为异常的可能、以及 Agent 把敏感数据带去第三方站点的外泄面,全部是你的责任。事件里有 judge_reached_captcha 这个字段只说明它统计了撞墙频率,不代表它替你解决了合规问题。碰到验证码和风控就该停下来评估这个自动化本身是否被许可,而不是想办法绕。数据面的账要怎么记,AI 应用的数据安全风险 那篇有更完整的清单。

六、上手与避坑清单

关开关要在 import 之前设。 ProductTelemetry 是单例,__init__ 里一次性读 CONFIG.ANONYMIZED_TELEMETRY。你在 Agent 已经构造完之后再改 os.environ,那次实例化的结果不会回退。examples/features/secure.py 的顺序是对的:先 load_dotenv()、设 os.environ['ANONYMIZED_TELEMETRY'] = 'false',再 from browser_use import Agent。放 .env 里同样可行,ANONYMIZED_TELEMETRY=false 一行。判定逻辑是取小写首字符是否落在 ty1 里,所以 Falseno0 都算关,默认值是 true

别以为关了遥测就断了所有外发。 config.pyBROWSER_USE_CLOUD_SYNC 的默认值是拿 ANONYMIZED_TELEMETRY 的字符串化结果当 fallback 的——默认联动,但它是独立的键,你可能在别处显式打开过。要确定就两个都显式写。另外 browser_use/llm/browser_use/chat.py 里,走项目自家云模型时请求 payload 会带一个 anonymized_telemetry 字段把这个开关值一起传上去;至于你选的 LLM 服务商那边收到什么、留多久,各家规则不同且会调整,以官方最新说明为准。

observe_debug 打不开的原因八成是变量名。 前面说过,docstring 和实现不一致,实际只认 LMNR_LOGGING_LEVEL=debug。这个坑之所以容易踩,是因为项目里另有 BROWSER_USE_LOGGING_LEVEL 这个变量(遥测服务用它决定要不要静音 posthog 的 logger),名字近、作用不同。先调 get_observability_status() 看返回的四个布尔值,比逐个试变量快。

装了 lmnr 不等于有 trace。 装饰器只负责在 lmnr 可用时把调用转过去,初始化得你自己做(Laminar.initialize())。还有那句注释别漏:tags 需要先在平台上创建,否则打上去的 observeobserve_debug 标签未必如你所愿出现在筛选器里。

排查「为什么没数据」的时候,先分清是哪条线。 遥测那条线只要 _posthog_client is None 就在 capture() 里直接 return,不报错也不打 warning(只在初始化时打一条 debug 级的 Telemetry disabled);观测那条线在 lmnr 缺失时走空壳装饰器,同样静默。两条都是「静默降级」,所以「什么都没看到」既可能是没接上,也可能是接上了但被 ignore 掉了内容。

评估合规时别只看代码,把上报字段抄下来给人看。 views.py 的三个 dataclass 加起来不到九十行,直接把字段名列成表交给需要拍板的人,比转述「它收匿名数据」有用得多。基类用 asdict 全量摊平这一点尤其要说清楚——升级依赖后字段可能变多。

收个尾

一个自检顺序,按这个走一遍就够判断它能不能进你的环境:browser_use/telemetry/views.py 看字段(决定合规能不能过),browser_use/telemetry/service.py 看开关与身份(决定怎么关、关得干净不干净),browser_use/config.pyANONYMIZED_TELEMETRYBROWSER_USE_CLOUD_SYNC 的默认联动(决定还有没有第二条外发通路),browser_use/observability.py 看两个装饰器的生效条件(决定你自己的 trace 有没有内容),最后 examples/observability/openLLMetry.py 照着接一遍你的 collector。

这个项目采用 MIT 许可证,代码就在那儿,上面每一句都能自己开文件核。真正需要你做判断的只有一个问题:task 原文和 urls_visited 离开这台机器,在你的场景里是可以接受的成本,还是不可以。这个问题没有通用答案,但它必须在你把 Agent 指向生产数据之前有答案。

本篇属于一个把开源浏览器操作 Agent 项目 browser-use逐层拆开讲的系列,整体地图见 browser-use 是什么;沿着这条线往下,还可以看 browser-use 的三条回放线browser-use 的两层 skills

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。