diff --git a/app/builders/v2/reports/conversations/base_report_builder.rb b/app/builders/v2/reports/conversations/base_report_builder.rb index 57b115945..ee0155150 100644 --- a/app/builders/v2/reports/conversations/base_report_builder.rb +++ b/app/builders/v2/reports/conversations/base_report_builder.rb @@ -4,7 +4,7 @@ class V2::Reports::Conversations::BaseReportBuilder private def builder_class(metric) - return unless ReportingEvents::MetricRegistry.supported_metric?(metric) + return unless Reports::ReportMetricRegistry.supported?(metric) V2::Reports::Timeseries::ReportBuilder end 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/metric_registry.rb b/app/services/reporting_events/metric_registry.rb index ea208d6cf..7a2712824 100644 --- a/app/services/reporting_events/metric_registry.rb +++ b/app/services/reporting_events/metric_registry.rb @@ -1,5 +1,7 @@ # Raw reporting events and rollup rows do not share a single metric namespace; this registry keeps write and read paths aligned. +# TODO: Split this into separate registries for raw event mappings and report metric definitions. module ReportingEvents::MetricRegistry + # Maps report summary response keys to the metric definitions they read from. SUMMARY_METRICS = { resolutions_count: :resolved_conversations_count, avg_resolution_time: :avg_resolution_time, @@ -7,6 +9,7 @@ module ReportingEvents::MetricRegistry reply_time: :avg_reply_time }.freeze + # Expands each raw reporting event into the rollup metric payloads persisted for aggregation. EVENT_METRICS = { 'conversation_resolved' => lambda do |values| { @@ -20,41 +23,18 @@ module ReportingEvents::MetricRegistry 'conversation_bot_handoff' => ->(values) { { bot_handoffs_count: count_metric(values[:count]) } } }.freeze + # Describes which report metrics are supported and how each one is sourced and aggregated. REPORT_METRICS = { conversations_count: { aggregate: :count }.freeze, incoming_messages_count: { aggregate: :count }.freeze, outgoing_messages_count: { aggregate: :count }.freeze, - avg_first_response_time: { - raw_event_name: :first_response, - rollup_metric: :first_response, - aggregate: :average - }.freeze, - avg_resolution_time: { - raw_event_name: :conversation_resolved, - rollup_metric: :resolution_time, - aggregate: :average - }.freeze, - reply_time: { - raw_event_name: :reply_time, - rollup_metric: :reply_time, - aggregate: :average - }.freeze, - resolutions_count: { - raw_event_name: :conversation_resolved, - rollup_metric: :resolutions_count, - aggregate: :count - }.freeze, - bot_resolutions_count: { - raw_event_name: :conversation_bot_resolved, - rollup_metric: :bot_resolutions_count, - aggregate: :count - }.freeze, - bot_handoffs_count: { - raw_event_name: :conversation_bot_handoff, - rollup_metric: :bot_handoffs_count, - aggregate: :count, - raw_count_strategy: :distinct_conversation - }.freeze + avg_first_response_time: { raw_event_name: :first_response, rollup_metric: :first_response, aggregate: :average }.freeze, + avg_resolution_time: { raw_event_name: :conversation_resolved, rollup_metric: :resolution_time, aggregate: :average }.freeze, + reply_time: { raw_event_name: :reply_time, rollup_metric: :reply_time, aggregate: :average }.freeze, + resolutions_count: { raw_event_name: :conversation_resolved, rollup_metric: :resolutions_count, aggregate: :count }.freeze, + bot_resolutions_count: { raw_event_name: :conversation_bot_resolved, rollup_metric: :bot_resolutions_count, aggregate: :count }.freeze, + bot_handoffs_count: { raw_event_name: :conversation_bot_handoff, rollup_metric: :bot_handoffs_count, aggregate: :count, + raw_count_strategy: :distinct_conversation }.freeze }.freeze module_function 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/app/services/reports/data_source.rb b/app/services/reports/data_source.rb index 7c013295d..0206551d8 100644 --- a/app/services/reports/data_source.rb +++ b/app/services/reports/data_source.rb @@ -49,7 +49,7 @@ class Reports::DataSource end def supported_metric?(metric) - metric.blank? || ReportingEvents::MetricRegistry.rollup_supported_metric?(metric) + metric.blank? || Reports::ReportMetricRegistry.rollup_supported?(metric) end def normalized_timezone_identifier(timezone) @@ -73,11 +73,11 @@ class Reports::DataSource private def report_metric - @report_metric ||= ReportingEvents::MetricRegistry.report_metric(metric) + @report_metric ||= Reports::ReportMetricRegistry.fetch(metric) end def average_metric? - report_metric&.dig(:aggregate) == :average + report_metric&.average? end def count_metric? @@ -85,19 +85,19 @@ class Reports::DataSource end def rollup_metric - report_metric&.dig(:rollup_metric) + report_metric&.rollup_metric end def raw_event_name - report_metric&.dig(:raw_event_name) + report_metric&.raw_event_name end def raw_count_strategy - report_metric&.dig(:raw_count_strategy) + report_metric&.raw_count_strategy end def summary_metrics - @summary_metrics ||= ReportingEvents::MetricRegistry.summary_metrics + @summary_metrics ||= Reports::ReportMetricRegistry.summary_metrics end def use_business_hours? diff --git a/app/services/reports/raw_data_source.rb b/app/services/reports/raw_data_source.rb index 5abcd7176..f37b3c454 100644 --- a/app/services/reports/raw_data_source.rb +++ b/app/services/reports/raw_data_source.rb @@ -114,17 +114,17 @@ class Reports::RawDataSource < Reports::DataSource end def summary_select_field(definition) - if definition[:aggregate] == :count - "COUNT(CASE WHEN name = '#{definition[:raw_event_name]}' THEN 1 END) as #{definition[:summary_key]}" + if definition.count? + "COUNT(CASE WHEN name = '#{definition.raw_event_name}' THEN 1 END) as #{definition.summary_key}" else - "AVG(CASE WHEN name = '#{definition[:raw_event_name]}' THEN #{average_value_key} END) as #{definition[:summary_key]}" + "AVG(CASE WHEN name = '#{definition.raw_event_name}' THEN #{average_value_key} END) as #{definition.summary_key}" end end def summary_attributes_for(record, conversations_count = 0) summary_metrics.each_with_object({ conversations_count: conversations_count.to_i }) do |definition, attributes| - value = record&.public_send(definition[:summary_key]) - attributes[definition[:summary_key]] = definition[:aggregate] == :count ? value.to_i : value + value = record&.public_send(definition.summary_key) + attributes[definition.summary_key] = definition.count? ? value.to_i : value end end diff --git a/app/services/reports/report_metric_registry.rb b/app/services/reports/report_metric_registry.rb new file mode 100644 index 000000000..aef62f32c --- /dev/null +++ b/app/services/reports/report_metric_registry.rb @@ -0,0 +1,120 @@ +module Reports::ReportMetricRegistry + # Describes one public report metric. + # name: API-facing metric name requested by reports. + # aggregate: whether the metric is a count or average. + # raw_event_name: source reporting_events name for raw queries. + # rollup_metric: source reporting_events_rollups metric for rollup queries. + # summary_key: key used when this metric appears in grouped summary responses. + # raw_count_strategy: optional raw-query counting rule, such as distinct conversations. + Metric = Data.define( + :name, + :aggregate, + :raw_event_name, + :rollup_metric, + :summary_key, + :raw_count_strategy + ) do + def initialize(name:, aggregate:, raw_event_name: nil, rollup_metric: nil, summary_key: nil, raw_count_strategy: nil) + super + end + + def average? + aggregate == :average + end + + def count? + aggregate == :count + end + + def rollup_supported? + rollup_metric.present? + end + + def summary? + summary_key.present? + end + end + + METRICS = { + conversations_count: Metric.new( + name: :conversations_count, + aggregate: :count + ), + incoming_messages_count: Metric.new( + name: :incoming_messages_count, + aggregate: :count + ), + outgoing_messages_count: Metric.new( + name: :outgoing_messages_count, + aggregate: :count + ), + avg_first_response_time: Metric.new( + name: :avg_first_response_time, + aggregate: :average, + raw_event_name: :first_response, + rollup_metric: :first_response, + summary_key: :avg_first_response_time + ), + avg_resolution_time: Metric.new( + name: :avg_resolution_time, + aggregate: :average, + raw_event_name: :conversation_resolved, + rollup_metric: :resolution_time, + summary_key: :avg_resolution_time + ), + reply_time: Metric.new( + name: :reply_time, + aggregate: :average, + raw_event_name: :reply_time, + rollup_metric: :reply_time, + summary_key: :avg_reply_time + ), + resolutions_count: Metric.new( + name: :resolutions_count, + aggregate: :count, + raw_event_name: :conversation_resolved, + rollup_metric: :resolutions_count, + summary_key: :resolved_conversations_count + ), + bot_resolutions_count: Metric.new( + name: :bot_resolutions_count, + aggregate: :count, + raw_event_name: :conversation_bot_resolved, + rollup_metric: :bot_resolutions_count + ), + bot_handoffs_count: Metric.new( + name: :bot_handoffs_count, + aggregate: :count, + raw_event_name: :conversation_bot_handoff, + rollup_metric: :bot_handoffs_count, + raw_count_strategy: :distinct_conversation + ) + }.freeze + + SUMMARY_METRIC_NAMES = %i[ + resolutions_count + avg_resolution_time + avg_first_response_time + reply_time + ].freeze + + module_function + + def fetch(name) + return if name.blank? + + METRICS[name.to_sym] + end + + def supported?(name) + fetch(name).present? + end + + def rollup_supported?(name) + fetch(name)&.rollup_supported? || false + end + + def summary_metrics + SUMMARY_METRIC_NAMES.map { |metric_name| METRICS.fetch(metric_name) } + end +end 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 diff --git a/spec/services/reports/report_metric_registry_spec.rb b/spec/services/reports/report_metric_registry_spec.rb new file mode 100644 index 000000000..33fcb41e5 --- /dev/null +++ b/spec/services/reports/report_metric_registry_spec.rb @@ -0,0 +1,74 @@ +require 'rails_helper' + +RSpec.describe Reports::ReportMetricRegistry do + describe '.fetch' do + it 'returns the definition for raw-only count metrics' do + metric = described_class.fetch(:conversations_count) + + expect(metric.name).to eq(:conversations_count) + expect(metric.count?).to be(true) + expect(metric.rollup_supported?).to be(false) + expect(metric.raw_event_name).to be_nil + end + + it 'returns the definition for avg_resolution_time' do + metric = described_class.fetch(:avg_resolution_time) + + expect(metric.name).to eq(:avg_resolution_time) + expect(metric.average?).to be(true) + expect(metric.raw_event_name).to eq(:conversation_resolved) + expect(metric.rollup_metric).to eq(:resolution_time) + expect(metric.summary_key).to eq(:avg_resolution_time) + end + + it 'locks the distinct conversation strategy for bot_handoffs_count' do + metric = described_class.fetch(:bot_handoffs_count) + + expect(metric.count?).to be(true) + expect(metric.raw_event_name).to eq(:conversation_bot_handoff) + expect(metric.rollup_metric).to eq(:bot_handoffs_count) + expect(metric.raw_count_strategy).to eq(:distinct_conversation) + end + + it 'returns nil for unsupported metrics' do + expect(described_class.fetch(:unknown_metric)).to be_nil + end + end + + describe '.supported?' do + it 'returns true for supported raw-only metrics' do + expect(described_class.supported?(:conversations_count)).to be(true) + end + + it 'returns false for unsupported metrics' do + expect(described_class.supported?(:unknown_metric)).to be(false) + end + end + + describe '.rollup_supported?' do + it 'returns true for rollup-backed metrics' do + expect(described_class.rollup_supported?(:reply_time)).to be(true) + end + + it 'returns false for raw-only metrics' do + expect(described_class.rollup_supported?(:conversations_count)).to be(false) + end + end + + describe '.summary_metrics' do + it 'returns the summary metric definitions in registry order' do + expect( + described_class.summary_metrics.map do |metric| + [metric.name, metric.summary_key, metric.aggregate, metric.raw_event_name, metric.rollup_metric] + end + ).to eq( + [ + [:resolutions_count, :resolved_conversations_count, :count, :conversation_resolved, :resolutions_count], + [:avg_resolution_time, :avg_resolution_time, :average, :conversation_resolved, :resolution_time], + [:avg_first_response_time, :avg_first_response_time, :average, :first_response, :first_response], + [:reply_time, :avg_reply_time, :average, :reply_time, :reply_time] + ] + ) + end + end +end