-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'develop' into feat/CW-1957
- Loading branch information
Showing
14 changed files
with
548 additions
and
15 deletions.
There are no files selected for viewing
30 changes: 30 additions & 0 deletions
30
app/builders/v2/reports/conversations/base_report_builder.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
48
app/builders/v2/reports/timeseries/average_report_builder.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
46
app/builders/v2/reports/timeseries/base_timeseries_builder.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
71
app/builders/v2/reports/timeseries/count_report_builder.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
spec/builders/v2/reports/conversations/metric_builder_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.