Bulletin

A mountable Rails engine that gives Bullet a memory.

Bullet is a great sensor — it detects N+1 queries, unused eager loading, and missing counter caches on every request — but it has no memory: each warning flashes in the footer/log and is gone. Bulletin reads Bullet's per-request findings, fingerprints them into durable issues (Sentry-style), and exposes a mounted UI for triage (Flipper UI / PgHero in spirit).

Bullet (sensor)  →  Bulletin (memory)  →  mounted UI (attention)

Install

# Gemfile (typically the :development group, alongside bullet)
gem "bulletin-rb"   # the Ruby namespace is still `Bulletin`
bundle install
bin/rails generate bulletin:install   # writes config/initializers/bulletin.rb
bin/rails db:migrate                   # creates bulletin_issues / bulletin_occurrences
# config/routes.rb
mount Bulletin::Engine => "/bulletin"

Visit /bulletin.

How it works

  • A Rack middleware is inserted inside Bullet::Rack. On the response unwind it reads Bullet's already-deduped notification_collector.collection (before Bullet clears it), attaches request context from the Rack env, and hands a JSON-safe payload to the store.
  • Warnings are fingerprinted by kind + model + association(s) + controller#action — deliberately not file:line, so refactors don't spawn duplicate issues. The precise call stack is kept per occurrence instead.
  • Each issue carries Bullet's own suggested fix. Occurrences are capped per issue; a resolved issue that recurs is automatically reopened.

Configuration

See config/initializers/bulletin.rb. Key knobs:

Option Default Notes
store :active_record :null disables; :redis/:hybrid planned
write_strategy :active_job :inline for zero-setup local dev
occurrence_cap 50 rows kept per issue
retention 7.days PruneJob ages out older issues
enabled follows Bullet.enable? dev/staging only by default
authenticate_with dev-only ->(request) { ... } like Sidekiq::Web

Status

MVP. The store is abstracted behind a single Bulletin::Store boundary so a Redis hot-path + hybrid drain (and a live occurrence view) can land later without touching the middleware or UI.