客户端 agent
Browser
服务器看不到的那些东西:对一个真实的人来说这个页面到底花了多久、哪些点击什么都没做,以及哪个组件重新渲染了四百次。
- 包
@sixty-sh/browser发布在 npm- 运行环境
- 任何页面。每观测一次请求,阻塞开销大约 2.4 微秒。
- 源码
- sixty-sh/sixty-browser
安装
Next, Remix, SvelteKit and friends. The key stays server-side behind a proxy route.
安装说明是写给你已经开着的那个编码助手的提示词,而不是给你的一份清单。这是有意的:它说的是安装完成之后什么必须成立,而不是要改哪些文件 —— 因为代码该放在哪里取决于框架,而放错地方是会静默失败的。助手可以读你的仓库并把这件事推断出来;文档页上的一个段落做不到。
同样这段文字,就是 install_sixty 通过 MCP 服务器 返回的内容,也是收集端在以下位置提供的内容: /v1/setup?kind=browser. 它只有一份。
Browser 的完整安装说明
Set up @sixty-sh/browser in this project so page performance reports to sixty.
Before editing, ask the user: "Do you want privacy-masked session replay?"
Do not choose for them. If yes, use the replay-enabled init in step 3. If no,
use plain init() and do not add replay configuration.
1. Install @sixty-sh/browser.
2. Add a server-side endpoint at the path /api/drift whose GET and POST
handlers are both the same createSixtyProxy() handler from
"@sixty-sh/browser/proxy". Work out the correct
file and export style for this project's framework and router. It must run
on the server, not the client. POST receives measurements and recordings;
GET reads the Sessions policy. Omitting either method is an incomplete install.
3. Call init() from "@sixty-sh/browser" exactly once in the browser, in a
client component that is mounted on every page (the root layout is usually
right). If the user chose replay, call
init({ replay: { enabled: true } }); otherwise call init(). The default
endpoint is /api/drift.
@sixty-sh/browser owns the compatible recorder dependency, so do not install
or import rrweb separately. Replay remains privacy-masked and the Sessions
setting decides whether recordings are off, incident-only, or sampled.
4. Set these server-side environment variables (never client-exposed ones):
SIXTY_API_KEY = a secret key starting sixty_sk_ — ask me for it, I can
generate one on the Settings page. Do not invent one.
SIXTY_SERVICE = my-app
SIXTY_ENDPOINT = https://ingest.sixty.sh
5. If this project has a router that knows its route patterns, call setRoute()
from "@sixty-sh/browser" with the pattern (e.g. "/orders/[id]") on each
navigation. Without it, routes are guessed from the URL, which is close but
not exact.
Constraints — these are correctness requirements, not style preferences:
- SIXTY_API_KEY must never reach the browser. Do not prefix it with NEXT_PUBLIC_,
VITE_, or PUBLIC_, do not pass it to init(), and do not import it into any
client component. The whole point of the proxy is that the key stays server-side.
- Do not add any analytics, user id, session id, or cookie. The agent is
deliberately anonymous and must stay that way.
- Do not call init() during server rendering. It returns null there, so guard it
in an effect or a client-only component rather than at module scope.
When you are done, tell me which files you changed and how to deploy so I can
confirm data is arriving.它需要一个私密密钥 —— 以 sixty_sk_ 开头,并且只留在服务端。登录之后可以在设置页生成一个。
它测量什么
| 信号 | 单位 | 含义 |
|---|---|---|
web_vital | ms | a page-speed metric got worse for real users |
render_storm | renders | a component re-renders many times for one interaction |
dead_interaction | of clicks | users click this and nothing observable happens |
stuck_loading | of loads never finish | a loading state is entered and never left |
client_error | of views | this error is being thrown in real users’ browsers |
auth_failures | — | The server is turning these away on permission grounds rather than failing. People see an empty page or a save that quietly does nothing. |
silent_empty | — | The query runs and succeeds, and returns no rows where it used to return plenty. Nothing reports an error, so the page just renders blank — this is what a broken permission rule looks like from the outside. |
latency | ms per call | this operation takes longer end to end than it used to |
errors | error rate | a larger fraction of calls are throwing |
runaway | calls per minute | this operation is being called far more often than anything triggers it |
它接在哪里
- Next、Remix、SvelteKit — 有自己的服务器,就意味着密钥留在服务端、藏在一条代理路由后面 —— 选这一档,而不是 Lovable 那一档。
- 完全没有服务器 — Lovable、静态托管、只有 Supabase 的应用:看 Lovable 那一档,它用的是一个绑定了来源的公开密钥。
只有它才做的事
- 按路由和按国家的页面速度 — 来自真实访问的 LCP、INP、CLS 和 TTFB,与上一个版本比较,而不是和一次实验室跑分比较。
- 渲染循环 — 一次交互把页面重新渲染几百次。服务端什么都看不到,也不会抛出任何错误。
- 什么都不做的点击 — 一个被点击、却没有产生任何可测量变化的控件 —— 没有请求、没有跳转、没有 DOM 变化。
- 永远加载不完 — 进入了却再也没有离开的加载状态 —— 那种会产生工单、却不留下任何一行日志的故障。
它做不到什么
- 不收集任何形式的用户身份 —— 没有 id、没有会话、没有 cookie。这是设计约束而不是一个开关,所以「是哪个用户看到了这个」是这个产品对任何人都答不出来的问题,包括我们自己。
- 生产环境下的调用栈指向打包后的 chunk。识别一个浏览器错误靠的是操作名和报错信息,而不是行号。
配置
每个 agent 都读同样四个变量,而且凡是 SIXTY_* 能用的地方 DRIFT_* 依然有效 —— 产品改过名,但那个名字不是我们说撤就能从别人的部署里撤掉的。
SIXTY_API_KEY | 没有它,agent 就保持沉默不动,并且会说出来。它从不猜测,从不对着一个未知端点重试,也从不抛异常。 |
|---|---|
SIXTY_SERVICE | 这个服务叫什么。在能读出项目名的地方,默认用项目名。 |
SIXTY_RELEASE | 最重要的一个。在 Vercel、Render、Railway、Fly、Heroku 和 GitHub Actions 上会自动取到;其他地方请把它设成 commit 的 SHA。没有它,所有测量都会落进同一个没有名字的桶里,任何比较都无从谈起。 |
SIXTY_ENDPOINT | 往哪里上报。默认是 http://localhost:4319,这在笔记本上是对的,而在应用被交付给别人的那一刻就是错的。 |
其余的 —— 发送间隔、采样率、要给什么埋点 —— 都在这个包自己的 README 里,因为那里才是它能随着 agent 变化而保持正确的地方。