Skip to content

How to avoid audits without actual changes in JSON field? #765

@denis-kos

Description

@denis-kos

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
end

But 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions