client agent
Browser
What the server cannot see: how long the page actually took for a real person, which clicks did nothing, and which component re-rendered four hundred times.
- package
@sixty-sh/browseron npm- runs on
- Any page. Roughly 2.4µs of blocking cost per request it observes.
- source
- packages/browser
Installing it
Next, Remix, SvelteKit and friends. The key stays server-side behind a proxy route.
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=browser. There is one copy of it.
the Browser install, in full
Set up @sixty-sh/browser in this project so page performance reports to sixty.
1. Install @sixty-sh/browser.
2. Add a server-side POST endpoint at the path /api/drift that is handled by
createSixtyProxy() 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.
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). It takes no arguments — the default endpoint is /api/drift.
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.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
| signal | unit | what it means |
|---|---|---|
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 |
Where it hooks in
- Next, Remix, SvelteKit — A server of your own means the key stays server-side behind a proxy route — take this tier, not the Lovable one.
- No server at all — Lovable, a static host, a Supabase-only app: see the Lovable tier, which uses a public, origin-pinned key.
What only this one does
- Page speed per route and per country — LCP, INP, CLS and TTFB from real visits, compared against the last release rather than against a lab run.
- Render loops — One interaction that re-renders the page hundreds of times. Nothing server-side can see this and no error is thrown.
- Clicks that do nothing — A control that is clicked and produces no measurable change — no request, no navigation, no DOM mutation.
- Never-finished loading — A loading state entered and never left, which is the failure that produces a support ticket and no log line.
What it cannot do
- No user identity of any kind is collected — no id, no session, no cookie. That is a design constraint rather than a setting, so "which user saw this" is a question this product cannot answer for anybody, including us.
- Stack frames in production point at bundled chunks. The operation name and the message are what identify a browser error, not the line number.
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_KEY | Without it the agent stays inert and says so. It never guesses, never retries against an unknown endpoint, and never throws. |
|---|---|
SIXTY_SERVICE | What to call this service. Defaults to the project name where one is legible. |
SIXTY_RELEASE | The 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_ENDPOINT | Where 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.