Log transitions on a state_machines gem to support auditing and business process analytics.
This plugin for the state_machines gem adds support for keeping an audit trail for any state machine. Having an audit trail gives you a complete history of the state changes in your model. This history allows you to investigate incidents or perform analytics, like: "How long does it take on average to go from state a to state b?", or "What percentage of cases goes from state a to b via state c?"
For more information read Why developers should be force-fed state machines.
Note: while the state_machines gem integrates with multiple ORMs, this plugin is currently limited to the following ORM backends:
- ActiveRecord
- Mongoid
It should be easy to add new backends by looking at the implementation of the current backends. Pull requests are welcome!
First, make the gem available by adding it to your Gemfile
, and run bundle install
:
# this gem
gem 'state_machines-audit_trail'
# required runtime dependency for your ORM; either
gem 'state_machines-activerecord'
# OR
gem 'state_machines-mongoid'
For the examples below, we will assume you have a pre-existing model called Subscription
that has a state_machine
configured to utilize the state
attribute.
A Rails generator is provided to create a model and a migration, call it with:
rails generate state_machines:audit_trail Subscription state
will generate the SubscriptionStateTransition
model and an accompanying migration.
class Subscription < ActiveRecord::Base
state_machine :state, initial: :start do
audit_trail
...
audit_trail
will register an after_transition
callback that is used to log all transitions including the initial state if there is one.
By default, upon instantiation, a StateTransition
is saved for null => initial
state. This is useful to understand the full history
of any model, but there are cases where this can pollute the audit_trail
. For example, when a model has multiple state_machine
s
that use a single StateTransition
model for persistence (in conjunction with context
below), there would be multiple initial state
transitions. By configuring initial: false
, it will skip the initial state transition logging for this specific state_machine
, while
leaving the others in the model unaffected.
audit_trail initial: false
If your Transition
model does not use the default naming scheme, provide it using the :class
option:
audit_trail class: FooStateTransition
An example use of a custom :class
and :context
(below) would be for a model that has multiple state_machine
definitions. The combination
of these options would allow the use of one transition class that logged state information from both state machines.
Using the :context
option, you can store method results (or attributes exposed as methods) in the state transition class.
In order to utilize this feature, you need to:
- add a field/column to your state transition class (i.e.
SubscriptionStateTransitions
) and perhaps underlying database through a migration - expose the attribute as a method, or create a method to compute a dynamic value
- configure
:context
Store Subscription
field1
in Transition
field field1
:
audit_trail context: :field1
Store Subscription
field1
and field2
in Transition
fields field1
and field2
:
audit_trail context: [:field1, :field2]
Store Subscription
user
in Transition
fields user_id
and user_name
:
class Subscription < ActiveRecord::Base
state_machine :state, initial: :start do
audit_trail context: :user
...
end
end
class SubscriptionStateTransition < ActiveRecord::Base
def user=(u)
self.user_id = u.id
self.user_name = u.name
end
end
Store simple method results.
Sometimes it can be useful to store dynamically computed information, such as those from a Subscription
method #plan_time_remaining
class Subscription < ActiveRecord::Base
state_machine :state, initial: :start do
audit_trail :context: :plan_time_remaining
...
def plan_time_remaining
# Dynamically computed field e.g., based on other models
...
Store method results that interrogate the transition for information such as event
arguments:
class Subscription < ActiveRecord::Base
state_machine :state, initial: :start do
audit_trail :context: :user_name
...
# method receives a state_machines transition object
def user_name(transition)
if transition.args.present?
user_id = transition.args.last.delete(:user_id)
User.find(user_id).name
else
'Undefined User'
end
...
model = Subscription.first
model.ignite!('arg1, 'arg2', 'arg3', user_id: 1)
Conversion from the original code to state_machines
by AlienFast.
The original plugin was written by Jesse Storimer and Willem van Bergen for Shopify. Mongoid support was contributed by Siddharth.
Released under the MIT license (see LICENSE).
- Fork it ( https://github.com/state-machines/state_machines-audit_trail/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request