for Ruby and Rails apps
There is no step two.
Rails has a documented boot hook and an event bus that already announces every query, so this agent needs neither an initializer nor a wrapper: add the gem, set two environment variables, and controllers become operations with their queries attributed to them. Everything below happens without you writing a line of instrumentation.
What it measures
- Every controller action
- Named by controller#action, so the feed reads the way your routes file does. The railtie wraps them on boot — there is nothing to include and nothing to remember on the next controller.
- Every query ActiveRecord issues
- Subscribed through sql.active_record, which is the bus Rails already publishes on. Rows, the normalized statement, the response size, and which action issued it.
- Three databases, by their own rules
- pg with query plans via EXPLAIN; mysql2 and trilogy read by MySQL's quoting rules rather than Postgres's, because a double-quoted string is a literal in one and an identifier in the other; and mongo, which is what Mongoid uses, with the command shape as identity.
- What the database did to answer
- Postgres is asked for the plan of each statement — a generic-plan EXPLAIN, cached per statement, issued outside the caller's span so it is never measured as their work, and without ever binding a parameter, so no value of yours is involved.
- Your own classes, when you want them
- include Sixty::Instrumented turns every public instance method into an operation and leaves private methods and accessors alone. Or Sixty.instrument(Stripe::Charge, :create) for one method on somebody else's class.
What it finds
Every one of these is compared against the release before it, so the finding names the change rather than the day.
A query that stopped using its index
The one finding here that names a cause instead of a symptom, and it often arrives before anything is slow at all: the table is still small enough that a scan is fine, and it becomes an outage weeks later when it is not.
An N+1 that was not there last release
The defect Rails is most often accused of, caught by count rather than by clock: one query per action became twenty, every one of them fast, and includes was dropped in a refactor three weeks ago.
A query that started reading the whole table
Rows per call went from 30 to 30,000 — a scope whose where moved into Ruby, which returns the same answer and reads perfectly sensibly.
Your own code getting slower
Time in the method itself rather than in what it called, so "did my code get slower or did the thing I called" is one number instead of an afternoon.
A query that quietly returns nothing
Succeeded, raised nothing, and came back empty where it used to come back with rows. ActiveRecord is perfectly happy; the page is blank.
Something that is now called in a loop
Unchanged per call and called hundreds of times where it used to be called once — including through a background job, which reports as its own process.
What lands in your feed
Not a dashboard to read. One card per finding, with the numbers, the release that changed them, and the evidence your coding agent needs to write the fix.
These are the two regressions apps/demo-rails ships on purpose, against the dataset bin/rails db:prepare seeds — about 30,000 orders. You can run it and watch them arrive.
OrdersQuery#for_userSomebody refactored the scope and the where moved into Ruby, so every call loads the whole table and filters in memory. It returns the same answer, and on a laptop it costs a few milliseconds.
since release v2, against v1
OrdersQuery#enrichA batched lookup became a per-order one. Each query is individually fast and correctly indexed, which is what lets this class of bug survive code review and staging.
since release v2, against v1
How it installs
On Rails this is the whole install: the gem, and two environment variables. The railtie adds the middleware, subscribes to the query bus and wraps controller actions on boot, so there is no initializer to write and nothing to call.
claude mcp add sixty \ -e SIXTY_API_KEY=sixty_sk_… \ -e SIXTY_ENDPOINT=https://ingest.sixty.sh \ -- npx -y @sixty-sh/mcp # or by hand: gem 'sixty' # Gemfile export SIXTY_API_KEY=sixty_sk_… export SIXTY_SERVICE=checkout-api # On Rails, that is the install. Outside it: require 'sixty' Sixty.init use Sixty::Instrument::Rack # config.ru — Sinatra, Hanami, a bare app # Your own classes, if you want them measured: class OrdersQuery include Sixty::Instrumented end
The release identifier is picked up on its own from Vercel, Render, Railway, Fly, Heroku or GitHub Actions, and SIXTY_RELEASE sets it anywhere else. The first comparison arrives after your next deploy, because there is nothing to compare one release against.
What it does not do
- Query plans are Postgres only. MySQL has no generic-plan EXPLAIN and MongoDB has no statement to explain — and explaining either would mean retaining somebody's query values in order to send them back to their own database in a command we composed, which this agent will not do.
- CPU and waiting are not split. That needs a per-thread clock and a span that owns its thread, which only the Python agent has.
- Under Puma with workers, or Sidekiq, each process reports independently and the collector merges them. That is correct, and worth knowing when you read a per-process number.
- include Sixty::Instrumented measures public instance methods. A private method that does the real work is invisible until you make it an operation some other way, which is the trade for not guessing at what is worth measuring.
If one of these is why you are here
Find out what your last change did.
Try it!Building something else? See Node services, PHP and Laravel apps or Python services — those pages are different, and so is what we can see.