First Response Time (FRT) Reporting

What is FRT

First Response Time measures how long a customer waited before receiving the first human agent reply in a conversation cycle. It is stored as a reporting_event record with name = 'first_response' and used by all reporting endpoints.


Data Flow Overview

Customer message arrives
       ↓
conversation.waiting_since set (on create) or first_incoming_at resolved
       ↓
Agent sends first qualifying reply
       ↓
Message#valid_first_reply? → true
       ↓
REPLY_CREATED event dispatched
       ↓
ReportingEventListener#first_reply_created
       ↓
frt_start_time(conversation) computed
       ↓
ReportingEvent created:
  name:                   'first_response'
  value:                  reply.created_at - frt_start_time   (seconds)
  value_in_business_hours: business_hours(inbox, start, end)  (seconds)
  event_start_time:       frt_start_time
  event_end_time:         reply.created_at
  user_id:                replying agent
       ↓
Reports read: AVG(value) or AVG(value_in_business_hours)

What Qualifies as a First Reply

Message#valid_first_reply? returns true only when all of the following hold:

Condition Why
Message is outgoing Must be an agent reply
human_response? — sender is a User, not a bot or automation rule or campaign Excludes automated messages
Not private (note) Notes are internal, not visible to customer
conversation.first_reply_created_at is nil Only counted once per conversation cycle
conversation.first_incoming_at is present Proactive agent-first conversations are excluded
No earlier public agent reply exists after first_incoming_at Only the very first qualifying reply counts

Message#earlier_public_reply_after?(since) checks for any prior outgoing, non-bot, non-private, non-campaign message since since.


FRT Start Time — All Cases

The start time is computed by ReportingEventListener#frt_start_time.

Case 1 — Standard (any channel, flag off)

frt_start_time = conversation.first_incoming_at

first_incoming_at is the created_at of the customer’s first incoming message in the current cycle (respects reopen window — see Case 4).

Meaning: FRT measures exactly how long the customer waited from the moment they wrote their first message.


Case 2 — frt_from_conversation_created = true on the inbox

frt_start_time = MAX(conversation.created_at, reopen_time)

Enabled per-inbox via inboxes.frt_from_conversation_created (boolean, default false). Available for all channel types except Call.

Meaning: FRT starts from when the conversation was created, not when the customer first wrote. Useful when the channel creates conversations proactively (e.g. outbound email threads, ticket imports) and the SLA clock should start from creation.

Previously this flag existed only on channel_email.frt_from_conversation_created. It was migrated to inboxes.frt_from_conversation_created so that any channel type can use it. The data migration (20260609120000) copies existing true values from channel_email to the inbox record.


Case 3 — Bot handoff (any channel, flag off)

frt_start_time = MAX(first_incoming_at, bot_handoff_end_at, reopen_time)

bot_handoff_end_at is the event_end_time of the last conversation_bot_handoff ReportingEvent for the conversation.

Meaning: If a bot handled the customer first, FRT does not count the bot-interaction time. The clock starts from when the human agent took over.

When flag is on + bot handoff: flag takes precedence — frt_start_time = conversation.created_at (or reopen_time if later). Bot handoff end time is not considered.


Case 4 — Reopened conversation

The additional_attributes['reopen_time'] timestamp is set when a resolved conversation is reopened.

first_incoming_at filters messages to those created at or after reopen_time - 5 seconds, so only messages in the new cycle are considered.

Flag off:

frt_start_time = MAX(first_incoming_at_after_reopen, bot_handoff_end_at, reopen_time)

Flag on:

frt_start_time = MAX(conversation.created_at, reopen_time)
                 = reopen_time   (because reopen_time > created_at)

Meaning: After a reopen, FRT restarts. With the flag on, the second cycle’s FRT is measured from the reopen timestamp.


Case 5 — Agent-first (proactive) conversation

If the agent sends the first message before any customer reply, conversation.first_incoming_at is nil. valid_first_reply? returns false for all agent messages until the customer writes. Once the customer replies, the next human agent message triggers the first reply event, and FRT is calculated from first_incoming_at (the customer’s message).

No first_response ReportingEvent is created for the proactive message itself. This applies regardless of the channel type or flag setting.


Case 6 — Call channel

FRT tracking is not applicable to call inboxes. The FRT configuration toggle is hidden in the UI (v-if="!isCallInbox"). The frt_from_conversation_created flag defaults to false and is not exposed via the settings UI for call channels. Call conversations may technically emit a first.reply.created event if an agent sends a text message, but this is incidental — FRT for call channels is not a supported metric.


Summary Table

Channel type Flag off Flag on
Any (API, WhatsApp, web widget, email, etc.) FRT starts from customer’s first message FRT starts from conversation created_at (or reopen_time if later)
Any with bot handoff FRT starts from bot handoff end time FRT starts from conversation created_at
Any — reopened FRT restarts; starts from first message after reopen FRT restarts; starts from reopen_time
Any — agent-first No FRT recorded until customer writes Same — no FRT until customer writes
Call Not tracked Flag not exposed in UI; not applicable

Key Files

File Role
app/listeners/reporting_event_listener.rb Writes first_response ReportingEvent; contains frt_start_time and bot_handoff_end_at
app/models/message.rb valid_first_reply?, earlier_public_reply_after?, human_response?
app/models/conversation.rb first_incoming_at — anchor for FRT start in standard mode
app/helpers/report_helper_v2.rb Reads reporting_events for report queries
app/builders/v2/report_builder.rb Aggregates FRT into averages for API responses
app/services/v2/summary_detailed_processor.rb Per-conversation FRT rows for detailed report
db/migrate/20260609120000_add_frt_from_conversation_created_to_inboxes.rb Adds inboxes.frt_from_conversation_created; migrates existing email channel values

What Reports Read

Reports never re-derive FRT from message timestamps. They always read pre-computed values from reporting_events:

  • value — FRT in seconds (wall clock)
  • value_in_business_hours — FRT in seconds, counting only inbox working hours
  • event_start_time — the resolved FRT start timestamp (useful for audit/debugging)
  • user_id — the agent who gave the first reply

Aggregation: AVG(value) or AVG(value_in_business_hours) grouped by day/week/month or per-inbox/agent/team/label.

conversation.first_reply_created_at is not used in FRT metric queries — it is only used to count “unattended conversations” (conversations with no first reply yet).