flipper_trail

CI Gem Version Downloads License: MIT

An append-only audit trail for Flipper feature-flag changes — who changed which flag, when, and the before/after state. A free, MIT-licensed alternative to Flipper Cloud's audit history, built on Flipper's public OSS adapter interface.

Not affiliated with or endorsed by Flipper or Flipper Cloud. "Flipper" is used only to describe compatibility.

Install

gem "flipper_trail"

ActiveRecord and Mongoid are supported storage backends; install whichever your app already uses.

Setup (Rails + ActiveRecord)

bin/rails g flipper_trail:install
bin/rails db:migrate

Wrap the decorator around your real Flipper adapter:

require "flipper/adapters/active_record"

Flipper.configure do |config|
  config.adapter { FlipperTrail.wrap(Flipper::Adapters::ActiveRecord.new) }
end

(FlipperTrail.wrap(x) is shorthand for FlipperTrail::Adapter.new(x).) The audit store is inferred from the adapter you wrap — here, :active_record — so there's nothing else to configure.

Capture the acting user per request:

class ApplicationController < ActionController::Base
  before_action { FlipperTrail::Current.actor = current_user }
end

For a mounted Flipper::UI, insert the middleware ahead of the mount so UI toggles are attributed:

config.middleware.use FlipperTrail::Middleware, resolver: ->(env) { resolve_admin(env) }

Mongoid

Wrap your Flipper Mongo adapter exactly the same way — the audit store is inferred from it (:mongoid), so there's nothing else to configure:

Flipper.configure do |config|
  config.default do
    collection = Mongoid.default_client["flipper"]
    Flipper.new(FlipperTrail.wrap(Flipper::Adapters::Mongo.new(collection)))
  end
end

No migration needed — audit entries are stored as Mongoid documents (flipper_trail_entries) in your default Mongoid database.

Mongoid declares the indexes on the document but does not build them automatically. Create them once (e.g. in a deploy or seed step):

bin/rails runner 'require "flipper_trail/storage/mongoid"; FlipperTrail::Storage::Mongoid::Entry.create_indexes'

(If you require "flipper_trail/storage/mongoid" in your initializer, the standard bin/rails db:mongoid:create_indexes will include the audit collection too.)

Where things are stored

flipper_trail has two independent storage concerns:

  • Your flags live wherever your Flipper adapter puts them — you wrap that adapter with FlipperTrail.wrap(...).
  • The audit trail is written to an audit store that defaults to match the adapter you wrap: an ActiveRecord Flipper adapter → audit via ActiveRecord; a Mongo adapter → audit via Mongoid. So you only configure your storage choice once.

Override the audit store when you want them to differ (e.g. flags in Redis, audit in Postgres), or when you wrap an adapter flipper_trail can't infer:

FlipperTrail.configure { |c| c.storage = :active_record } # :active_record | :mongoid | any object responding to #record/#query

If you wrap an adapter flipper_trail can't infer (Redis, Memory, HTTP/Cloud, …) and don't set config.storage, it raises a clear error telling you to pick one.

Query the trail

FlipperTrail.history(feature: "new_checkout", actor_id: 42, since: 1.week.ago, limit: 100)
# => newest-first array of entries (feature_name, operation, gate_name, before, after, actor, created_at)

How it works

FlipperTrail::Adapter decorates your Flipper adapter. On each write (enable/disable/add/remove/clear) it reads gate state before and after, attributes the change to FlipperTrail::Current.actor (falling back to a configurable system actor), and persists an entry. No-op diffs are suppressed, so Flipper's internal add+enable double-write collapses to one meaningful entry for existing flags.

Reliability

Audit writes are isolated from your flag writes. If the audit store is unavailable and record raises, the error goes to config.on_error (default: logged) and the flag operation still succeeds. Set config.raise_on_audit_error = true to fail closed instead.

Performance

Recording is synchronous on the thread performing the toggle, and each audited write reads gate state before and after (a few extra adapter reads per Flipper.enable). Feature toggles are low-frequency admin operations, so this is normally negligible. For high-volume or remote (HTTP/Cloud) flag adapters, supply a custom storage backend (any object responding to #record/#query) whose #record enqueues a background job to move persistence off the request path.

Privacy & data captured

Each entry stores the actor (actor_label commonly holds an email) and the full before/after gate state — which, when a feature is targeted at specific actors or groups, contains those actor/group identifiers. The trail is append-only, so plan retention accordingly (e.g. a TTL index on created_at for Mongoid, or a scheduled prune on the created_at index for ActiveRecord) and account for it when handling data-erasure requests. A pluggable redaction hook is planned for a future release.

Compatibility

  • Ruby >= 3.1.
  • Runtime dependencies: activesupport >= 6.1 and flipper >= 1.0.
  • Optional, host-provided backends: activerecord >= 6.1 and mongoid >= 8.0. These are not runtime dependencies — your application supplies whichever ORM it already uses, and you pick the matching storage backend.

Development

git clone https://github.com/saygun/flipper_trail.git
cd flipper_trail
bin/setup            # installs dependencies into vendor/bundle
bundle exec rake     # runs the spec suite + RuboCop (the full gate)

The default bundle exec rspec run uses an in-memory SQLite database. The Mongoid suite (MONGOID=1 bundle exec rspec) requires a running mongod reachable at 127.0.0.1:27017.

See CONTRIBUTING.md for the full contributor guide.

License

MIT. See LICENSE.txt. The audit-log concept is reimplemented clean-room from public documentation; this gem contains no Flipper Cloud code.


Code of Conduct · Contributing · Security