From 0885feb686a9ec9650ec0688adcc59c7bd8be8b3 Mon Sep 17 00:00:00 2001 From: Mariama Date: Wed, 19 Aug 2026 13:52:13 -0400 Subject: [PATCH] Add first human admin response and seconds-based first response time metrics --- CHANGELOG.md | 7 +++++++ dbt_project.yml | 1 + models/intercom.yml | 7 +++++++ models/intercom__conversation_metrics.sql | 4 +++- ...nt_intercom__conversation_part_aggregates.sql | 16 ++++++++++++++++ 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e40353e..f04eaae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ ## Feature Updates - Adds DuckDB as a supported destination. +# dbt_intercom Unreleased + +## Feature Updates +- Adds `time_to_first_response_sec` to `intercom__conversation_metrics`, computed with the same `dbt.datediff` source as `time_to_first_response_minutes` to preserve sub-minute response times. +- Adds `first_human_admin_response_at` to `int_intercom__conversation_part_aggregates` and passes the field through to `intercom__conversation_metrics`. Supports excluding bot or automated admin author IDs via the `intercom__first_response_excluded_admin_author_ids` project variable (defaults to an empty list). +- Updates `time_to_first_response_minutes` to cast the second-based `dbt.datediff` result to numeric before dividing by 60, preventing sub-60-second responses from truncating to zero on warehouses with integer division. + # dbt_intercom v1.7.0 [PR #84](https://github.com/fivetran/dbt_intercom/pull/84) and [PR #85](https://github.com/fivetran/dbt_intercom/pull/85) includes the following updates: diff --git a/dbt_project.yml b/dbt_project.yml index b59c8bf..b453ab1 100644 --- a/dbt_project.yml +++ b/dbt_project.yml @@ -34,6 +34,7 @@ vars: intercom__contact_history_pass_through_columns: [] intercom__conversation_history_pass_through_columns: [] intercom__article_history_pass_through_columns: [] + intercom__first_response_excluded_admin_author_ids: [] clean-targets: - target - dbt_modules diff --git a/models/intercom.yml b/models/intercom.yml index 0e72724..df8d762 100644 --- a/models/intercom.yml +++ b/models/intercom.yml @@ -361,8 +361,15 @@ models: description: The time difference (not factoring in business hours) between the conversation creation and the first_assignment_at. - name: first_admin_response_at description: The time when the conversation part author of type admin first commented in a conversation. + - name: time_to_first_response_sec + description: The time difference in seconds (not factoring in business hours) between the conversation creation and the first_admin_response_at. - name: time_to_first_response_minutes description: The time difference (not factoring in business hours) between the conversation creation and the first_admin_response_at. + - name: first_human_admin_response_at + description: > + The time when the first admin with a non-null message body commented in the conversation, optionally excluding + admin author IDs listed in the `intercom__first_response_excluded_admin_author_ids` variable. When no IDs are + excluded, this matches `first_admin_response_at`. - name: first_admin_close_at description: The time of the first conversation part where part_type was 'close' by an admin indicating the conversation was closed. - name: first_close_at diff --git a/models/intercom__conversation_metrics.sql b/models/intercom__conversation_metrics.sql index 137f898..98d7d40 100644 --- a/models/intercom__conversation_metrics.sql +++ b/models/intercom__conversation_metrics.sql @@ -19,7 +19,9 @@ final as ( conversation_part_aggregates.first_assignment_at, round(cast(({{ dbt.datediff("conversation_enhanced.conversation_created_at", "conversation_part_aggregates.first_assignment_at", 'second') }}/60) as numeric), 2) as time_to_first_assignment_minutes, conversation_part_aggregates.first_admin_response_at, - round(cast(({{ dbt.datediff("conversation_enhanced.conversation_created_at", "conversation_part_aggregates.first_admin_response_at", 'second') }}/60) as numeric), 2) as time_to_first_response_minutes, + {{ dbt.datediff("conversation_enhanced.conversation_created_at", "conversation_part_aggregates.first_admin_response_at", 'second') }} as time_to_first_response_sec, + round(cast({{ dbt.datediff("conversation_enhanced.conversation_created_at", "conversation_part_aggregates.first_admin_response_at", 'second') }} as numeric) / 60, 2) as time_to_first_response_minutes, + conversation_part_aggregates.first_human_admin_response_at, round(cast(({{ dbt.datediff("conversation_part_aggregates.first_contact_reply_at", "conversation_enhanced.first_admin_close_at", 'second') }} /60) as numeric), 2) as time_to_admin_first_close_minutes, round(cast(({{ dbt.datediff("conversation_part_aggregates.first_contact_reply_at", "conversation_enhanced.first_close_at", 'second') }} /60) as numeric), 2) as time_to_first_close_minutes, conversation_part_aggregates.first_reopen_at, diff --git a/models/intermediate/int_intercom__conversation_part_aggregates.sql b/models/intermediate/int_intercom__conversation_part_aggregates.sql index fb7b342..350262e 100644 --- a/models/intermediate/int_intercom__conversation_part_aggregates.sql +++ b/models/intermediate/int_intercom__conversation_part_aggregates.sql @@ -19,6 +19,22 @@ final as ( min(case when latest_conversation_part.part_type = 'comment' and latest_conversation_part.author_type in ('lead','user') then latest_conversation_part.created_at else null end) as first_contact_reply_at, min(case when latest_conversation_part.part_type like '%assignment%' then latest_conversation_part.created_at else null end) as first_assignment_at, min(case when latest_conversation_part.part_type in ('comment','assignment') and latest_conversation_part.author_type = 'admin' and latest_conversation_part.body is not null then latest_conversation_part.created_at else null end) as first_admin_response_at, + min( + case + when latest_conversation_part.part_type in ('comment','assignment') + and latest_conversation_part.author_type = 'admin' + and latest_conversation_part.body is not null + {% if var('intercom__first_response_excluded_admin_author_ids', []) | length > 0 %} + and latest_conversation_part.author_id not in ( + {%- for admin_author_id in var('intercom__first_response_excluded_admin_author_ids', []) -%} + {{ admin_author_id }}{%- if not loop.last -%},{%- endif -%} + {%- endfor -%} + ) + {% endif %} + then latest_conversation_part.created_at + else null + end + ) as first_human_admin_response_at, min(case when latest_conversation_part.part_type = 'open' then latest_conversation_part.created_at else null end) as first_reopen_at, max(case when latest_conversation_part.part_type like '%assignment%' then latest_conversation_part.created_at else null end) as last_assignment_at, max(case when latest_conversation_part.part_type = 'comment' and latest_conversation_part.author_type in ('lead','user') then latest_conversation_part.created_at else null end) as last_contact_reply_at,