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-dedupednotification_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.