feat: add metric registry mapping events to rollup columns
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user