fix: match captain reopens by reopen time to survive inference event ordering

This commit is contained in:
Shivam Mishra
2026-07-01 15:44:47 +05:30
parent f5c6d375ef
commit 563619c95d
2 changed files with 19 additions and 4 deletions
@@ -188,15 +188,17 @@ class Captain::AssistantStatsBuilder
def reopen_rate(range)
resolved_scope = account.reporting_events.where(name: RESOLVED_EVENT_NAMES, created_at: range,
conversation_id: handled_scope(range).select(:conversation_id))
# event_start_time on a reopen is the preceding resolve's timestamp. Join it to the conversation's
# own Captain resolves and keep only reopens at/after one of them, so a human resolve/reopen
# earlier in the same window isn't mistaken for a reopen-after-Captain-resolve.
# event_end_time on a reopen is when it actually reopened. Join it to the conversation's own
# Captain resolves and keep only reopens at/after one of them, so a human resolve/reopen earlier
# in the same window isn't mistaken for a reopen-after-Captain-resolve. (Comparing the reopen's
# start time instead would misfire: the inference event is dispatched just after the generic
# conversation_resolved that seeds event_start_time, so it can land after the reopen's start.)
reopened = account.reporting_events
.where(name: 'conversation_opened')
.where('reporting_events.value > 0')
.joins("INNER JOIN (#{resolved_scope.to_sql}) resolves " \
'ON resolves.conversation_id = reporting_events.conversation_id ' \
'AND reporting_events.event_start_time >= resolves.event_end_time')
'AND reporting_events.event_end_time >= resolves.event_end_time')
.distinct.count('reporting_events.conversation_id')
rate(reopened, resolved_scope.distinct.count(:conversation_id))
end
@@ -120,6 +120,19 @@ RSpec.describe Captain::AssistantStatsBuilder do
expect(described_class.new(assistant, '30').metrics[:reopen_rate][:current]).to eq(0.0)
end
it 'counts an evaluated-path reopen when bot_resolved is skipped and the inference event is newer' do
# Prior human reply => create_bot_resolved_event skips conversation_bot_resolved, so the cohort
# only holds the inference event, which is dispatched a moment after the generic conversation_resolved
# that seeds the reopen's event_start_time. The match must use the reopen's actual reopen time.
create(:reporting_event, account: account, inbox: inbox, conversation: conversation,
name: 'conversation_captain_inference_resolved',
event_start_time: 6.days.ago, event_end_time: 6.days.ago + 1.second)
create(:reporting_event, account: account, inbox: inbox, conversation: conversation,
name: 'conversation_opened', value: 120, event_start_time: 6.days.ago, event_end_time: 3.days.ago)
expect(described_class.new(assistant, '30').metrics[:reopen_rate][:current]).to eq(100.0)
end
it 'counts both inference and time-based bot resolves in the denominator' do
# conversation: inference-resolved and reopened
create(:reporting_event, account: account, inbox: inbox, conversation: conversation,