服务端 agent
Node
唯一一个不用你开口就会给你自己的函数埋点的 agent:构建期转换为每一个导出的异步函数开一个 span,于是每条查询都有一个可以归属的调用者。
- 包
@sixty-sh/node发布在 npm- 运行环境
- Node 20 及以上,你部署的任何服务端进程。
- 源码
- sixty-sh/sixty-node
安装
A server process you control: functions, HTTP routes and SQL.
安装说明是写给你已经开着的那个编码助手的提示词,而不是给你的一份清单。这是有意的:它说的是安装完成之后什么必须成立,而不是要改哪些文件 —— 因为代码该放在哪里取决于框架,而放错地方是会静默失败的。助手可以读你的仓库并把这件事推断出来;文档页上的一个段落做不到。
同样这段文字,就是 install_sixty 通过 MCP 服务器 返回的内容,也是收集端在以下位置提供的内容: /v1/setup?kind=node. 它只有一份。
Node 的完整安装说明
Install the @sixty-sh/node agent in this service so its functions, HTTP routes
and database queries report to sixty.
Before editing, inspect whether this service makes LLM or agent calls. If it
does, ask the user: "Do you want LLM monitoring?" Do not enable it without
their answer. Core HTTP, function and database monitoring is installed either
way. If they say yes, also follow the AI wiring in step 3; if no, leave every
AI client and call site untouched.
1. Install @sixty-sh/node as a dependency (not a devDependency — it runs in
production; that is the entire point).
2. Call init() from "@sixty-sh/node" in the server entry point, BEFORE the
application imports anything else. It patches the database client at call
time, so a module that already imported "pg", "mysql2", "postgres" or
"mongodb" above it is not instrumented. In an ESM entry, imports are
hoisted above statements — so either put the init in its own module
imported first, or use --import as in step 4.
Then check which database client this project actually uses. pg, mysql2,
postgres.js and mongodb are found and patched by init() with nothing
further to do — including Mongoose, which drives the mongodb driver.
PRISMA IS THE EXCEPTION and it is the one worth checking for, because
getting it wrong is invisible. Prisma does not use pg — it runs queries
through its own engine, so no patch reaches them — and its extension hook
returns a NEW client instead of modifying the one it was given. So the
application has to use the wrapped client:
import { instrumentPrisma } from "@sixty-sh/node"
export const prisma = instrumentPrisma(new PrismaClient())
Replace the project's existing PrismaClient export with that one. Skip this
and everything still reports except the queries, which is the failure that
looks like success.
3. If LLM monitoring was requested, instrument only the SDKs the project
actually uses: instrumentOpenAI(client), instrumentAnthropic(client),
instrumentAISDK({ generateText, streamText }), or langChainCallbacks().
For other providers and OpenAI-compatible services such as DeepSeek, wrap
the call with generation({ provider, model }, () => call()). Never attach
prompts, generated text, tool arguments or tool results; sixty records
timing, provider/model, token usage and cost metadata only.
4. Turn on function-level tracing. Work out which of these this project can
use; do not do both:
a. It has a build step (Next, Vite, webpack, Rollup, esbuild). Wrap the
config with withSixty() from "@sixty-sh/node/transform", or add the
matching bundler plugin from that same module. The transform needs
@babel/core and unplugin, which are peer dependencies — without them it
emits nothing and says nothing, so add them if they are not there.
b. It has no build step — it is started with a plain "node src/server.js"
or "tsx src/server.ts". Add --import @sixty-sh/node/register to the
start command, which both installs the transform and calls init() early
enough that step 2 is already satisfied.
NEXT.JS, SPECIFICALLY. Use (a) for the transform, and start init() from a
flag rather than from instrumentation.js:
// sixty.mjs, beside package.json
import { init } from "@sixty-sh/node"
init()
# the deployed start command
node --import ./sixty.mjs node_modules/next/dist/bin/next start
instrumentation.js looks like the right hook and is compiled once per
runtime. If the app has middleware there is an edge compilation of it, and
the bundler pulls the agent's node:fs, node:http and node:crypto into a
bundle that cannot read those schemes — the build fails. A NEXT_RUNTIME
guard inside register() does not help: it runs at runtime, and the
bundling already happened.
Change the command the deployed process actually runs, not only the local
dev script. A Dockerfile CMD, a Procfile, or a platform start command
overrides package.json and is the one that matters.
5. Set these environment variables wherever the service is deployed:
SIXTY_API_KEY = a secret key starting sixty_sk_ — ask me for it. Do not
invent one, and do not commit it.
SIXTY_SERVICE = my-app
SIXTY_ENDPOINT = https://ingest.sixty.sh
The release identifier is picked up automatically on Vercel, Render,
Railway, Fly, Heroku and GitHub Actions. If this deploys some other way,
set SIXTY_RELEASE to the commit SHA — without one, every measurement lands
in a single nameless bucket and no comparison can ever be made.
Constraints — correctness requirements, not style preferences:
- Do NOT change any application behaviour. This is instrumentation only: no
refactors, no reordering of business logic, no "while I was in here" fixes.
- Do NOT apply the transform to client bundles. It is for server code; in a
framework that builds both, restrict it to the server build.
- Do NOT add any analytics library, user id, session id, or cookie to what is
reported. The agent is deliberately anonymous and must stay that way.
- If a hot function must not be traced, put // @sixty-ignore above it rather
than turning the transform off for the whole file.
When you are done, tell me which files you changed and what the deployed start
command now is, so I can confirm data is arriving.它需要一个私密密钥 —— 以 sixty_sk_ 开头,并且只留在服务端。登录之后可以在设置页生成一个。
它测量什么
| 信号 | 单位 | 含义 |
|---|---|---|
rows | rows per call | this query returns more rows than it used to |
fanout | queries per call | this operation now issues more database calls per invocation — an N+1 |
latency | ms per call | this operation takes longer end to end than it used to |
self_latency | ms per call | the time spent in this function itself got longer — its children did not |
payload | bytes per call | the serialized result of this operation got bigger |
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 |
repeated_query | times per request | the identical query runs several times within one request |
overfetch | rows per call | far more rows are fetched than the code appears to use |
unbounded | rows per call | this query has no upper bound on what it can return |
recursion | levels deep | this operation calls itself, deeper than it should |
new_error | occurrences | an error that did not occur in the previous release |
missing_tenancy | — | This reads a table of per-person data without saying whose rows it wants. Unless your database is filtering it for you, everyone gets everyone else's. |
collapse | — | This is handing back roughly half the data it used to, or less. If that was not deliberate, something is filtering out rows that somebody expects to see. |
vanished | — | It was being used steadily until this release and has not been used once since. Usually the link, button, or redirect that led here stopped working. |
traffic_drop | — | This is still being used, but a fraction as often, and its share of your traffic fell too — so it is not just a quiet period. |
round_trips | round trips per read | one read now waits on the database many times instead of once |
plan | — | the database chose a different plan for this query |
它接在哪里
- 任何 node:http 服务器 — Express、Fastify、Koa、Hono、Next.js 的 route handler —— 打补丁的是服务器模块而不是框架,所以什么都不需要适配层。
- 你自己的函数 — 构建期转换会包住导出的异步函数。正是这一点把「接口变慢了」变成「这个函数开始发十四条查询了」。
数据库
- pg — 行数、语句形状,以及通过 EXPLAIN 得到的查询计划。
- mysql2 — 行数和受影响行数,query 和 execute 都算,包括流式结果。
- postgres.js — 标签模板从不把值放进语句里,所以没有什么需要脱敏。
- mongodb — 命令形状作为 identity、文档作为行,以及游标往返次数。
- Prisma — 显式调用:instrumentPrisma(new PrismaClient())。
只有它才做的事
- 自动的函数 span — 不用装饰器、不用 include,也不用在函数开头加两行。其他每一个服务端 agent 都要求你自己标出值得测量的代码。
- Postgres 上的查询计划 — 一次通用计划的 EXPLAIN,按语句缓存,并在调用方的 span 之外执行,所以它永远不会被算作调用方的工作。
- 游标往返次数 — 分批送达的 MongoDB 读取 —— 一个其他驱动都不暴露的信号。
它做不到什么
- 并发的 span 靠 AsyncLocalStorage 区分,而连接池默认会破坏它。适配层会把连接池的回调重新绑回创建它们的上下文;一个我们没有埋点的驱动,会把查询算到那个释放了连接的人头上。
- CPU 和等待没法分开。process.cpuUsage() 是整个进程的,而许多异步上下文在一个线程上交错运行,所以没有哪个 span 能诚实地声称其中一部分属于自己。
配置
每个 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 变化而保持正确的地方。