Enhancement: Move reporting metrics to postgres (#606)
This commit is contained in:
110
app/builders/v2/report_builder.rb
Normal file
110
app/builders/v2/report_builder.rb
Normal file
@@ -0,0 +1,110 @@
|
||||
class V2::ReportBuilder
|
||||
attr_reader :account, :params
|
||||
|
||||
def initialize(account, params)
|
||||
@account = account
|
||||
@params = params
|
||||
end
|
||||
|
||||
def timeseries
|
||||
send(params[:metric])
|
||||
end
|
||||
|
||||
# For backward compatible with old report
|
||||
def build
|
||||
timeseries.each_with_object([]) do |p, arr|
|
||||
arr << { value: p[1], timestamp: p[0].to_time.to_i }
|
||||
end
|
||||
end
|
||||
|
||||
def summary
|
||||
{
|
||||
conversations_count: conversations_count.values.sum,
|
||||
incoming_messages_count: incoming_messages_count.values.sum,
|
||||
outgoing_messages_count: outgoing_messages_count.values.sum,
|
||||
avg_first_response_time: avg_first_response_time_summary,
|
||||
avg_resolution_time: avg_resolution_time_summary,
|
||||
resolutions_count: resolutions_count.values.sum
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scope
|
||||
return account if params[:type].match?('account')
|
||||
return inbox if params[:type].match?('inbox')
|
||||
return user if params[:type].match?('agent')
|
||||
end
|
||||
|
||||
def inbox
|
||||
@inbox ||= account.inboxes.where(id: params[:id]).first
|
||||
end
|
||||
|
||||
def user
|
||||
@user ||= account.users.where(id: params[:id]).first
|
||||
end
|
||||
|
||||
def conversations_count
|
||||
scope.conversations
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
end
|
||||
|
||||
def incoming_messages_count
|
||||
scope.messages.unscoped.incoming
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
end
|
||||
|
||||
def outgoing_messages_count
|
||||
scope.messages.unscoped.outgoing
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
end
|
||||
|
||||
def resolutions_count
|
||||
scope.conversations
|
||||
.resolved
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
end
|
||||
|
||||
def avg_first_response_time
|
||||
scope.events
|
||||
.where(name: 'first_response')
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.average(:value)
|
||||
end
|
||||
|
||||
def avg_resolution_time
|
||||
scope.events.where(name: 'conversation_resolved')
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.average(:value)
|
||||
end
|
||||
|
||||
def range
|
||||
parse_date_time(params[:since])..parse_date_time(params[:until])
|
||||
end
|
||||
|
||||
# Taking average of average is not too accurate
|
||||
# https://en.wikipedia.org/wiki/Simpson's_paradox
|
||||
# TODO: Will optimize this later
|
||||
def avg_resolution_time_summary
|
||||
return 0 if avg_resolution_time.values.empty?
|
||||
|
||||
(avg_resolution_time.values.sum / avg_resolution_time.values.length)
|
||||
end
|
||||
|
||||
def avg_first_response_time_summary
|
||||
return 0 if avg_first_response_time.values.empty?
|
||||
|
||||
(avg_first_response_time.values.sum / avg_first_response_time.values.length)
|
||||
end
|
||||
|
||||
def parse_date_time(datetime)
|
||||
return datetime if datetime.is_a?(DateTime)
|
||||
return datetime.to_datetime if datetime.is_a?(Time) || datetime.is_a?(Date)
|
||||
|
||||
DateTime.strptime(datetime, '%s')
|
||||
end
|
||||
end
|
||||
39
app/controllers/api/v2/accounts/reports_controller.rb
Normal file
39
app/controllers/api/v2/accounts/reports_controller.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
class Api::V2::Accounts::ReportsController < Api::BaseController
|
||||
def account
|
||||
builder = V2::ReportBuilder.new(current_account, account_report_params)
|
||||
data = builder.build
|
||||
render json: data
|
||||
end
|
||||
|
||||
def account_summary
|
||||
render json: account_summary_metrics
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def current_account
|
||||
current_user.account
|
||||
end
|
||||
|
||||
def account_summary_params
|
||||
{
|
||||
type: :account,
|
||||
since: params[:since],
|
||||
until: params[:until]
|
||||
}
|
||||
end
|
||||
|
||||
def account_report_params
|
||||
{
|
||||
metric: params[:metric],
|
||||
type: :account,
|
||||
since: params[:since],
|
||||
until: params[:until]
|
||||
}
|
||||
end
|
||||
|
||||
def account_summary_metrics
|
||||
builder = V2::ReportBuilder.new(current_account, account_summary_params)
|
||||
builder.summary
|
||||
end
|
||||
end
|
||||
@@ -1,11 +1,13 @@
|
||||
class AsyncDispatcher < BaseDispatcher
|
||||
def dispatch(event_name, timestamp, data)
|
||||
event_object = Events::Base.new(event_name, timestamp, data)
|
||||
# TODO: Move this to worker
|
||||
publish(event_object.method_name, event_object)
|
||||
end
|
||||
|
||||
def listeners
|
||||
listeners = [AgentBotListener.instance, EmailNotificationListener.instance, ReportingListener.instance, WebhookListener.instance]
|
||||
listeners << EventListener.instance
|
||||
listeners << SubscriptionListener.instance if ENV['BILLING_ENABLED']
|
||||
listeners
|
||||
end
|
||||
|
||||
31
app/listeners/event_listener.rb
Normal file
31
app/listeners/event_listener.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
class EventListener < BaseListener
|
||||
def conversation_resolved(event)
|
||||
conversation = extract_conversation_and_account(event)[0]
|
||||
time_to_resolve = conversation.updated_at.to_i - conversation.created_at.to_i
|
||||
|
||||
event = Event.new(
|
||||
name: 'conversation_resolved',
|
||||
value: time_to_resolve,
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
user_id: conversation.assignee_id,
|
||||
conversation_id: conversation.id
|
||||
)
|
||||
event.save
|
||||
end
|
||||
|
||||
def first_reply_created(event)
|
||||
message = extract_message_and_account(event)[0]
|
||||
conversation = message.conversation
|
||||
first_response_time = message.created_at.to_i - conversation.created_at.to_i
|
||||
|
||||
event = Event.new(
|
||||
name: 'first_response',
|
||||
value: first_response_time,
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
user_id: conversation.assignee_id
|
||||
)
|
||||
event.save
|
||||
end
|
||||
end
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
class Account < ApplicationRecord
|
||||
include Events::Types
|
||||
include Reportable
|
||||
|
||||
validates :name, presence: true
|
||||
|
||||
@@ -18,6 +19,7 @@ class Account < ApplicationRecord
|
||||
has_many :users, through: :account_users
|
||||
has_many :inboxes, dependent: :destroy
|
||||
has_many :conversations, dependent: :destroy
|
||||
has_many :messages, dependent: :destroy
|
||||
has_many :contacts, dependent: :destroy
|
||||
has_many :facebook_pages, dependent: :destroy, class_name: '::Channel::FacebookPage'
|
||||
has_many :twitter_profiles, dependent: :destroy, class_name: '::Channel::TwitterProfile'
|
||||
|
||||
9
app/models/concerns/reportable.rb
Normal file
9
app/models/concerns/reportable.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Reportable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_many :events, dependent: :destroy
|
||||
end
|
||||
end
|
||||
33
app/models/event.rb
Normal file
33
app/models/event.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: events
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# name :string
|
||||
# value :float
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :integer
|
||||
# conversation_id :integer
|
||||
# inbox_id :integer
|
||||
# user_id :integer
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_events_on_account_id (account_id)
|
||||
# index_events_on_created_at (created_at)
|
||||
# index_events_on_inbox_id (inbox_id)
|
||||
# index_events_on_name (name)
|
||||
# index_events_on_user_id (user_id)
|
||||
#
|
||||
|
||||
class Event < ApplicationRecord
|
||||
validates :account_id, presence: true
|
||||
validates :name, presence: true
|
||||
validates :value, presence: true
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :user, optional: true
|
||||
belongs_to :inbox, optional: true
|
||||
belongs_to :conversation, optional: true
|
||||
end
|
||||
@@ -19,6 +19,8 @@
|
||||
#
|
||||
|
||||
class Inbox < ApplicationRecord
|
||||
include Reportable
|
||||
|
||||
validates :account_id, presence: true
|
||||
|
||||
belongs_to :account
|
||||
|
||||
@@ -20,9 +20,12 @@
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_messages_on_account_id (account_id)
|
||||
# index_messages_on_contact_id (contact_id)
|
||||
# index_messages_on_conversation_id (conversation_id)
|
||||
# index_messages_on_inbox_id (inbox_id)
|
||||
# index_messages_on_source_id (source_id)
|
||||
# index_messages_on_user_id (user_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
|
||||
@@ -43,6 +43,7 @@ class User < ApplicationRecord
|
||||
include Events::Types
|
||||
include Pubsubable
|
||||
include Rails.application.routes.url_helpers
|
||||
include Reportable
|
||||
|
||||
devise :database_authenticatable,
|
||||
:registerable,
|
||||
|
||||
Reference in New Issue
Block a user