feat: kickstart new aggregator based builder

This commit is contained in:
Shivam Mishra
2025-10-14 16:59:59 +05:30
parent 3b65450a0b
commit 8dcabee652
6 changed files with 184 additions and 1 deletions
@@ -0,0 +1,13 @@
class V2::Reports::Conversations::BaseAggregator < V2::Reports::Timeseries::BaseTimeseriesBuilder
def metrics
return {} if range.blank?
compute_metrics
end
private
def compute_metrics
raise NotImplementedError, 'Subclasses must implement #compute_metrics'
end
end
@@ -0,0 +1,67 @@
class V2::Reports::Conversations::BetterMetricBuilder < V2::Reports::Conversations::BaseReportBuilder
COUNT_METRICS = %i[
conversations_count
incoming_messages_count
outgoing_messages_count
resolutions_count
bot_resolutions_count
bot_handoffs_count
].freeze
AVERAGE_METRICS = %i[
avg_first_response_time
avg_resolution_time
reply_time
].freeze
SUMMARY_METRICS = %i[
conversations_count
outgoing_messages_count
avg_first_response_time
avg_resolution_time
resolutions_count
reply_time
].freeze
BOT_SUMMARY_METRICS = %i[
bot_resolutions_count
bot_handoffs_count
].freeze
AGGREGATOR_CLASSES = [
V2::Reports::Conversations::ConversationsAggregator,
V2::Reports::Conversations::MessagesAggregator,
V2::Reports::Conversations::ReportingEventsAggregator
].freeze
def summary
result = SUMMARY_METRICS.index_with { |metric| fetch_metric(metric) }
result[:incoming_messages_count] = fetch_metric(:incoming_messages_count) unless params[:type] == :agent
result
end
def bot_summary
BOT_SUMMARY_METRICS.index_with { |metric| fetch_metric(metric) }
end
private
def fetch_metric(key)
aggregated_metrics.fetch(key, default_for(key))
end
def aggregated_metrics
@aggregated_metrics ||= AGGREGATOR_CLASSES.each_with_object({}) do |aggregator_class, memo|
memo.merge!(aggregator_class.new(account, params).metrics) do |_metric, existing_value, new_value|
new_value.nil? ? existing_value : new_value
end
end
end
def default_for(metric)
return 0 if COUNT_METRICS.include?(metric)
return nil if AVERAGE_METRICS.include?(metric)
nil
end
end
@@ -0,0 +1,16 @@
class V2::Reports::Conversations::ConversationsAggregator < V2::Reports::Conversations::BaseAggregator
private
def compute_metrics
{
conversations_count: base_relation.count
}
end
def base_relation
scope.conversations
.where(account_id: account.id, created_at: range)
.unscope(:order)
.reorder(nil)
end
end
@@ -0,0 +1,26 @@
class V2::Reports::Conversations::MessagesAggregator < V2::Reports::Conversations::BaseAggregator
private
def compute_metrics
incoming_count, outgoing_count = base_relation.pluck(
Arel.sql(count_sql(Message.message_types[:incoming])),
Arel.sql(count_sql(Message.message_types[:outgoing]))
).first || [0, 0]
{
incoming_messages_count: incoming_count || 0,
outgoing_messages_count: outgoing_count || 0
}
end
def base_relation
scope.messages
.where(account_id: account.id, created_at: range)
.unscope(:order)
.reorder(nil)
end
def count_sql(message_type)
"COUNT(*) FILTER (WHERE message_type = #{message_type})"
end
end
@@ -0,0 +1,61 @@
class V2::Reports::Conversations::ReportingEventsAggregator < V2::Reports::Conversations::BaseAggregator
COUNT_MAPPINGS = {
resolutions_count: :conversation_resolved,
bot_resolutions_count: :conversation_bot_resolved,
bot_handoffs_count: :conversation_bot_handoff
}.freeze
AVERAGE_MAPPINGS = {
avg_first_response_time: :first_response,
avg_resolution_time: :conversation_resolved,
reply_time: :reply_time
}.freeze
private
def compute_metrics
row = base_relation.pluck(*select_fragments).first || default_row
counts_hash(row.first(COUNT_MAPPINGS.size)).merge(averages_hash(row.last(AVERAGE_MAPPINGS.size)))
end
def counts_hash(values)
COUNT_MAPPINGS.keys.zip(values).to_h.transform_values { |value| value || 0 }
end
def averages_hash(values)
AVERAGE_MAPPINGS.keys.zip(values).to_h
end
def select_fragments
COUNT_MAPPINGS.map { |_metric, event_name| count_fragment(event_name) } +
AVERAGE_MAPPINGS.map { |_metric, event_name| average_fragment(event_name) }
end
def count_fragment(event_name)
if event_name == :conversation_bot_handoff
Arel.sql("COUNT(DISTINCT CASE WHEN name = '#{event_name}' THEN conversation_id END)")
else
Arel.sql("COUNT(*) FILTER (WHERE name = '#{event_name}')")
end
end
def average_fragment(event_name)
Arel.sql("AVG(CASE WHEN name = '#{event_name}' THEN #{value_column} END)")
end
def base_relation
scope.reporting_events
.where(account_id: account.id, created_at: range)
.unscope(:order)
.reorder(nil)
end
def value_column
params[:business_hours].present? ? 'value_in_business_hours' : 'value'
end
def default_row
Array.new(COUNT_MAPPINGS.size, 0) + Array.new(AVERAGE_MAPPINGS.size)
end
end
@@ -125,7 +125,7 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
end
def build_summary(method)
builder = V2::Reports::Conversations::MetricBuilder
builder = V2::Reports::Conversations::BetterMetricBuilder
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)