diff --git a/app/services/reporting_events/backfill_service.rb b/app/services/reporting_events/backfill_service.rb index 7f1dd705e..3c3036fb0 100644 --- a/app/services/reporting_events/backfill_service.rb +++ b/app/services/reporting_events/backfill_service.rb @@ -68,7 +68,7 @@ class ReportingEvents::BackfillService def grouped_events(start_utc, end_utc) @account.reporting_events - .where(name: ReportingEvents::MetricRegistry::EVENT_METRICS.keys, created_at: start_utc...end_utc) + .where(name: ReportingEvents::EventMetricRegistry.event_names, created_at: start_utc...end_utc) .group(:name, :user_id, :inbox_id) .pluck(*AGGREGATE_SELECTS) .map { |grouped_row| grouped_event_attributes(grouped_row) } @@ -83,7 +83,7 @@ class ReportingEvents::BackfillService end def accumulate_grouped_aggregates(aggregates, grouped_event) - ReportingEvents::MetricRegistry.event_metrics_for_aggregate( + ReportingEvents::EventMetricRegistry.metrics_for_aggregate( grouped_event[:event_name], count: grouped_event[:count], sum_value: grouped_event[:sum_value], diff --git a/app/services/reporting_events/event_metric_registry.rb b/app/services/reporting_events/event_metric_registry.rb new file mode 100644 index 000000000..1d59ddc47 --- /dev/null +++ b/app/services/reporting_events/event_metric_registry.rb @@ -0,0 +1,79 @@ +module ReportingEvents::EventMetricRegistry + # Describes one rollup metric emitted by a raw reporting event. + # rollup_metric: metric name stored in reporting_events_rollups. + # payload_kind: whether the emitted row carries only a count or a duration payload. + Metric = Data.define(:rollup_metric, :payload_kind) + + EVENTS = { + conversation_resolved: [ + Metric.new(rollup_metric: :resolutions_count, payload_kind: :count), + Metric.new(rollup_metric: :resolution_time, payload_kind: :duration) + ].freeze, + first_response: [ + Metric.new(rollup_metric: :first_response, payload_kind: :duration) + ].freeze, + reply_time: [ + Metric.new(rollup_metric: :reply_time, payload_kind: :duration) + ].freeze, + conversation_bot_resolved: [ + Metric.new(rollup_metric: :bot_resolutions_count, payload_kind: :count) + ].freeze, + conversation_bot_handoff: [ + Metric.new(rollup_metric: :bot_handoffs_count, payload_kind: :count) + ].freeze + }.freeze + + module_function + + def event_names + EVENTS.keys.map(&:to_s) + end + + def metrics_for(event) + return {} if event.blank? + + metrics_for_aggregate( + event.name, + count: 1, + sum_value: event.try(:value), + sum_value_business_hours: event.try(:value_in_business_hours) + ) + end + + def metrics_for_aggregate(event_name, count:, sum_value:, sum_value_business_hours:) + return {} if event_name.blank? + + values = { + count: count.to_i, + sum_value: sum_value.to_f, + sum_value_business_hours: sum_value_business_hours.to_f + } + + EVENTS.fetch(event_name.to_sym, []).to_h do |metric| + [metric.rollup_metric, metric_values(metric.payload_kind, values)] + end + end + + private_class_method def metric_values(payload_kind, values) + case payload_kind + when :count + count_values(values[:count]) + when :duration + duration_values(values) + else + raise ArgumentError, "Unknown metric payload kind: #{payload_kind.inspect}" + end + end + + private_class_method def count_values(count) + { count: count, sum_value: 0, sum_value_business_hours: 0 } + end + + private_class_method def duration_values(values) + { + count: values[:count], + sum_value: values[:sum_value], + sum_value_business_hours: values[:sum_value_business_hours] + } + end +end diff --git a/app/services/reporting_events/rollup_service.rb b/app/services/reporting_events/rollup_service.rb index 9deb03b87..096378d59 100644 --- a/app/services/reporting_events/rollup_service.rb +++ b/app/services/reporting_events/rollup_service.rb @@ -39,7 +39,7 @@ class ReportingEvents::RollupService end def build_rollup_rows - event_metrics = ReportingEvents::MetricRegistry.event_metrics_for(@reporting_event) + event_metrics = ReportingEvents::EventMetricRegistry.metrics_for(@reporting_event) dimensions.each_with_object([]) do |(dimension_type, dimension_id), rows| next if dimension_id.nil? diff --git a/spec/services/reporting_events/event_metric_registry_spec.rb b/spec/services/reporting_events/event_metric_registry_spec.rb new file mode 100644 index 000000000..4469476d2 --- /dev/null +++ b/spec/services/reporting_events/event_metric_registry_spec.rb @@ -0,0 +1,125 @@ +require 'rails_helper' + +RSpec.describe ReportingEvents::EventMetricRegistry do + describe '.event_names' do + it 'returns the supported raw event names' do + expect(described_class.event_names).to eq( + %w[ + conversation_resolved + first_response + reply_time + conversation_bot_resolved + conversation_bot_handoff + ] + ) + end + end + + describe '.metrics_for' do + it 'returns the emitted rollup metrics for conversation_resolved' do + event = instance_double(ReportingEvent, name: 'conversation_resolved', value: 120, value_in_business_hours: 45) + + expect(described_class.metrics_for(event)).to eq( + resolutions_count: { + count: 1, + sum_value: 0, + sum_value_business_hours: 0 + }, + resolution_time: { + count: 1, + sum_value: 120.0, + sum_value_business_hours: 45.0 + } + ) + end + + it 'returns the emitted rollup metrics for first_response' do + event = instance_double(ReportingEvent, name: 'first_response', value: 80, value_in_business_hours: 20) + + expect(described_class.metrics_for(event)).to eq( + first_response: { + count: 1, + sum_value: 80.0, + sum_value_business_hours: 20.0 + } + ) + end + + it 'returns the emitted rollup metrics for reply_time' do + event = instance_double(ReportingEvent, name: 'reply_time', value: 40, value_in_business_hours: 15) + + expect(described_class.metrics_for(event)).to eq( + reply_time: { + count: 1, + sum_value: 40.0, + sum_value_business_hours: 15.0 + } + ) + end + + it 'returns the emitted rollup metrics for conversation_bot_resolved' do + event = instance_double(ReportingEvent, name: 'conversation_bot_resolved') + + expect(described_class.metrics_for(event)).to eq( + bot_resolutions_count: { + count: 1, + sum_value: 0, + sum_value_business_hours: 0 + } + ) + end + + it 'returns the emitted rollup metrics for conversation_bot_handoff' do + event = instance_double(ReportingEvent, name: 'conversation_bot_handoff') + + expect(described_class.metrics_for(event)).to eq( + bot_handoffs_count: { + count: 1, + sum_value: 0, + sum_value_business_hours: 0 + } + ) + end + + it 'returns an empty hash for unsupported events' do + event = instance_double(ReportingEvent, name: 'conversation_created') + + expect(described_class.metrics_for(event)).to eq({}) + end + end + + describe '.metrics_for_aggregate' do + it 'returns aggregated rollup metrics for conversation_resolved groups' do + expect( + described_class.metrics_for_aggregate( + 'conversation_resolved', + count: 3, + sum_value: 420, + sum_value_business_hours: 210 + ) + ).to eq( + resolutions_count: { + count: 3, + sum_value: 0, + sum_value_business_hours: 0 + }, + resolution_time: { + count: 3, + sum_value: 420.0, + sum_value_business_hours: 210.0 + } + ) + end + + it 'returns an empty hash for unsupported grouped events' do + expect( + described_class.metrics_for_aggregate( + 'conversation_created', + count: 2, + sum_value: 100, + sum_value_business_hours: 50 + ) + ).to eq({}) + end + end +end diff --git a/spec/services/reporting_events/metric_registry_spec.rb b/spec/services/reporting_events/metric_registry_spec.rb deleted file mode 100644 index 3c73a4cb8..000000000 --- a/spec/services/reporting_events/metric_registry_spec.rb +++ /dev/null @@ -1,215 +0,0 @@ -require 'rails_helper' - -RSpec.describe ReportingEvents::MetricRegistry do - describe '.event_metrics_for' do - it 'returns the emitted rollup metrics for conversation_resolved' do - event = instance_double(ReportingEvent, name: 'conversation_resolved', value: 120, value_in_business_hours: 45) - - expect(described_class.event_metrics_for(event)).to eq( - resolutions_count: { - count: 1, - sum_value: 0, - sum_value_business_hours: 0 - }, - resolution_time: { - count: 1, - sum_value: 120.0, - sum_value_business_hours: 45.0 - } - ) - end - - it 'returns the emitted rollup metrics for first_response' do - event = instance_double(ReportingEvent, name: 'first_response', value: 80, value_in_business_hours: 20) - - expect(described_class.event_metrics_for(event)).to eq( - first_response: { - count: 1, - sum_value: 80.0, - sum_value_business_hours: 20.0 - } - ) - end - - it 'returns the emitted rollup metrics for reply_time' do - event = instance_double(ReportingEvent, name: 'reply_time', value: 40, value_in_business_hours: 15) - - expect(described_class.event_metrics_for(event)).to eq( - reply_time: { - count: 1, - sum_value: 40.0, - sum_value_business_hours: 15.0 - } - ) - end - - it 'returns the emitted rollup metrics for conversation_bot_resolved' do - event = instance_double(ReportingEvent, name: 'conversation_bot_resolved') - - expect(described_class.event_metrics_for(event)).to eq( - bot_resolutions_count: { - count: 1, - sum_value: 0, - sum_value_business_hours: 0 - } - ) - end - - it 'returns the emitted rollup metrics for conversation_bot_handoff' do - event = instance_double(ReportingEvent, name: 'conversation_bot_handoff') - - expect(described_class.event_metrics_for(event)).to eq( - bot_handoffs_count: { - count: 1, - sum_value: 0, - sum_value_business_hours: 0 - } - ) - end - - it 'returns an empty hash for unsupported events' do - event = instance_double(ReportingEvent, name: 'conversation_created') - - expect(described_class.event_metrics_for(event)).to eq({}) - end - end - - describe '.event_metrics_for_aggregate' do - it 'returns aggregated rollup metrics for conversation_resolved groups' do - expect( - described_class.event_metrics_for_aggregate( - 'conversation_resolved', - count: 3, - sum_value: 420, - sum_value_business_hours: 210 - ) - ).to eq( - resolutions_count: { - count: 3, - sum_value: 0, - sum_value_business_hours: 0 - }, - resolution_time: { - count: 3, - sum_value: 420.0, - sum_value_business_hours: 210.0 - } - ) - end - - it 'returns an empty hash for unsupported grouped events' do - expect( - described_class.event_metrics_for_aggregate( - 'conversation_created', - count: 2, - sum_value: 100, - sum_value_business_hours: 50 - ) - ).to eq({}) - end - end - - describe '.report_metric' do - it 'returns the definition for raw-only count metrics' do - expect(described_class.report_metric(:conversations_count)).to eq( - aggregate: :count - ) - end - - it 'returns the definition for avg_resolution_time' do - expect(described_class.report_metric(:avg_resolution_time)).to eq( - raw_event_name: :conversation_resolved, - rollup_metric: :resolution_time, - aggregate: :average - ) - end - - it 'locks the distinct conversation strategy for bot_handoffs_count' do - expect(described_class.report_metric(:bot_handoffs_count)).to eq( - raw_event_name: :conversation_bot_handoff, - rollup_metric: :bot_handoffs_count, - aggregate: :count, - raw_count_strategy: :distinct_conversation - ) - end - - it 'returns nil for unsupported metrics' do - expect(described_class.report_metric(:unknown_metric)).to be_nil - end - end - - describe '.supported_metric?' do - it 'returns true for supported raw-only metrics' do - expect(described_class.supported_metric?(:conversations_count)).to be(true) - end - - it 'returns false for unsupported metrics' do - expect(described_class.supported_metric?(:unknown_metric)).to be(false) - end - end - - describe '.aggregate_for' do - it 'returns the aggregate type for a supported metric' do - expect(described_class.aggregate_for(:avg_first_response_time)).to eq(:average) - end - end - - describe '.rollup_supported_metric?' do - it 'returns true for rollup-backed metrics' do - expect(described_class.rollup_supported_metric?(:reply_time)).to be(true) - end - - it 'returns false for unsupported metrics' do - expect(described_class.rollup_supported_metric?(:conversations_count)).to be(false) - end - end - - describe '.rollup_metric_for' do - it 'returns the rollup metric name' do - expect(described_class.rollup_metric_for(:avg_first_response_time)).to eq(:first_response) - end - end - - describe '.raw_event_name_for' do - it 'returns the raw event name' do - expect(described_class.raw_event_name_for(:bot_resolutions_count)).to eq(:conversation_bot_resolved) - end - end - - describe '.summary_metrics' do - it 'returns the registry-backed summary metric definitions' do - expect(described_class.summary_metrics).to eq( - [ - { - metric_name: :resolutions_count, - summary_key: :resolved_conversations_count, - aggregate: :count, - raw_event_name: :conversation_resolved, - rollup_metric: :resolutions_count - }, - { - metric_name: :avg_resolution_time, - summary_key: :avg_resolution_time, - aggregate: :average, - raw_event_name: :conversation_resolved, - rollup_metric: :resolution_time - }, - { - metric_name: :avg_first_response_time, - summary_key: :avg_first_response_time, - aggregate: :average, - raw_event_name: :first_response, - rollup_metric: :first_response - }, - { - metric_name: :reply_time, - summary_key: :avg_reply_time, - aggregate: :average, - raw_event_name: :reply_time, - rollup_metric: :reply_time - } - ] - ) - end - end -end