EagerEye

CI Gem Version Ruby License: MIT

Static analysis tool for detecting N+1 queries in Rails applications.

EagerEye analyzes your Ruby code without running it, using AST (Abstract Syntax Tree) parsing to find potential N+1 query issues before they hit production.

Why EagerEye?

Unlike runtime tools like Bullet, EagerEye:

  • Runs without executing code - Works in CI pipelines without a test suite
  • Catches more patterns - Detects serializer N+1s and missing counter caches
  • Proactive detection - Finds issues at code review time, not after deployment
Feature EagerEye Bullet
Detection method Static analysis Runtime
Requires test suite No Yes
Serializer N+1 detection Yes Limited
Counter cache suggestions Yes No
CI integration Native Requires tests
False positive rate Higher Lower

Installation

Add to your Gemfile:

gem "eager_eye", group: :development

Then run:

bundle install

Or install standalone:

gem install eager_eye

Quick Start

CLI Usage

# Analyze default app/ directory
eager_eye

# Analyze specific paths
eager_eye app/controllers app/serializers

# Output as JSON (for CI)
eager_eye --format json

# Don't fail on issues (exit 0)
eager_eye --no-fail

# Run specific detectors only
eager_eye --only loop_association,serializer_nesting

# Exclude paths
eager_eye --exclude "app/legacy/**"

Rails Integration

# Generate config file
rails g eager_eye:install

# Run via rake
rake eager_eye:analyze

# JSON output
rake eager_eye:json

Detected Issues

1. Loop Association (N+1 in iterations)

Detects association calls inside loops that may cause N+1 queries.

# Bad - N+1 query on each iteration
posts.each do |post|
  post.author.name      # Query for each post!
  post.comments.count   # Another query for each post!
end

# Good - Eager load associations
posts.includes(:author, :comments).each do |post|
  post.author.name      # No additional query
  post.comments.count   # No additional query
end

2. Serializer Nesting (N+1 in serializers)

Detects nested association access in serializer blocks.

# Bad - N+1 in serializer
class PostSerializer < ActiveModel::Serializer
  attribute :author_name do
    object.author.name  # Query for each serialized post!
  end
end

# Good - Eager load in controller
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
    render json: @posts, each_serializer: PostSerializer
  end
end

Supports multiple serializer libraries:

  • ActiveModel::Serializers
  • Blueprinter
  • Alba

3. Missing Counter Cache

Detects .count or .size calls on associations that could benefit from counter caches.

# Bad - COUNT query every time
post.comments.count
post.comments.size

# Good - Add counter cache
# In Comment model:
belongs_to :post, counter_cache: true

# Then this is a simple column read:
post.comments_count

Configuration

Config File (.eager_eye.yml)

# Paths to exclude from analysis (glob patterns)
excluded_paths:
  - app/serializers/legacy/**
  - lib/tasks/**

# Detectors to enable (default: all)
enabled_detectors:
  - loop_association
  - serializer_nesting
  - missing_counter_cache

# Base path to analyze (default: app)
app_path: app

# Exit with error code when issues found (default: true)
fail_on_issues: true

Programmatic Configuration

EagerEye.configure do |config|
  config.excluded_paths = ["app/legacy/**"]
  config.enabled_detectors = [:loop_association, :serializer_nesting]
  config.app_path = "app"
  config.fail_on_issues = true
end

CI Integration

GitHub Actions

name: EagerEye
on: [pull_request]

jobs:
  analyze:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.3"
      - run: gem install eager_eye
      - run: eager_eye app/ --format json > report.json
      - name: Check results
        run: |
          issues=$(cat report.json | ruby -rjson -e 'puts JSON.parse(STDIN.read)["summary"]["total_issues"]')
          if [ "$issues" -gt 0 ]; then
            echo "::warning::Found $issues potential N+1 issues"
          fi

See examples/github_action.yml for a complete example with PR annotations.

CLI Reference

Usage: eager_eye [paths] [options]

Options:
    -f, --format FORMAT      Output format: console, json (default: console)
    -e, --exclude PATTERN    Exclude files matching pattern (can be used multiple times)
    -o, --only DETECTORS     Run only specified detectors (comma-separated)
        --no-fail            Exit with 0 even when issues are found
        --no-color           Disable colored output
    -v, --version            Show version
    -h, --help               Show this help message

Output Formats

Console (default)

EagerEye Analysis Results
=========================

app/controllers/posts_controller.rb
  Line 15: [LoopAssociation] Potential N+1 query: `post.author` called inside iteration
           Suggestion: Consider using `includes(:author)` on the collection before iterating

  Line 23: [MissingCounterCache] `.count` called on `comments` may cause N+1 queries
           Suggestion: Consider adding `counter_cache: true` to the belongs_to association

----------------------------------------
Total: 2 issues (2 warnings, 0 errors)

JSON

{
  "summary": {
    "total_issues": 2,
    "warnings": 2,
    "errors": 0,
    "files_analyzed": 15
  },
  "issues": [
    {
      "detector": "loop_association",
      "file_path": "app/controllers/posts_controller.rb",
      "line_number": 15,
      "message": "Potential N+1 query: `post.author` called inside iteration",
      "severity": "warning",
      "suggestion": "Consider using `includes(:author)` on the collection"
    }
  ]
}

Limitations

EagerEye uses static analysis, which means:

  • No runtime context - Cannot know if associations are already eager loaded elsewhere
  • Heuristic-based - Uses naming conventions to identify associations (may have false positives)
  • Ruby code only - Does not analyze SQL queries or ActiveRecord internals

For best results, use EagerEye alongside runtime tools like Bullet for comprehensive N+1 detection.

Development

# Setup
bin/setup

# Run tests
bundle exec rspec

# Run linter
bundle exec rubocop

# Interactive console
bin/console

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/hamzagedikkaya/eager_eye.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/my-feature)
  3. Commit your changes (git commit -am 'Add my feature')
  4. Push to the branch (git push origin feature/my-feature)
  5. Create a Pull Request

License

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

Code of Conduct

Everyone interacting in the EagerEye project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.