First Response Time (FRT) Reporting
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 hoursevent_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).