客户端 agent
Supabase
唯一一个知道这个端点「是什么」而不只是「花了多少」的 agent —— 所以它能把一条慢查询和一条拒绝了查询的策略区分开。
- 包
@sixty-sh/supabase发布在 npm- 运行环境
- 和浏览器 agent 一起用,用在后端是 Supabase 的项目里。
- 源码
- sixty-sh/sixty-supabase
安装
For a frontend with no server of your own to deploy: runs entirely in the browser with a public, origin-pinned key. If the project has any server, use browser or node.
安装说明是写给你已经开着的那个编码助手的提示词,而不是给你的一份清单。这是有意的:它说的是安装完成之后什么必须成立,而不是要改哪些文件 —— 因为代码该放在哪里取决于框架,而放错地方是会静默失败的。助手可以读你的仓库并把这件事推断出来;文档页上的一个段落做不到。
同样这段文字,就是 install_sixty 通过 MCP 服务器 返回的内容,也是收集端在以下位置提供的内容: /v1/setup?kind=lovable. 它只有一份。
Supabase 的完整安装说明
Add sixty.sh performance monitoring to this app. It measures Supabase query
shape, page speed, stuck loading states and browser errors, and reports
anonymously.
Before anything else: does this project run a server of its own? A Lovable
app often grows one — an API route, a serverless function, an Express or
FastAPI backend added later, a Supabase edge function you deploy. If it has
one, STOP and ask for the "browser" install instead, or the "node" or
"python" one if the server holds logic worth measuring. Those keep the key
server-side behind a proxy and can measure the server as well as the page.
This install is for the case where the frontend is genuinely the only code
you deploy, and it trades a world-readable key for that.
If that is this project, work out which shape it is, because it decides where
the code goes and there is no error if you get it wrong — it just silently
never runs:
A. Client-only Vite SPA. There is a createRoot(...) call in a file like
src/main.tsx and no server entry.
B. Server-rendered framework app — React Router v7 framework mode, Remix,
or similar. Signs: an entry.client.tsx / entry.server.tsx pair, a
root.tsx, a hydrateRoot(...) call, or a routes.ts.
Then:
1. Add two dependencies: @sixty-sh/supabase and @sixty-sh/browser.
2. Call init() from "@sixty-sh/browser" exactly once, in the browser.
init({ key: "sixty_pk_YOUR_KEY" })
In shape A, module scope in the app entry is fine.
In shape B it MUST go in the client entry (entry.client.tsx), after
hydration. Do not put it in a shared module that the server also
evaluates: there it runs during server rendering, finds no window,
returns null, and reports nothing at all — with no error to tell you.
3. Find where the Supabase client is created. Search the repository for
"createClient(" rather than guessing a path — it is often in
src/integrations/supabase/client.ts but in this kind of project it may be
anywhere. Then, at module scope in that same file:
import { withSixty } from "@sixty-sh/supabase"
withSixty(supabase, { key: "sixty_pk_YOUR_KEY", service: "my-app" })
Use the client that already exists. withSixty patches the PostgREST
prototype and returns the same object it was given, so it instruments every
query in the app without replacing the export.
Constraints — correctness requirements, not style preferences:
- Do NOT create a second Supabase client. There must still be exactly one.
- Do NOT edit vite.config.ts and do NOT add a Vite plugin.
- Do NOT change any existing query, component, or hook. This is
instrumentation only; no behaviour should change.
- The key above is PUBLIC by design: write-only, pinned to one service, and
refused from any origin it does not list. Paste it literally. Do not move it
into an environment variable or a secret.
- Do NOT add any analytics library, user id, session id, or cookie. This agent
is deliberately anonymous and must stay that way.
When you are done, tell me which shape you found and which files you changed,
then publish the app. Findings need two published releases with traffic under
each before anything can be compared.它需要一个公开密钥 —— 绑定到你的来源,因为一个没有自己服务器的应用没有地方存放机密。
它测量什么
| 信号 | 单位 | 含义 |
|---|---|---|
errors | error rate | a larger fraction of calls are throwing |
new_error | occurrences | an error that did not occur in the previous release |
latency | ms per call | this operation takes longer end to end than it used to |
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. |
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. |
它能叫出名字的失败
这些不是信号。这些是它给一个错误打上的分类,而正是它们造成了「一次调用失败了」和「一条策略拒绝了它」之间的差别。
rls_denied | A row-level security policy refused the statement (Postgres 42501). It reaches the browser as an empty list and your logs as nothing at all. |
|---|---|
schema_missing | A column, table, relationship or function the code expects is not in the database. |
constraint_violated | A write was rejected by a database constraint. |
jwt_expired | A session token was expired or invalid where one was required. |
realtime_duplicate_subscription | One topic subscribed concurrently three times or more — an effect with no teardown, seen from the wire. |
realtime_channel_error | A channel reported CHANNEL_ERROR or TIMED_OUT instead of subscribing. |
它接在哪里
- supabase-js — PostgREST 调用、实时通道和认证,在它们发生的地方就地埋点。
- Vite — 一个插件,给用 Vite 构建的项目。
数据库
- PostgREST — 请求本身就描述了这次查询,所以表、过滤条件和失败码不用看 SQL 语句也能读出来。
只有它才做的事
- 给失败一个名字 — 别的 agent 都能说某次调用失败了。这一个能说是某条策略拒绝了它、是某个列不存在,或者是令牌过期了 —— 因为一个 PostgREST 请求描述的是查询本身,而不只是消耗了一点什么。
- 除了读取,还有实时 — 出错、超时,或者因为某个副作用没有清理而被订阅了三遍的通道。
它做不到什么
- 这是对浏览器 agent 或 Lovable agent 的补充,不是替代。它解释 Supabase 的失败;它不测量页面。
- 一个没有自己服务器的项目用的是绑定来源的公开密钥。如果它后来长出了 API 路由或者 edge function,那就改用浏览器或 Node 那一档 —— 密钥会留在服务端,而且服务器也会一并被测量。
配置
每个 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 变化而保持正确的地方。