
Security News
Follow-up and Clarification on Recent Malicious Ruby Gems Campaign
A clarification on our recent research investigating 60 malicious Ruby gems.
SeparateHistory
provides a simple and flexible way to keep a complete history of your ActiveRecord model changes in a separate, dedicated history table. It automatically records every create
, update
, and destroy
event, ensuring you have a full audit trail of your data.
manipulated?
method to easily check if a history record has been altered after its creation.Add this line to your application's Gemfile:
gem 'separate_history'
And then execute:
$ bundle install
Getting started with SeparateHistory
is a three-step process:
Use the provided generator to create a migration for the history table. For a model named User
, run:
$ rails g separate_history:sync User
This creates a migration file that defines the schema for your history table.
Next, generate the history model file. This model will include the necessary SeparateHistory::History
module.
$ rails g separate_history:model User
This creates the app/models/user_history.rb
file.
Run the migration to create the table in your database:
$ rails db:migrate
Finally, add the has_separate_history
macro to your original model:
# app/models/user.rb
class User < ApplicationRecord
has_separate_history
end
That's it! Now, every change to a User
instance will be recorded in the user_histories
table.
rails generate separate_history:migration User
rails db:migrate
class User < ApplicationRecord
has_separate_history
end
class Article < ApplicationRecord
has_separate_history only: [:title, :content]
end
class User < ApplicationRecord
has_separate_history except: [:last_sign_in_ip, :encrypted_password]
end
class User < ApplicationRecord
has_separate_history track_changes: true
end
class AdminUser < ApplicationRecord
has_separate_history history_class_name: 'AdminActionLog'
end
Track only certain events (create/update/destroy):
class Document < ApplicationRecord
has_separate_history events: [:create, :update] # Only track creation and updates
end
# Get all history records for a user
user = User.find(1)
user.user_histories.each do |history|
puts "Event: #{history.event} at #{history.history_created_at}"
end
# Or use the alias
user.separate_histories.each { |h| puts h.inspect }
# Get historical state of a record at a specific time
old_user = User.history_as_of(user_id, 1.month.ago)
# Check if history exists for a record
if User.history_exists?(user_id)
# Do something with history
end
You can retrieve the state of a record at any given point in time using the history_for
class method. It returns the last history record created before or at the specified timestamp, giving you a precise snapshot of the record's state.
This query uses the history_updated_at
timestamp to ensure accuracy, even if records were created out of order or their timestamps were manually altered.
# Get the user record as it was 2 days ago
user_snapshot = User.history_for(user.id, 2.days.ago)
puts user_snapshot.name # => "Old Name"
# Get what a user looked like 1 week ago
user_week_ago = user.history_as_of(1.week.ago)
# Get the state of a record that might have been deleted
old_user = User.history_as_of(deleted_user_id, 1.month.ago)
When the history table is missing, you'll get a helpful error message:
History table `user_histories` is missing.
Run `rails g separate_history:model User` to create it.
SeparateHistory includes built-in validation for options:
# These will raise ArgumentError:
has_separate_history only: [:name], except: [:email] # Can't use both only and except
has_separate_history invalid_option: true # Invalid option
has_separate_history events: [:invalid_event] # Invalid event type
has_separate_history track_changes: 'yes' # Must be boolean
When you include has_separate_history
in your model, the following instance methods become available:
#snapshot_history
Manually create a snapshot history record for the current state.
#history?
Returns true
if any history exists for this record, otherwise false
.
#history_as_of(timestamp)
Returns the state of the record at or before the given timestamp.
#all_history
Returns all history records for this instance.
#latest_history
Returns the most recent history record for this instance.
#clear_history(force: true)
Deletes all history records for this instance.
Warning: You must pass force: true
to confirm deletion.
Example:
user = User.create!(name: "Alice")
user.update!(name: "Bob")
user.snapshot_history
user.history? # => true
user.all_history # => [<UserHistory ...>, ...]
user.latest_history # => <UserHistory ...>
user.history_as_of(1.day.ago) # => <UserHistory ...>
user.clear_history(force: true)
By default, SeparateHistory
saves a complete snapshot of the record on every change. For high-traffic tables, this can lead to a lot of data storage. You can optimize this by enabling the track_changes
option. When set to true
, only the attributes that actually changed during an update
event will be saved.
# in app/models/user.rb
class User < ApplicationRecord
has_separate_history track_changes: true
end
With this enabled, if you only update a user's name, the history record will store the new name, but all other attributes will be nil
.
You can prevent certain attributes from being saved to the history table by using the except
option. This is useful for ignoring fields that change frequently but aren't important for auditing, like sign_in_count
or last_login_at
.
# Only track changes to name and email
class User < ApplicationRecord
has_separate_history only: [:name, :email]
end
# Track all attributes except for sign_in_count
class User < ApplicationRecord
has_separate_history except: [:sign_in_count]
end
If you want to use a different name for your history model, you can specify it with the history_class_name
option.
# in app/models/user.rb
class User < ApplicationRecord
has_separate_history history_class_name: 'UserAuditTrail'
end
# in app/models/user_audit_trail.rb
class UserAuditTrail < ApplicationRecord
# ...
end
To verify that a history record has not been altered since it was first created, you can use the manipulated?
method.
last_history = user.histories.last
last_history.manipulated? # => false
# If someone changes the record later...
last_history.update(name: "A new name")
last_history.manipulated? # => true
If you add SeparateHistory
to a model with existing records, you may want to create an initial history entry for them. You can do this by creating a snapshot
event. This is also useful for creating periodic backups of your records.
Here is an example of a Rake task to create an initial snapshot for all records in your User
model:
# lib/tasks/history.rake
namespace :history do
desc "Create initial history records for existing users"
task sync_users: :environment do
User.find_each do |user|
history_class = User.history_class
unless history_class.exists?(original_id: user.id)
history_class.create!(user.attributes.merge(original_id: user.id, event: 'snapshot'))
puts "Created snapshot for User ##{user.id}"
end
end
end
end
Run it with bundle exec rake history:sync_users
.
After checking out the repo, run bin/setup
to install dependencies. Then, run bundle exec rake
to run the tests (RSpec, Minitest, and RuboCop).
This project uses appraisal
to test against multiple versions of Rails. The test suites can be run with:
$ bundle exec appraisal rake
Before running test suites install dependencies.
$ bundle exec appraisal install
You can also run bin/console
for an interactive prompt that will allow you to experiment.
For a detailed log of the debugging and development process for the Rails 7 compatibility fixes, please see DEV.md.
Bug reports and pull requests are welcome on GitHub at https://github.com/sarvesh4396/separate_history.
The gem is available as open source under the terms of the MIT License.
FAQs
Unknown package
We found that separate_history demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
A clarification on our recent research investigating 60 malicious Ruby gems.
Security News
ESLint now supports parallel linting with a new --concurrency flag, delivering major speed gains and closing a 10-year-old feature request.
Research
/Security News
A malicious Go module posing as an SSH brute forcer exfiltrates stolen credentials to a Telegram bot controlled by a Russian-speaking threat actor.