From ec9a82a0176f53aacba23a65a7522065f25ef9b8 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 13 Oct 2025 15:59:59 +0530 Subject: [PATCH] feat: Open conversation when agent bot webhook fails (#12379) # Changelog When an agent bot webhook fails, we now flip any pending conversation back to an open state so a human agent can pick it up immediately. There will be an clear activity message giving the team clear visibility into what went wrong. This keeps customers from getting stuck in limbo when their connected bot goes offline. # Testing instructions 1. Initial setup: Create an agent bot with a working webhook URL and connect it to a test inbox. Send a message from a contact (e.g., via the widget) so a conversation is created; it should enter the Pending state while the bot handles the reply. 2. Introduce failure: Edit that agent bot and swap the webhook URL for a dummy endpoint that will fail. Have the same contact send another message in the existing conversation. Because the webhook call now fails, the conversation should flip from Pending back to Open, making it visible to agents. Also verify the activity message 3. New conversation check: With the dummy URL still in place, start a brand-new conversation from a contact. When the bot tries (and fails) to respond, confirm that the conversation appears immediately as Open rather than remaining Pending. Also the activity message is visible 4. Subsequent messages in open conversations will show no change --------- Co-authored-by: Muhsin Keloth --- app/jobs/agent_bots/webhook_job.rb | 4 ++ config/locales/en.yml | 2 + lib/webhooks/trigger.rb | 27 +++++++++++-- spec/lib/webhooks/trigger_spec.rb | 65 +++++++++++++++++++++++++++++- 4 files changed, 93 insertions(+), 5 deletions(-) diff --git a/app/jobs/agent_bots/webhook_job.rb b/app/jobs/agent_bots/webhook_job.rb index d0c4e7959..b3a3d6cc1 100644 --- a/app/jobs/agent_bots/webhook_job.rb +++ b/app/jobs/agent_bots/webhook_job.rb @@ -1,3 +1,7 @@ class AgentBots::WebhookJob < WebhookJob queue_as :high + + def perform(url, payload, webhook_type = :agent_bot_webhook) + super(url, payload, webhook_type) + end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 6afab9253..ad54b8dfa 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -202,6 +202,8 @@ en: captain: resolved: 'Conversation was marked resolved by %{user_name} due to inactivity' open: 'Conversation was marked open by %{user_name}' + agent_bot: + error_moved_to_open: 'Conversation was marked open by system due to an error with the agent bot.' status: resolved: 'Conversation was marked resolved by %{user_name}' contact_resolved: 'Conversation was resolved by %{contact_name}' diff --git a/lib/webhooks/trigger.rb b/lib/webhooks/trigger.rb index 41b3a415d..95c399d54 100644 --- a/lib/webhooks/trigger.rb +++ b/lib/webhooks/trigger.rb @@ -31,14 +31,33 @@ class Webhooks::Trigger end def handle_error(error) - return unless should_handle_error? + return unless SUPPORTED_ERROR_HANDLE_EVENTS.include?(@payload[:event]) return unless message - update_message_status(error) + case @webhook_type + when :agent_bot_webhook + conversation = message.conversation + return unless conversation&.pending? + + conversation.open! + create_agent_bot_error_activity(conversation) + when :api_inbox_webhook + update_message_status(error) + end end - def should_handle_error? - @webhook_type == :api_inbox_webhook && SUPPORTED_ERROR_HANDLE_EVENTS.include?(@payload[:event]) + def create_agent_bot_error_activity(conversation) + content = I18n.t('conversations.activity.agent_bot.error_moved_to_open') + Conversations::ActivityMessageJob.perform_later(conversation, activity_message_params(conversation, content)) + end + + def activity_message_params(conversation, content) + { + account_id: conversation.account_id, + inbox_id: conversation.inbox_id, + message_type: :activity, + content: content + } end def update_message_status(error) diff --git a/spec/lib/webhooks/trigger_spec.rb b/spec/lib/webhooks/trigger_spec.rb index 8ff2a21a5..224a35e07 100644 --- a/spec/lib/webhooks/trigger_spec.rb +++ b/spec/lib/webhooks/trigger_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe Webhooks::Trigger do + include ActiveJob::TestHelper + subject(:trigger) { described_class } let!(:account) { create(:account) } @@ -8,8 +10,18 @@ describe Webhooks::Trigger do let!(:conversation) { create(:conversation, inbox: inbox) } let!(:message) { create(:message, account: account, inbox: inbox, conversation: conversation) } - let!(:webhook_type) { :api_inbox_webhook } + let(:webhook_type) { :api_inbox_webhook } let!(:url) { 'https://test.com' } + let(:agent_bot_error_content) { I18n.t('conversations.activity.agent_bot.error_moved_to_open') } + + before do + ActiveJob::Base.queue_adapter = :test + end + + after do + clear_enqueued_jobs + clear_performed_jobs + end describe '#execute' do it 'triggers webhook' do @@ -54,6 +66,57 @@ describe Webhooks::Trigger do ).and_raise(RestClient::ExceptionWithResponse.new('error', 500)).once expect { trigger.execute(url, payload, webhook_type) }.to change { message.reload.status }.from('sent').to('failed') end + + context 'when webhook type is agent bot' do + let(:webhook_type) { :agent_bot_webhook } + + it 'reopens conversation and enqueues activity message if pending' do + conversation.update(status: :pending) + payload = { event: 'message_created', conversation: { id: conversation.id }, id: message.id } + + expect(RestClient::Request).to receive(:execute) + .with( + method: :post, + url: url, + payload: payload.to_json, + headers: { content_type: :json, accept: :json }, + timeout: 5 + ).and_raise(RestClient::ExceptionWithResponse.new('error', 500)).once + + expect do + perform_enqueued_jobs do + trigger.execute(url, payload, webhook_type) + end + end.not_to(change { message.reload.status }) + + expect(conversation.reload.status).to eq('open') + + activity_message = conversation.reload.messages.order(:created_at).last + expect(activity_message.message_type).to eq('activity') + expect(activity_message.content).to eq(agent_bot_error_content) + end + + it 'does not change message status or enqueue activity when conversation is not pending' do + payload = { event: 'message_created', conversation: { id: conversation.id }, id: message.id } + + expect(RestClient::Request).to receive(:execute) + .with( + method: :post, + url: url, + payload: payload.to_json, + headers: { content_type: :json, accept: :json }, + timeout: 5 + ).and_raise(RestClient::ExceptionWithResponse.new('error', 500)).once + + expect do + trigger.execute(url, payload, webhook_type) + end.not_to(change { message.reload.status }) + + expect(Conversations::ActivityMessageJob).not_to have_been_enqueued + + expect(conversation.reload.status).to eq('open') + end + end end it 'does not update message status if webhook fails for other events' do