feat: Add first response time distribution report endpoint (#13400)

The index is already added in production.

Adds a new reporting API that returns conversation counts grouped by
channel type and first response time buckets (0-1h, 1-4h, 4-8h, 8-24h,
24h+).

- GET /api/v2/accounts/:id/reports/first_response_time_distribution
- Uses SQL aggregation to handle large datasets efficiently
- Adds composite index on reporting_events for query performance

Tested on production workload.
Request: GET
`/api/v2/accounts/1/reports/first_response_time_distribution?since=<since>&until=<until>`
Response payload:
```
{
    "Channel::WebWidget": {
      "0-1h": 120,
      "1-4h": 85,
      "4-8h": 32,
      "8-24h": 12,
      "24h+": 3
    },
    "Channel::Email": {
      "0-1h": 12,
      "1-4h": 28,
      "4-8h": 45,
      "8-24h": 35,
      "24h+": 10
    },
    "Channel::FacebookPage": {
      "0-1h": 50,
      "1-4h": 30,
      "4-8h": 15,
      "8-24h": 8,
      "24h+": 2
    }
  }
```

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Pranav
2026-01-30 10:22:27 -08:00
committed by GitHub
parent 85324c82fa
commit 5ec77aca64
7 changed files with 291 additions and 24 deletions

View File

@@ -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: message).and_return(process_service)
expect(Twitter::SendOnTwitterService).to receive(:new).with(message: message)
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))
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: message).and_return(process_service)
expect(Twilio::SendOnTwilioService).to receive(:new).with(message: message)
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))
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: message).and_return(process_service)
expect(Telegram::SendOnTelegramService).to receive(:new).with(message: message)
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))
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: message).and_return(process_service)
expect(Line::SendOnLineService).to receive(:new).with(message: message)
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))
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: message).and_return(process_service)
expect(Whatsapp::SendOnWhatsappService).to receive(:new).with(message: message)
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))
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: message).and_return(process_service)
expect(Sms::SendOnSmsService).to receive(:new).with(message: message)
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))
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: message).and_return(process_service)
expect(Instagram::SendOnInstagramService).to receive(:new).with(message: message)
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))
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: message).and_return(process_service)
expect(Email::SendOnEmailService).to receive(:new).with(message: message)
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))
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: message).and_return(process_service)
expect(Messages::SendEmailNotificationService).to receive(:new).with(message: message)
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))
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: message).and_return(process_service)
expect(Messages::SendEmailNotificationService).to receive(:new).with(message: message)
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))
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: message).and_return(process_service)
expect(Tiktok::SendOnTiktokService).to receive(:new).with(message: message)
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))
expect(process_service).to receive(:perform)
described_class.perform_now(message.id)
end