-
Notifications
You must be signed in to change notification settings - Fork 670
Description
Hey, guys.
I have recently faced an issue when audits for an update action are created even though there were no changes in the JSON field.
Here is the script to reproduce
# frozen_string_literal: true
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
gem "rails"
gem "sqlite3"
gem "audited"
end
require 'sqlite3'
require 'active_record'
ActiveRecord::Base.establish_connection(
adapter: 'sqlite3',
database: ':memory:'
)
ActiveRecord::Schema.define do
create_table "audits", force: :cascade do |t|
t.integer "auditable_id"
t.string "auditable_type"
t.integer "associated_id"
t.string "associated_type"
t.integer "user_id"
t.string "user_type"
t.string "username"
t.string "action"
t.json "audited_changes"
t.integer "version", default: 0, null: false
t.string "comment"
t.string "remote_address"
t.string "request_uuid"
t.string "user_agent"
t.string "ip"
t.string "session_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "custom_objects", force: :cascade do |t|
t.json "custom", default: {}, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
class CustomObject < ApplicationRecord
audited
end
co = CustomObject.create!
co.reload
co.custom_will_change!
co.save!
puts "audits: #{CustomObject.first.audits.count}"
p CustomObject.first.audits.last
# action: "update",
# audited_changes: {"custom"=>[{}, {}]}
co2 = CustomObject.create!
time = Time.now
co2.custom['time'] = time
co2.save!
co2.reload
co2.custom['time'] = time
co2.save!
puts "audits: #{CustomObject.second.audits.count}"
p CustomObject.second.audits.last
# action: "update",
# audited_changes: {"custom"=>[{"time"=>"2025-09-14T11:50:58.152+02:00"}, {"time"=>"2025-09-14T11:50:58.152+02:00"}]}I have added around_audit method to avoid such cases
class CustomObject < ApplicationRecord
audited
def around_audit
if persisted? && changes.except('updated_at').one? && (custom_changes = changes['custom'])
before, after = custom_changes
if (before == after) || (JSON.parse(before.to_json) == JSON.parse(after.to_json))
return
end
end
current_audit = yield
current_audit.tap(&:save!)
end
endBut still, it looks like this is not covering all use cases as I still see such records with no actual changes are still being created in the database:
#<Audited::Audit:0x00007f6e60141498
action: "update",
audited_changes:
{"custom"=>
[{"updated_at"=>"",
"updated_by"=>"",
"open_orders"=>"183055.92",
"credit_limit"=>"1400000.0",
"open_invoices"=>"158781.49",
"payment_terms"=>67,
"delivery_notes"=>"41252.92",
"custom_object_id"=>""},
{"updated_at"=>"",
"updated_by"=>"",
"open_orders"=>"183055.92",
"credit_limit"=>"1400000.0",
"open_invoices"=>"158781.49",
"payment_terms"=>67,
"delivery_notes"=>"41252.92",
"custom_object_id"=>""}]},This is problematic because there are a lot of record updates happening daily and we have ~20k of such empty audits daily. Not only it takes up DB space and maintenance to clear those out, it also confusing from the reports perspective as show the change even though there were no real changes made.
Maybe someone also encountered such issue as well and know how to deal with it? I would appreciate any help here.
Script I use to clear those audits
records = Audited::Audit.where("(SELECT jsonb_agg(key) FROM jsonb_each(audited_changes) ) = '[\"custom\"]'").where(action: 'update').where("jsonb_array_length(audited_changes->'custom') = 2").where("(audited_changes->'custom'->0)::jsonb = (audited_changes->'custom'->1)::jsonb");nil
loop do
count = records.limit(10000).delete_all
break if count.zero?
putc '.'
end