sixty

server agent

Node

The only agent that instruments your own functions without being asked: a build transform opens a span per exported async function, so a query has a caller to be credited to.

package
@sixty-sh/node on npm
runs on
Node 20 or later, any server process you deploy.
source
packages/node

Installing it

A server process you control: functions, HTTP routes and SQL.

The install is written as a prompt for the coding agent you already have open, not as a checklist for you. That is deliberate: it names what must be true when the install is finished rather than which files to edit, because where the code goes depends on the framework and putting it in the wrong place fails silently. An agent can read your repository and work that out; a paragraph on a documentation page cannot.

The same text is what install_sixty returns through the MCP server and what the collector serves at /v1/setup?kind=node. There is one copy of it.

the Node install, in full
Install the @sixty-sh/node agent in this service so its functions, HTTP routes
and database queries report to sixty.

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 3.

   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. 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.

   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.

   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.

4. 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.

It needs a secret key — it starts sixty_sk_ and stays server-side. Mint one on the Settings page once you have signed in.

What it measures

signalunitwhat it means
rowsrows per callthis query returns more rows than it used to
fanoutqueries per callthis operation now issues more database calls per invocation — an N+1
latencyms per callthis operation takes longer end to end than it used to
self_latencyms per callthe time spent in this function itself got longer — its children did not
payloadbytes per callthe serialized result of this operation got bigger
errorserror ratea larger fraction of calls are throwing
runawaycalls per minutethis operation is being called far more often than anything triggers it
repeated_querytimes per requestthe identical query runs several times within one request
overfetchrows per callfar more rows are fetched than the code appears to use
unboundedrows per callthis query has no upper bound on what it can return
recursionlevels deepthis operation calls itself, deeper than it should
new_erroroccurrencesan error that did not occur in the previous release
missing_tenancyThis 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.
collapseThis 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.
vanishedIt 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_dropThis 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_tripsround trips per readone read now waits on the database many times instead of once
planthe database chose a different plan for this query

Where it hooks in

  • Any node:http serverExpress, Fastify, Koa, Hono, Next.js route handlers — the server module is patched, not the framework, so nothing needs an adapter.
  • Your own functionsA build transform wraps exported async functions. This is what turns "the endpoint got slow" into "this function started issuing fourteen queries".

Databases

  • pgRows, statement shape, and query plans via EXPLAIN.
  • mysql2Rows and affected rows, both query and execute, streamed results included.
  • postgres.jsTagged templates never put values in the statement, so there is nothing to redact.
  • mongodbCommand shape as identity, documents as rows, and cursor round trips.
  • PrismaExplicit: instrumentPrisma(new PrismaClient()).

What only this one does

  • Automatic function spansNo decorator, no include, no two lines at the top of the function. Every other server agent asks you to mark the code worth measuring.
  • Query plans on PostgresA generic-plan EXPLAIN, cached per statement, run outside the caller's span so it is never measured as their work.
  • Cursor round tripsMongoDB reads that arrive in instalments — a signal no other driver exposes.

What it cannot do

  • Concurrent spans are told apart by AsyncLocalStorage, which the connection pool defeats by default. The adapters bind pool callbacks back to the context that created them; a driver we do not instrument will attribute its queries to whoever released a connection.
  • CPU and waiting cannot be split. process.cpuUsage() is process-wide and many async contexts interleave on one thread, so no span can honestly claim a slice of it.

Configuration

Every agent reads the same four variables, and DRIFT_* still answers everywhere SIXTY_* does — the product was renamed and that name is not ours to retire from other people’s deployments.

SIXTY_API_KEYWithout it the agent stays inert and says so. It never guesses, never retries against an unknown endpoint, and never throws.
SIXTY_SERVICEWhat to call this service. Defaults to the project name where one is legible.
SIXTY_RELEASEThe one that matters most. Picked up automatically on Vercel, Render, Railway, Fly, Heroku and GitHub Actions; set it to the commit SHA anywhere else. Without it every measurement lands in a single nameless bucket and no comparison can ever be made.
SIXTY_ENDPOINTWhere to report. Defaults to http://localhost:4319, which is right on a laptop and wrong the moment the app is served to anyone else.

The rest — flush interval, sample rate, what to instrument — is in the package’s own README, which is where it can stay true as the agent changes.

The sixty Node agent — what it measures and how to install it