0.0
No release in over 3 years
RailsVitals is a lightweight Rails engine gem that makes the hidden runtime behavior of a Rails application visible, measurable, and teachable. It provides insights into the inner workings of a Rails app, helping developers understand and optimize their code. With RailsVitals, you can easily identify performance bottlenecks, track database queries, and gain a deeper understanding of how your application operates under the hood.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 7.0
 Project Readme

⚑ RailsVitals

The Rails gem that made me understand performance.

RailsVitals is a zero-dependency Rails Engine that instruments every request in your app and surfaces performance diagnostics: N+1 queries, slow SQL, fat callbacks, and health scores through an embedded admin UI and an injectable panel overlay.

It doesn't just tell you something is wrong. It shows you why, where, and what to do about it.


Screenshots

RailsVitals Dashboard

Full admin UI at /rails_vitals with Dashboard, Request History, Request Detail with Query DNA, Endpoint Heatmap, Per-Model Breakdown, N+1 Patterns and Association Map.


Features

πŸ”΄ Per-Request Health Score

Every request gets a score from 0–100 based on query volume and N+1 severity. Scores are color-coded: Healthy (90–100), Acceptable (70–89), Warning (50–69), Critical (0–49).

Health Score

🧬 Query DNA β€” Visual SQL Fingerprinting

Every query in a request is decomposed into color-coded tokens: SELECT *, WHERE fk =, IN (...), JOIN, ORDER BY, OFFSET, and more. Click any query to expand its DNA. Click any token to read an education card explaining what it means, why it matters, and how to fix it.

Query DNA

πŸ—ΊοΈ Association Map

A live SVG diagram of your ActiveRecord model graph, annotated with N+1 status, query counts, average query time, foreign key names, and index status on every edge. Nodes light up red when N+1 patterns are detected. Dashed edges signal missing indexes on foreign keys. Click any node to open a detail panel with fix suggestions and links to affected requests.

Association Map

πŸ”₯ Endpoint Heatmap

A ranked table of every endpoint in your app sorted by worst average health score. Columns: average score, hit count, average query count, average DB time, average callback time, N+1 frequency. The worst offenders surface immediately.

Endpoint Heatmap

πŸ“Š Per-Model Breakdown

Which ActiveRecord models are hammering your database? The model breakdown aggregates queries by table, shows total query count, total DB time, average query time, and the endpoints responsible.

Model Breakdown

πŸ” N+1 Pattern Detector

Cross-request N+1 aggregation using normalized SQL fingerprinting. Each pattern shows total occurrences, affected endpoints, and a concrete fix suggestion generated by reflecting on your actual ActiveRecord associations e.g. Post.includes(:likes), not a generic hint.

N+1 Pattern Detector

πŸ’₯ Impact Simulator

Each N+1 pattern has a detail page showing affected requests, estimated query savings, and a generated migration-ready fix. See the blast radius before you write a line of code.

Impact Simulator

🎭 Callback Map

Every ActiveRecord callback (before_save, after_create, before_validation, etc.) is timed and grouped by model in the Request Detail view. Expensive callbacks surface immediately β€” including hidden side effects like callbacks that trigger additional queries.

Callback Map

πŸ›‘οΈ Injected Panel

A collapsible overlay injected into every HTML response showing the current request's score, query count, DB time, N+1 count, and callback time. Zero configuration. Disappears in production.

Injected Panel


Installation

Add to your Gemfile:

gem "rails_vitals", group: :development

Run:

bundle install

Mount the engine in config/routes.rb:

mount RailsVitals::Engine, at: "/rails_vitals"

That's it. Visit /rails_vitals and start browsing your app.


Configuration

RailsVitals works out of the box with sensible defaults. To customize, add an initializer:

# config/initializers/rails_vitals.rb
RailsVitals.configure do |config|
  # Enable/disable instrumentation entirely
  config.enabled = !Rails.env.production?

  # Ring buffer size β€” how many requests to keep in memory
  config.store_size = 200

  # Query count thresholds for scoring
  config.query_warn_threshold     = 10   # above this β†’ score starts dropping
  config.query_critical_threshold = 25   # above this β†’ score = 0

  # DB time thresholds (ms) for slow query detection
  config.db_time_warn_ms     = 100
  config.db_time_critical_ms = 500

  # Admin UI authentication
  # :none      β†’ no auth (default for development)
  # :basic     β†’ HTTP Basic Auth
  # lambda     β†’ custom auth logic
  config.auth = :none

  # Basic auth credentials (if auth: :basic)
  config.basic_auth_username = "admin"
  config.basic_auth_password = "secret"

  # Custom auth lambda (if auth: :lambda)
  # config.auth = ->(request) { request.session[:admin] == true }
