Skip to content

Commit

Permalink
Merge branch 'develop' into feat/CW-1957
Browse files Browse the repository at this point in the history
  • Loading branch information
iamsivin committed May 23, 2024
2 parents 1b132bc + 4b93738 commit f23cc22
Show file tree
Hide file tree
Showing 14 changed files with 548 additions and 15 deletions.
30 changes: 30 additions & 0 deletions app/builders/v2/reports/conversations/base_report_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class V2::Reports::Conversations::BaseReportBuilder
pattr_initialize :account, :params

private

AVG_METRICS = %w[avg_first_response_time avg_resolution_time reply_time].freeze
COUNT_METRICS = %w[
conversations_count
incoming_messages_count
outgoing_messages_count
resolutions_count
bot_resolutions_count
bot_handoffs_count
].freeze

def builder_class(metric)
case metric
when *AVG_METRICS
V2::Reports::Timeseries::AverageReportBuilder
when *COUNT_METRICS
V2::Reports::Timeseries::CountReportBuilder
end
end

def log_invalid_metric
Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}"

{}
end
end
30 changes: 30 additions & 0 deletions app/builders/v2/reports/conversations/metric_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class V2::Reports::Conversations::MetricBuilder < V2::Reports::Conversations::BaseReportBuilder
def summary
{
conversations_count: count('conversations_count'),
incoming_messages_count: count('incoming_messages_count'),
outgoing_messages_count: count('outgoing_messages_count'),
avg_first_response_time: count('avg_first_response_time'),
avg_resolution_time: count('avg_resolution_time'),
resolutions_count: count('resolutions_count'),
reply_time: count('reply_time')
}
end

def bot_summary
{
bot_resolutions_count: count('bot_resolutions_count'),
bot_handoffs_count: count('bot_handoffs_count')
}
end

private

def count(metric)
builder_class(metric).new(account, builder_params(metric)).aggregate_value
end

def builder_params(metric)
params.merge({ metric: metric })
end
end
21 changes: 21 additions & 0 deletions app/builders/v2/reports/conversations/report_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class V2::Reports::Conversations::ReportBuilder < V2::Reports::Conversations::BaseReportBuilder
def timeseries
perform_action(:timeseries)
end

def aggregate_value
perform_action(:aggregate_value)
end

private

def perform_action(method_name)
return builder.new(account, params).public_send(method_name) if builder.present?

log_invalid_metric
end

def builder
builder_class(params[:metric])
end
end
48 changes: 48 additions & 0 deletions app/builders/v2/reports/timeseries/average_report_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
class V2::Reports::Timeseries::AverageReportBuilder < V2::Reports::Timeseries::BaseTimeseriesBuilder
def timeseries
grouped_average_time = reporting_events.average(average_value_key)
grouped_event_count = reporting_events.count
grouped_average_time.each_with_object([]) do |element, arr|
event_date, average_time = element
arr << {
value: average_time,
timestamp: event_date.in_time_zone(timezone).to_i,
count: grouped_event_count[event_date]
}
end
end

def aggregate_value
object_scope.average(average_value_key)
end

private

def event_name
metric_to_event_name = {
avg_first_response_time: :first_response,
avg_resolution_time: :conversation_resolved,
reply_time: :reply_time
}
metric_to_event_name[params[:metric].to_sym]
end

def object_scope
scope.reporting_events.where(name: event_name, created_at: range)
end

def reporting_events
@grouped_values = object_scope.group_by_period(
group_by,
:created_at,
default_value: 0,
range: range,
permit: %w[day week month year hour],
time_zone: timezone
)
end

def average_value_key
@average_value_key ||= params[:business_hours].present? ? :value_in_business_hours : :value
end
end
46 changes: 46 additions & 0 deletions app/builders/v2/reports/timeseries/base_timeseries_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
class V2::Reports::Timeseries::BaseTimeseriesBuilder
include TimezoneHelper
include DateRangeHelper
DEFAULT_GROUP_BY = 'day'.freeze

pattr_initialize :account, :params

def scope
case params[:type].to_sym
when :account
account
when :inbox
inbox
when :agent
user
when :label
label
when :team
team
end
end

def inbox
@inbox ||= account.inboxes.find(params[:id])
end

def user
@user ||= account.users.find(params[:id])
end

def label
@label ||= account.labels.find(params[:id])
end

def team
@team ||= account.teams.find(params[:id])
end

def group_by
@group_by ||= %w[day week month year hour].include?(params[:group_by]) ? params[:group_by] : DEFAULT_GROUP_BY
end

def timezone
@timezone ||= timezone_name_from_offset(params[:timezone_offset])
end
end
71 changes: 71 additions & 0 deletions app/builders/v2/reports/timeseries/count_report_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
class V2::Reports::Timeseries::CountReportBuilder < V2::Reports::Timeseries::BaseTimeseriesBuilder
def timeseries
grouped_count.each_with_object([]) do |element, arr|
event_date, event_count = element

# The `event_date` is in Date format (without time), such as "Wed, 15 May 2024".
# We need a timestamp for the start of the day. However, we can't use `event_date.to_time.to_i`
# because it converts the date to 12:00 AM server timezone.
# The desired output should be 12:00 AM in the specified timezone.
arr << { value: event_count, timestamp: event_date.in_time_zone(timezone).to_i }
end
end

def aggregate_value
object_scope.count
end

private

def metric
@metric ||= params[:metric]
end

def object_scope
send("scope_for_#{metric}")
end

