Enhancement: Move reporting metrics to postgres (#606)

This commit is contained in:
Subin T P
2020-03-18 16:53:35 +05:30
committed by GitHub
parent f69eb7e542
commit 8f6f07177d
27 changed files with 575 additions and 2 deletions

View 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

View 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

View File

@@ -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

View 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

View File

@@ -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'

View 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
View 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

View File

@@ -19,6 +19,8 @@
#
class Inbox < ApplicationRecord
include Reportable
validates :account_id, presence: true
belongs_to :account

View File

@@ -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
#

View File

@@ -43,6 +43,7 @@ class User < ApplicationRecord
include Events::Types
include Pubsubable
include Rails.application.routes.url_helpers
include Reportable
devise :database_authenticatable,
:registerable,