end

Admin UI

Navigate to /rails_vitals to access the full admin interface.

Page Path Description
Dashboard /rails_vitals Score distribution, health trend, query volume
Requests /rails_vitals/requests Full request history with filters
Request Detail /rails_vitals/requests/:id Queries, Query DNA, Callback Map, Score Projection
Heatmap /rails_vitals/heatmap Endpoints ranked by worst health score
Models /rails_vitals/models Per-model query breakdown
N+1 Patterns /rails_vitals/n_plus_ones Cross-request N+1 aggregation with fix suggestions
Association Map /rails_vitals/associations Live SVG model graph with N+1 and index annotations

How Scoring Works

RailsVitals scores each request using a CompositeScorer with two weighted dimensions:

Score = (QueryScore Γ— 40%) + (N+1Score Γ— 60%)

QueryScore (40%) β€” penalizes query volume:

  • ≀ query_warn_threshold queries β†’ 100
  • β‰₯ query_critical_threshold queries β†’ 0
  • Between thresholds β†’ linear interpolation

N+1Score (60%) β€” penalizes detected patterns:

  • 0 patterns β†’ 100
  • 1 pattern β†’ 75
  • 2 patterns β†’ 50
  • 3 patterns β†’ 25
  • 4+ patterns β†’ 0

N+1 patterns are weighted more heavily because they represent an architectural problem that grows worse as data scales not just a snapshot of current query volume.


Query DNA Token Reference

Token Color Risk What it means
SELECT * Blue ⚠ Warning Fetches all columns β€” consider .select(:id, :name)
SELECT Blue βœ… Healthy Specific column selection
COUNT(*) Amber ⚠ Warning Aggregation in a loop = N+1 variant
AGGREGATE Amber ⚠ Warning SUM/AVG/MIN/MAX in a loop
FROM Green βœ… Healthy Identifies the model being queried
WHERE fk = Red πŸ”΄ Danger Single FK lookup β€” the N+1 signature
WHERE Orange β€” Neutral Filter condition β€” check for missing index
IN (...) Green βœ… Healthy Batch lookup β€” eager loading is working
INNER JOIN Purple β€” Neutral .joins() β€” note: association not loaded
LEFT JOIN Purple β€” Neutral .eager_load() or .left_joins()
ORDER BY Cyan ⚠ Warning Ensure sort column has an index
LIMIT Gray βœ… Healthy Good β€” always paginate
OFFSET Red ⚠ Warning O(n) at scale β€” consider cursor pagination
GROUP BY Cyan β€” Neutral Consider counter cache for frequent use

N+1 Fix Suggestions

RailsVitals generates fix suggestions by reflecting on your actual ActiveRecord associations, not by guessing. Given a detected pattern:

SELECT "likes".* FROM "likes" WHERE "likes"."post_id" = ?

It extracts the foreign key (post_id), infers the owner model (Post), reflects on Post.reflect_on_all_associations, and generates:

Post.includes(:likes)

Real associations, real fix, zero guesswork.


Association Map β€” Reading the Diagram

  • 🟒 Green node β€” model is queried, no N+1 detected
  • πŸ”΄ Red node β€” N+1 patterns detected on this model's associations
  • ⬜ Gray node β€” model exists but hasn't been queried in recent requests
  • Solid edge β€” foreign key is indexed βœ…
  • Dashed edge β€” foreign key is missing an index ⚠️
  • Orange edge β€” has_many / has_one
  • Purple edge β€” belongs_to
  • Red edge β€” association has active N+1 patterns

Click any node to open the detail panel with query stats, association breakdown, fix suggestions, and links to affected requests.


Architecture

RailsVitals is a mountable Rails Engine with zero runtime dependencies beyond Rails itself.