def scope_for_conversations_count
scope.conversations.where(account_id: account.id, created_at: range)
end

def scope_for_incoming_messages_count
scope.messages.where(account_id: account.id, created_at: range).incoming.unscope(:order)
end

def scope_for_outgoing_messages_count
scope.messages.where(account_id: account.id, created_at: range).outgoing.unscope(:order)
end

def scope_for_resolutions_count
scope.reporting_events.joins(:conversation).select(:conversation_id).where(
name: :conversation_resolved,
conversations: { status: :resolved }, created_at: range
).distinct
end

def scope_for_bot_resolutions_count
scope.reporting_events.joins(:conversation).select(:conversation_id).where(
name: :conversation_bot_resolved,
conversations: { status: :resolved }, created_at: range
).distinct
end

def scope_for_bot_handoffs_count
scope.reporting_events.joins(:conversation).select(:conversation_id).where(
name: :conversation_bot_handoff,
created_at: range
).distinct
end

def grouped_count
@grouped_values = object_scope.group_by_period(
group_by,
:created_at,
default_value: 0,
range: range,
permit: %w[day week month year hour],
time_zone: timezone
).count
end
end
19 changes: 9 additions & 10 deletions app/controllers/api/v2/accounts/reports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,17 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
before_action :check_authorization

def index
builder = V2::ReportBuilder.new(Current.account, report_params)
data = builder.build
builder = V2::Reports::Conversations::ReportBuilder.new(Current.account, report_params)
data = builder.timeseries
render json: data
end

def summary
render json: summary_metrics
render json: build_summary(:summary)
end

def bot_summary
summary = V2::ReportBuilder.new(Current.account, current_summary_params).bot_summary
summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).bot_summary
render json: summary
render json: build_summary(:bot_summary)
end

def agents
Expand Down Expand Up @@ -126,10 +124,11 @@ def range
}
end

def summary_metrics
summary = V2::ReportBuilder.new(Current.account, current_summary_params).summary
summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).summary
summary
def build_summary(method)
builder = V2::Reports::Conversations::MetricBuilder
current_summary = builder.new(Current.account, current_summary_params).send(method)
previous_summary = builder.new(Current.account, previous_summary_params).send(method)
current_summary.merge(previous: previous_summary)
end

def conversation_metrics
Expand Down
19 changes: 19 additions & 0 deletions app/helpers/timezone_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module TimezoneHelper
# ActiveSupport TimeZone is not aware of the current time, so ActiveSupport::Timezone[offset]
# would return the timezone without considering day light savings. To get the correct timezone,
# this method uses zone.now.utc_offset for comparison as referenced in the issues below
#
# https://github.com/rails/rails/pull/22243
# https://github.com/rails/rails/issues/21501
# https://github.com/rails/rails/issues/7297
def timezone_name_from_offset(offset)
return 'UTC' if offset.blank?

offset_in_seconds = offset.to_f * 3600
matching_zone = ActiveSupport::TimeZone.all.find do |zone|
zone.now.utc_offset == offset_in_seconds
end

return matching_zone.name if matching_zone
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ defineProps({
<div
class="flex items-center justify-between h-10 min-h-[40px] sticky top-0 bg-white z-10 dark:bg-slate-800 gap-2 px-3 border-b rounded-t-xl border-slate-50 dark:border-slate-700"
>
<div class="flex items-center w-full gap-2">
<div class="flex items-center w-full gap-2" @keyup.space.prevent>
<fluent-icon
icon="search"
size="18"
class="text-slate-400 dark:text-slate-400"
size="16"
class="text-slate-400 dark:text-slate-400 flex-shrink-0"
/>
<input
type="text"
Expand Down
50 changes: 50 additions & 0 deletions spec/builders/v2/reports/conversations/metric_builder_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
require 'rails_helper'

RSpec.describe V2::Reports::Conversations::MetricBuilder, type: :model do
subject { described_class.new(account, params) }

let(:account) { create(:account) }
let(:params) { { since: '2023-01-01', until: '2024-01-01' } }
let(:count_builder_instance) { instance_double(V2::Reports::Timeseries::CountReportBuilder, aggregate_value: 42) }
let(:avg_builder_instance) { instance_double(V2::Reports::Timeseries::AverageReportBuilder, aggregate_value: 42) }

before do
allow(V2::Reports::Timeseries::CountReportBuilder).to receive(:new).and_return(count_builder_instance)
allow(V2::Reports::Timeseries::AverageReportBuilder).to receive(:new).and_return(avg_builder_instance)
end

describe '#summary' do
it 'returns the correct summary values' do
summary = subject.summary
expect(summary).to eq(
{
conversations_count: 42,
incoming_messages_count: 42,
outgoing_messages_count: 42,
avg_first_response_time: 42,
avg_resolution_time: 42,
resolutions_count: 42,
reply_time: 42
}
)
end

it 'creates builders with proper params' do
subject.summary
expect(V2::Reports::Timeseries::CountReportBuilder).to have_received(:new).with(account, params.merge(metric: 'conversations_count'))
expect(V2::Reports::Timeseries::AverageReportBuilder).to have_received(:new).with(account, params.merge(metric: 'avg_first_response_time'))
end
end

describe '#bot_summary' do
it 'returns a detailed summary of bot-specific conversation metrics' do
bot_summary = subject.bot_summary
expect(bot_summary).to eq(
{
bot_resolutions_count: 42,
bot_handoffs_count: 42
}
)
end
end
end
Loading

0 comments on commit f23cc22

Please sign in to comment.