sixty

客户端 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_vitalmsa page-speed metric got worse for real users
render_stormrendersa component re-renders many times for one interaction
dead_interactionof clicksusers click this and nothing observable happens
stuck_loadingof loads never finisha loading state is entered and never left
client_errorof viewsthis 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.
latencyms per callthis operation takes longer end to end than it used to
errorserror ratea larger fraction of calls are throwing
runawaycalls per minutethis 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 变化而保持正确的地方。

sixty 的 Browser agent —— 它测量什么、怎么安装