rails_vitals/
β”œβ”€β”€ lib/
β”‚   └── rails_vitals/
β”‚       β”œβ”€β”€ engine.rb                          # Mountable engine
β”‚       β”œβ”€β”€ configuration.rb                   # Config object
β”‚       β”œβ”€β”€ collector.rb                       # Thread-local request collector
β”‚       β”œβ”€β”€ store.rb                           # Thread-safe in-memory ring buffer
β”‚       β”œβ”€β”€ request_record.rb                  # Immutable request snapshot
β”‚       β”œβ”€β”€ panel_renderer.rb                  # Injected HTML panel
β”‚       β”œβ”€β”€ sse_writer.rb                      # Server-Sent Events writer
β”‚       β”œβ”€β”€ notifications/
β”‚       β”‚   └── subscriber.rb                  # AS::Notifications SQL + controller hooks
β”‚       β”œβ”€β”€ instrumentation/
β”‚       β”‚   └── callback_instrumentation.rb    # Module prepend for AR callbacks
β”‚       β”œβ”€β”€ analyzers/
β”‚       β”‚   β”œβ”€β”€ sql_tokenizer.rb               # Query DNA tokenizer
β”‚       β”‚   β”œβ”€β”€ n_plus_one_aggregator.rb       # Cross-request N+1 aggregation
β”‚       β”‚   └── association_mapper.rb          # AR reflection + SVG layout
β”‚       β”œβ”€β”€ scorers/
β”‚       β”‚   β”œβ”€β”€ base_scorer.rb
β”‚       β”‚   β”œβ”€β”€ query_scorer.rb                # 40% weight
β”‚       β”‚   β”œβ”€β”€ n_plus_one_scorer.rb           # 60% weight
β”‚       β”‚   └── composite_scorer.rb
β”‚       └── middleware/
β”‚           └── panel_injector.rb              # Rack middleware for panel injection
└── app/
    β”œβ”€β”€ controllers/rails_vitals/
    β”‚   β”œβ”€β”€ dashboard_controller.rb
    β”‚   β”œβ”€β”€ requests_controller.rb
    β”‚   β”œβ”€β”€ heatmap_controller.rb
    β”‚   β”œβ”€β”€ models_controller.rb
    β”‚   β”œβ”€β”€ n_plus_ones_controller.rb
    β”‚   β”œβ”€β”€ associations_controller.rb
    β”‚   └── live_controller.rb
    └── views/rails_vitals/
        β”œβ”€β”€ dashboard/
        β”œβ”€β”€ requests/
        β”œβ”€β”€ heatmap/
        β”œβ”€β”€ models/
        β”œβ”€β”€ n_plus_ones/
        β”œβ”€β”€ associations/
        └── live/

Key architectural decisions:

  • Zero JS dependencies β€” no Chartkick, no D3, no Chart.js. Tables for data, SVG for diagrams, vanilla JS for interactions.
  • Thread-local Collector β€” instrumentation state is stored per-thread, never shared between concurrent requests.
  • In-memory ring buffer β€” the Store keeps the last N requests in memory. No database writes, no schema migrations.
  • Module prepend for callbacks β€” callback instrumentation wraps ActiveRecord::Base#run_callbacks via Module#prepend. No TracePoint, no monkey-patching.
  • ActiveSupport::Notifications β€” SQL and controller events are captured via the standard Rails instrumentation bus.

Philosophy

Most Rails performance tools tell you what is slow. RailsVitals tells you why it is slow and what the right mental model is.

Every feature is designed around a teaching moment:

  • Query DNA turns SQL into a readable fingerprint with explanations for each token
  • The Association Map connects your data model structure to live performance data
  • N+1 fix suggestions come from your actual associations, not generic advice
  • Score Projection lets you understand the impact of a fix before writing code
  • The Impact Simulator shows blast radius, how many requests are affected, how many queries would be saved

The goal is not just a faster app. The goal is a developer who understands why the app was slow and how to prevent it next time.


Requirements

  • Ruby 3.0+
  • Rails 7.0+
  • PostgreSQL (some internal query filters are PostgreSQL-specific)

Development

git clone https://github.com/your-username/rails_vitals
cd rails_vitals
bundle install

To test against a real app, add to your Gemfile with a local path:

gem "rails_vitals", path: "../rails_vitals"

Contributing

Bug reports and pull requests are welcome on GitHub. This project is intended to be a safe, welcoming space for collaboration.


License

The gem is available as open source under the terms of the MIT License.


Author

Built by David