From 21f5fcce6a3f4da519d64b875238573f48b05ab5 Mon Sep 17 00:00:00 2001 From: Rodribm10 Date: Sun, 26 Apr 2026 12:46:20 -0300 Subject: [PATCH] =?UTF-8?q?fix(retention):=20cohort=20endpoint=20500=20?= =?UTF-8?q?=E2=80=94=20Pundit=20policy=20+=20SQL=20binding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dois bugs que faziam o cohort retornar 500 e a página de Retenção mostrar "Falha ao carregar cohort": 1. `Captain::AssistantPolicy` não tinha `cohort?` → Pundit batia em NoMethodError no `check_authorization`. Adicionado como leitura pública da assistente, igual `show?`/`playground?`. 2. `RetentionCohortService#cohort_activity` chamava `exec_query(sql, name, [@account.id])` passando array de valores onde a API espera bind objects. A SQL ainda interpolava `account_id = $1` direto na string (sem placeholder ligado). Migrado pra `ActiveRecord::Base.sanitize_sql_array` com `?`, igual ao resto da base. Mantém parametrização e remove acoplamento com posicional. Validado em prod via hot-patch (USR2): GET retention/cohort agora 200 com 3 cohorts. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../app/policies/captain/assistant_policy.rb | 4 ++ .../reports/retention_cohort_service.rb | 72 ++++++++++--------- 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/enterprise/app/policies/captain/assistant_policy.rb b/enterprise/app/policies/captain/assistant_policy.rb index ee35b32f8..263f802f9 100644 --- a/enterprise/app/policies/captain/assistant_policy.rb +++ b/enterprise/app/policies/captain/assistant_policy.rb @@ -34,4 +34,8 @@ class Captain::AssistantPolicy < ApplicationPolicy def revenue? true end + + def cohort? + true + end end diff --git a/enterprise/app/services/captain/reports/retention_cohort_service.rb b/enterprise/app/services/captain/reports/retention_cohort_service.rb index 2eafb7102..18708f64b 100644 --- a/enterprise/app/services/captain/reports/retention_cohort_service.rb +++ b/enterprise/app/services/captain/reports/retention_cohort_service.rb @@ -74,7 +74,7 @@ class Captain::Reports::RetentionCohortService end_month = Time.current.beginning_of_month sql = activity_sql(start_month, end_month) - rows = ActiveRecord::Base.connection.exec_query(sql, 'RetentionCohort', [@account.id]) + rows = ActiveRecord::Base.connection.exec_query(sql, 'RetentionCohort') hash = Hash.new { |h, k| h[k] = {} } @@ -89,37 +89,45 @@ class Captain::Reports::RetentionCohortService # rubocop:disable Metrics/MethodLength def activity_sql(start_month, end_month) - <<~SQL.squish - WITH contact_cohorts AS ( - SELECT id AS contact_id, - DATE_TRUNC('month', first_interaction_at) AS cohort_month - FROM contacts - WHERE first_interaction_at IS NOT NULL - AND first_interaction_at >= '#{start_month.iso8601}' - AND account_id = $1 - ), - contact_activity AS ( - SELECT DISTINCT - conversations.contact_id, - DATE_TRUNC('month', messages.created_at) AS activity_month - FROM messages - INNER JOIN conversations ON conversations.id = messages.conversation_id - WHERE messages.sender_type = 'Contact' - AND messages.private = false - AND (messages.status IS NULL OR messages.status <> #{failed_status_value}) - AND conversations.account_id = $1 - AND messages.created_at >= '#{start_month.iso8601}' - AND messages.created_at < '#{(end_month + 1.month).iso8601}' - ) - SELECT c.cohort_month, - a.activity_month, - COUNT(DISTINCT c.contact_id) AS active_contacts - FROM contact_cohorts c - INNER JOIN contact_activity a ON a.contact_id = c.contact_id - WHERE a.activity_month >= c.cohort_month - GROUP BY c.cohort_month, a.activity_month - ORDER BY c.cohort_month, a.activity_month - SQL + ActiveRecord::Base.sanitize_sql_array([ + <<~SQL.squish, + WITH contact_cohorts AS ( + SELECT id AS contact_id, + DATE_TRUNC('month', first_interaction_at) AS cohort_month + FROM contacts + WHERE first_interaction_at IS NOT NULL + AND first_interaction_at >= ? + AND account_id = ? + ), + contact_activity AS ( + SELECT DISTINCT + conversations.contact_id, + DATE_TRUNC('month', messages.created_at) AS activity_month + FROM messages + INNER JOIN conversations ON conversations.id = messages.conversation_id + WHERE messages.sender_type = 'Contact' + AND messages.private = false + AND (messages.status IS NULL OR messages.status <> ?) + AND conversations.account_id = ? + AND messages.created_at >= ? + AND messages.created_at < ? + ) + SELECT c.cohort_month, + a.activity_month, + COUNT(DISTINCT c.contact_id) AS active_contacts + FROM contact_cohorts c + INNER JOIN contact_activity a ON a.contact_id = c.contact_id + WHERE a.activity_month >= c.cohort_month + GROUP BY c.cohort_month, a.activity_month + ORDER BY c.cohort_month, a.activity_month + SQL + start_month, + @account.id, + failed_status_value, + @account.id, + start_month, + end_month + 1.month + ]) end # rubocop:enable Metrics/MethodLength