From 199dcd382ed7740d9b8bac3d274ef354ceb08423 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 11 Mar 2026 21:40:38 -0700 Subject: [PATCH] fix: Skip redundant contact saves in ContactIdentifyAction (#13778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the SDK sends identify calls with identical payloads (common on every page load), `save!` fires even though no attributes changed. While Rails skips the actual UPDATE SQL, it still opens a transaction, runs all callbacks (including validation queries like `Contact Exists?`), and triggers `after_commit` hooks — all for a no-op. This adds a `changed?` guard before `save!` to skip it entirely when no attributes have actually changed. **How to test** - Trigger an identify call via the SDK with a contact's existing attributes (same name, email, custom_attributes, etc.) - The contact should not fire a save (no transaction, no callbacks) - Trigger an identify call with a changed attribute — save should work normally **What changed** - `ContactIdentifyAction#update_contact`: guard `save!` with `changed?` check - Added specs to verify `save!` is skipped for unchanged params and avatar job still enqueues independently --- app/actions/contact_identify_action.rb | 2 +- spec/actions/contact_identify_action_spec.rb | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/actions/contact_identify_action.rb b/app/actions/contact_identify_action.rb index bcf5a93c3..9afa5d019 100644 --- a/app/actions/contact_identify_action.rb +++ b/app/actions/contact_identify_action.rb @@ -104,7 +104,7 @@ class ContactIdentifyAction # blank identifier or email will throw unique index error # TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded @contact.discard_invalid_attrs if discard_invalid_attrs - @contact.save! + @contact.save! if @contact.changed? enqueue_avatar_job end diff --git a/spec/actions/contact_identify_action_spec.rb b/spec/actions/contact_identify_action_spec.rb index 568f4db98..76b370f35 100644 --- a/spec/actions/contact_identify_action_spec.rb +++ b/spec/actions/contact_identify_action_spec.rb @@ -145,5 +145,24 @@ describe ContactIdentifyAction do expect(contact.phone_number).to be_nil end end + + context 'when params have not changed' do + it 'skips save and does not issue an UPDATE query' do + contact.update!(name: 'test', identifier: 'test_id', custom_attributes: { test: 'test', test1: 'test1' }) + params = { name: 'test', identifier: 'test_id', custom_attributes: { test: 'test', test1: 'test1' } } + + # any_instance is needed because merge lookup can reassign @contact to a different Ruby object + expect_any_instance_of(Contact).not_to receive(:save!) # rubocop:disable RSpec/AnyInstance + described_class.new(contact: contact, params: params).perform + end + + it 'still enqueues avatar job even when attributes have not changed' do + contact.update!(name: 'test') + params = { name: 'test', avatar_url: 'https://chatwoot-assets.local/sample.png' } + + expect(Avatar::AvatarFromUrlJob).to receive(:perform_later).with(contact, params[:avatar_url]).once + described_class.new(contact: contact, params: params).perform + end + end end end