diff --git a/.circleci/config.yml b/.circleci/config.yml index 09bd5191d..c0320652b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -144,7 +144,7 @@ jobs: # Backend tests with parallelization backend-tests: <<: *defaults - parallelism: 16 + parallelism: 20 steps: - checkout - node/install: diff --git a/app/builders/v2/reports/first_response_time_distribution_builder.rb b/app/builders/v2/reports/first_response_time_distribution_builder.rb index 62565dd44..971542596 100644 --- a/app/builders/v2/reports/first_response_time_distribution_builder.rb +++ b/app/builders/v2/reports/first_response_time_distribution_builder.rb @@ -16,28 +16,27 @@ class V2::Reports::FirstResponseTimeDistributionBuilder def build_distribution results = fetch_aggregated_counts - format_results(results) + map_to_channel_types(results) end def fetch_aggregated_counts ReportingEvent - .joins('INNER JOIN inboxes ON reporting_events.inbox_id = inboxes.id') .where(account_id: account.id, name: 'first_response') .where(range_condition) - .group('inboxes.channel_type') + .group(:inbox_id) .select( - 'inboxes.channel_type', + :inbox_id, bucket_case_statements ) end def bucket_case_statements <<~SQL.squish - COUNT(CASE WHEN reporting_events.value < 3600 THEN 1 END) AS bucket_0_1h, - COUNT(CASE WHEN reporting_events.value >= 3600 AND reporting_events.value < 14400 THEN 1 END) AS bucket_1_4h, - COUNT(CASE WHEN reporting_events.value >= 14400 AND reporting_events.value < 28800 THEN 1 END) AS bucket_4_8h, - COUNT(CASE WHEN reporting_events.value >= 28800 AND reporting_events.value < 86400 THEN 1 END) AS bucket_8_24h, - COUNT(CASE WHEN reporting_events.value >= 86400 THEN 1 END) AS bucket_24h_plus + COUNT(CASE WHEN value < 3600 THEN 1 END) AS bucket_0_1h, + COUNT(CASE WHEN value >= 3600 AND value < 14400 THEN 1 END) AS bucket_1_4h, + COUNT(CASE WHEN value >= 14400 AND value < 28800 THEN 1 END) AS bucket_4_8h, + COUNT(CASE WHEN value >= 28800 AND value < 86400 THEN 1 END) AS bucket_8_24h, + COUNT(CASE WHEN value >= 86400 THEN 1 END) AS bucket_24h_plus SQL end @@ -45,15 +44,25 @@ class V2::Reports::FirstResponseTimeDistributionBuilder range.present? ? { created_at: range } : {} end - def format_results(results) + def inbox_channel_types + @inbox_channel_types ||= account.inboxes.pluck(:id, :channel_type).to_h + end + + def map_to_channel_types(results) results.each_with_object({}) do |row, hash| - hash[row.channel_type] = { - '0-1h' => row.bucket_0_1h, - '1-4h' => row.bucket_1_4h, - '4-8h' => row.bucket_4_8h, - '8-24h' => row.bucket_8_24h, - '24h+' => row.bucket_24h_plus - } + channel_type = inbox_channel_types[row.inbox_id] + next unless channel_type + + hash[channel_type] ||= empty_buckets + hash[channel_type]['0-1h'] += row.bucket_0_1h + hash[channel_type]['1-4h'] += row.bucket_1_4h + hash[channel_type]['4-8h'] += row.bucket_4_8h + hash[channel_type]['8-24h'] += row.bucket_8_24h + hash[channel_type]['24h+'] += row.bucket_24h_plus end end + + def empty_buckets + { '0-1h' => 0, '1-4h' => 0, '4-8h' => 0, '8-24h' => 0, '24h+' => 0 } + end end diff --git a/db/migrate/20260130061021_add_index_to_reporting_events_for_response_distribution.rb b/db/migrate/20260130061021_add_index_to_reporting_events_for_response_distribution.rb new file mode 100644 index 000000000..b7807901c --- /dev/null +++ b/db/migrate/20260130061021_add_index_to_reporting_events_for_response_distribution.rb @@ -0,0 +1,11 @@ +class AddIndexToReportingEventsForResponseDistribution < ActiveRecord::Migration[7.1] + disable_ddl_transaction! + + def change + add_index :reporting_events, + [:account_id, :name, :inbox_id, :created_at], + name: 'index_reporting_events_for_response_distribution', + algorithm: :concurrently, + if_not_exists: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 148e7769c..fd4d18cb1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do +ActiveRecord::Schema[7.1].define(version: 2026_01_30_061021) do # These extensions should be enabled to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -1115,6 +1115,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do t.datetime "event_start_time", precision: nil t.datetime "event_end_time", precision: nil t.index ["account_id", "name", "created_at"], name: "reporting_events__account_id__name__created_at" + t.index ["account_id", "name", "inbox_id", "created_at"], name: "index_reporting_events_for_response_distribution" t.index ["account_id"], name: "index_reporting_events_on_account_id" t.index ["conversation_id"], name: "index_reporting_events_on_conversation_id" t.index ["created_at"], name: "index_reporting_events_on_created_at" diff --git a/spec/enterprise/services/enterprise/billing/topup_checkout_service_spec.rb b/spec/enterprise/services/enterprise/billing/topup_checkout_service_spec.rb index 3b4d138f3..8a64e6fd2 100644 --- a/spec/enterprise/services/enterprise/billing/topup_checkout_service_spec.rb +++ b/spec/enterprise/services/enterprise/billing/topup_checkout_service_spec.rb @@ -46,7 +46,7 @@ describe Enterprise::Billing::TopupCheckoutService do it 'raises error for invalid credits' do expect do service.create_checkout_session(credits: 500) - end.to(raise_error { |error| expect(error.class.name).to eq('Enterprise::Billing::TopupCheckoutService::Error') }) + end.to raise_error(Enterprise::Billing::TopupCheckoutService::Error) end it 'raises error when account is on free plan' do @@ -54,7 +54,7 @@ describe Enterprise::Billing::TopupCheckoutService do expect do service.create_checkout_session(credits: 1000) - end.to(raise_error { |error| expect(error.class.name).to eq('Enterprise::Billing::TopupCheckoutService::Error') }) + end.to raise_error(Enterprise::Billing::TopupCheckoutService::Error) end end end diff --git a/spec/jobs/send_reply_job_spec.rb b/spec/jobs/send_reply_job_spec.rb index 908d75088..46d8e5e56 100644 --- a/spec/jobs/send_reply_job_spec.rb +++ b/spec/jobs/send_reply_job_spec.rb @@ -33,8 +33,8 @@ RSpec.describe SendReplyJob do twitter_channel = create(:channel_twitter_profile) twitter_inbox = create(:inbox, channel: twitter_channel) message = create(:message, conversation: create(:conversation, inbox: twitter_inbox)) - allow(Twitter::SendOnTwitterService).to receive(:new).with(message: having_attributes(id: message.id)).and_return(process_service) - expect(Twitter::SendOnTwitterService).to receive(:new).with(message: having_attributes(id: message.id)) + allow(Twitter::SendOnTwitterService).to receive(:new).with(message: message).and_return(process_service) + expect(Twitter::SendOnTwitterService).to receive(:new).with(message: message) expect(process_service).to receive(:perform) described_class.perform_now(message.id) end @@ -42,8 +42,8 @@ RSpec.describe SendReplyJob do it 'calls ::Twilio::SendOnTwilioService when its twilio message' do twilio_channel = create(:channel_twilio_sms) message = create(:message, conversation: create(:conversation, inbox: twilio_channel.inbox)) - allow(Twilio::SendOnTwilioService).to receive(:new).with(message: having_attributes(id: message.id)).and_return(process_service) - expect(Twilio::SendOnTwilioService).to receive(:new).with(message: having_attributes(id: message.id)) + allow(Twilio::SendOnTwilioService).to receive(:new).with(message: message).and_return(process_service) + expect(Twilio::SendOnTwilioService).to receive(:new).with(message: message) expect(process_service).to receive(:perform) described_class.perform_now(message.id) end @@ -51,8 +51,8 @@ RSpec.describe SendReplyJob do it 'calls ::Telegram::SendOnTelegramService when its telegram message' do telegram_channel = create(:channel_telegram) message = create(:message, conversation: create(:conversation, inbox: telegram_channel.inbox)) - allow(Telegram::SendOnTelegramService).to receive(:new).with(message: having_attributes(id: message.id)).and_return(process_service) - expect(Telegram::SendOnTelegramService).to receive(:new).with(message: having_attributes(id: message.id)) + allow(Telegram::SendOnTelegramService).to receive(:new).with(message: message).and_return(process_service) + expect(Telegram::SendOnTelegramService).to receive(:new).with(message: message) expect(process_service).to receive(:perform) described_class.perform_now(message.id) end @@ -60,8 +60,8 @@ RSpec.describe SendReplyJob do it 'calls ::Line:SendOnLineService when its line message' do line_channel = create(:channel_line) message = create(:message, conversation: create(:conversation, inbox: line_channel.inbox)) - allow(Line::SendOnLineService).to receive(:new).with(message: having_attributes(id: message.id)).and_return(process_service) - expect(Line::SendOnLineService).to receive(:new).with(message: having_attributes(id: message.id)) + allow(Line::SendOnLineService).to receive(:new).with(message: message).and_return(process_service) + expect(Line::SendOnLineService).to receive(:new).with(message: message) expect(process_service).to receive(:perform) described_class.perform_now(message.id) end @@ -70,8 +70,8 @@ RSpec.describe SendReplyJob do stub_request(:post, 'https://waba.360dialog.io/v1/configs/webhook') whatsapp_channel = create(:channel_whatsapp, sync_templates: false) message = create(:message, conversation: create(:conversation, inbox: whatsapp_channel.inbox)) - allow(Whatsapp::SendOnWhatsappService).to receive(:new).with(message: having_attributes(id: message.id)).and_return(process_service) - expect(Whatsapp::SendOnWhatsappService).to receive(:new).with(message: having_attributes(id: message.id)) + allow(Whatsapp::SendOnWhatsappService).to receive(:new).with(message: message).and_return(process_service) + expect(Whatsapp::SendOnWhatsappService).to receive(:new).with(message: message) expect(process_service).to receive(:perform) described_class.perform_now(message.id) end @@ -79,8 +79,8 @@ RSpec.describe SendReplyJob do it 'calls ::Sms::SendOnSmsService when its sms message' do sms_channel = create(:channel_sms) message = create(:message, conversation: create(:conversation, inbox: sms_channel.inbox)) - allow(Sms::SendOnSmsService).to receive(:new).with(message: having_attributes(id: message.id)).and_return(process_service) - expect(Sms::SendOnSmsService).to receive(:new).with(message: having_attributes(id: message.id)) + allow(Sms::SendOnSmsService).to receive(:new).with(message: message).and_return(process_service) + expect(Sms::SendOnSmsService).to receive(:new).with(message: message) expect(process_service).to receive(:perform) described_class.perform_now(message.id) end @@ -88,8 +88,8 @@ RSpec.describe SendReplyJob do it 'calls ::Instagram::Direct::SendOnInstagramService when its instagram message' do instagram_channel = create(:channel_instagram) message = create(:message, conversation: create(:conversation, inbox: instagram_channel.inbox)) - allow(Instagram::SendOnInstagramService).to receive(:new).with(message: having_attributes(id: message.id)).and_return(process_service) - expect(Instagram::SendOnInstagramService).to receive(:new).with(message: having_attributes(id: message.id)) + allow(Instagram::SendOnInstagramService).to receive(:new).with(message: message).and_return(process_service) + expect(Instagram::SendOnInstagramService).to receive(:new).with(message: message) expect(process_service).to receive(:perform) described_class.perform_now(message.id) end @@ -112,8 +112,8 @@ RSpec.describe SendReplyJob do it 'calls ::Email::SendOnEmailService when its email message' do email_channel = create(:channel_email) message = create(:message, conversation: create(:conversation, inbox: email_channel.inbox)) - allow(Email::SendOnEmailService).to receive(:new).with(message: having_attributes(id: message.id)).and_return(process_service) - expect(Email::SendOnEmailService).to receive(:new).with(message: having_attributes(id: message.id)) + allow(Email::SendOnEmailService).to receive(:new).with(message: message).and_return(process_service) + expect(Email::SendOnEmailService).to receive(:new).with(message: message) expect(process_service).to receive(:perform) described_class.perform_now(message.id) end @@ -121,8 +121,8 @@ RSpec.describe SendReplyJob do it 'calls ::Messages::SendEmailNotificationService when its webwidget message' do webwidget_channel = create(:channel_widget) message = create(:message, conversation: create(:conversation, inbox: webwidget_channel.inbox)) - allow(Messages::SendEmailNotificationService).to receive(:new).with(message: having_attributes(id: message.id)).and_return(process_service) - expect(Messages::SendEmailNotificationService).to receive(:new).with(message: having_attributes(id: message.id)) + allow(Messages::SendEmailNotificationService).to receive(:new).with(message: message).and_return(process_service) + expect(Messages::SendEmailNotificationService).to receive(:new).with(message: message) expect(process_service).to receive(:perform) described_class.perform_now(message.id) end @@ -130,8 +130,8 @@ RSpec.describe SendReplyJob do it 'calls ::Messages::SendEmailNotificationService when its api channel message' do api_channel = create(:channel_api) message = create(:message, conversation: create(:conversation, inbox: api_channel.inbox)) - allow(Messages::SendEmailNotificationService).to receive(:new).with(message: having_attributes(id: message.id)).and_return(process_service) - expect(Messages::SendEmailNotificationService).to receive(:new).with(message: having_attributes(id: message.id)) + allow(Messages::SendEmailNotificationService).to receive(:new).with(message: message).and_return(process_service) + expect(Messages::SendEmailNotificationService).to receive(:new).with(message: message) expect(process_service).to receive(:perform) described_class.perform_now(message.id) end @@ -139,8 +139,8 @@ RSpec.describe SendReplyJob do it 'calls ::Tiktok::SendOnTiktokService when its tiktok message' do tiktok_channel = create(:channel_tiktok) message = create(:message, conversation: create(:conversation, inbox: tiktok_channel.inbox)) - allow(Tiktok::SendOnTiktokService).to receive(:new).with(message: having_attributes(id: message.id)).and_return(process_service) - expect(Tiktok::SendOnTiktokService).to receive(:new).with(message: having_attributes(id: message.id)) + allow(Tiktok::SendOnTiktokService).to receive(:new).with(message: message).and_return(process_service) + expect(Tiktok::SendOnTiktokService).to receive(:new).with(message: message) expect(process_service).to receive(:perform) described_class.perform_now(message.id) end