feat: add metric registry mapping events to rollup columns

This commit is contained in:
Shivam Mishra
2026-03-13 10:53:29 +05:30
parent ea5779a712
commit 911f211427
2 changed files with 342 additions and 0 deletions
@@ -0,0 +1,127 @@
# Raw reporting events and rollup rows do not share a single metric namespace; this registry keeps write and read paths aligned.
module ReportingEvents::MetricRegistry
SUMMARY_METRICS = {
resolutions_count: :resolved_conversations_count,
avg_resolution_time: :avg_resolution_time,
avg_first_response_time: :avg_first_response_time,
reply_time: :avg_reply_time
}.freeze
EVENT_METRICS = {
'conversation_resolved' => lambda do |values|
{
resolutions_count: count_metric(values[:count]),
resolution_time: duration_metric(values)
}
end,
'first_response' => ->(values) { { first_response: duration_metric(values) } },
'reply_time' => ->(values) { { reply_time: duration_metric(values) } },
'conversation_bot_resolved' => ->(values) { { bot_resolutions_count: count_metric(values[:count]) } },
'conversation_bot_handoff' => ->(values) { { bot_handoffs_count: count_metric(values[:count]) } }
}.freeze
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
}.freeze
module_function
def event_metrics_for(event)
return {} if event.blank?
return {} unless EVENT_METRICS.key?(event.name.to_s)
event_metrics_for_aggregate(
event.name,
count: 1,
sum_value: event.try(:value),
sum_value_business_hours: event.try(:value_in_business_hours)
)
end
def event_metrics_for_aggregate(event_name, count:, sum_value:, sum_value_business_hours:)
values = {
count: count.to_i,
sum_value: sum_value.to_f,
sum_value_business_hours: sum_value_business_hours.to_f
}
EVENT_METRICS[event_name.to_s]&.call(values) || {}
end
def report_metric(metric)
return if metric.blank?
REPORT_METRICS[metric.to_sym]
end
def supported_metric?(metric)
report_metric(metric).present?
end
def aggregate_for(metric)
report_metric(metric)&.dig(:aggregate)
end
def rollup_supported_metric?(metric)
rollup_metric_for(metric).present?
end
def rollup_metric_for(metric)
report_metric(metric)&.dig(:rollup_metric)
end
def raw_event_name_for(metric)
report_metric(metric)&.dig(:raw_event_name)
end
def summary_metrics
SUMMARY_METRICS.map do |metric_name, summary_key|
report_metric(metric_name).merge(metric_name: metric_name, summary_key: summary_key)
end
end
private_class_method def count_metric(count)
{ count: count, sum_value: 0, sum_value_business_hours: 0 }
end
private_class_method def duration_metric(values)
{
count: values[:count],
sum_value: values[:sum_value],
sum_value_business_hours: values[:sum_value_business_hours]
}
end
end
@@ -0,0 +1,215 @@
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