feat: APIs to filter reports (#2889)

Fixes #2823
This commit is contained in:
Tejaswini Chile 2021-08-27 22:46:32 +05:30 committed by GitHub
parent f94abaef5f
commit 65f3e83afd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 542 additions and 61 deletions

View file

@ -32,9 +32,16 @@ class V2::ReportBuilder
private
def scope
return account if params[:type].match?('account')
return inbox if params[:type].match?('inbox')
return user if params[:type].match?('agent')
case params[:type]
when :account
account
when :inbox
inbox
when :agent
user
when :label
label
end
end
def inbox
@ -45,6 +52,10 @@ class V2::ReportBuilder
@user ||= account.users.where(id: params[:id]).first
end
def label
@label ||= account.labels.where(id: params[:id]).first
end
def conversations_count
scope.conversations
.group_by_day(:created_at, range: range, default_value: 0)

View file

@ -1,14 +1,14 @@
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
before_action :check_authorization
def account
builder = V2::ReportBuilder.new(Current.account, account_report_params)
def index
builder = V2::ReportBuilder.new(Current.account, report_params)
data = builder.build
render json: data
end
def account_summary
render json: account_summary_metrics
def summary
render json: summary_metrics
end
def agents
@ -23,31 +23,39 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
render layout: false, template: 'api/v2/accounts/reports/inboxes.csv.erb', format: 'csv'
end
def labels
response.headers['Content-Type'] = 'text/csv'
response.headers['Content-Disposition'] = 'attachment; filename=labels_report.csv'
render layout: false, template: 'api/v2/accounts/reports/labels.csv.erb', format: 'csv'
end
private
def check_authorization
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
end
def account_summary_params
def summary_params
{
type: :account,
type: params[:type].to_sym,
since: params[:since],
until: params[:until]
until: params[:until],
id: params[:id]
}
end
def account_report_params
def report_params
{
metric: params[:metric],
type: :account,
type: params[:type].to_sym,
since: params[:since],
until: params[:until]
until: params[:until],
id: params[:id]
}
end
def account_summary_metrics
builder = V2::ReportBuilder.new(Current.account, account_summary_params)
def summary_metrics
builder = V2::ReportBuilder.new(Current.account, summary_params)
builder.summary
end
end

View file

@ -7,14 +7,14 @@ class ReportsAPI extends ApiClient {
}
getAccountReports(metric, since, until) {
return axios.get(`${this.url}/account`, {
params: { metric, since, until },
return axios.get(`${this.url}`, {
params: { metric, since, until, type: 'account' },
});
}
getAccountSummary(since, until) {
return axios.get(`${this.url}/account_summary`, {
params: { since, until },
return axios.get(`${this.url}/summary`, {
params: { since, until, type: 'account' },
});
}

View file

@ -23,12 +23,13 @@ describe('#Reports API', () => {
1621621800
);
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/account',
'/api/v2/reports',
{
params: {
metric: 'conversations_count',
since: 1621103400,
until: 1621621800,
type: 'account'
},
}
);
@ -37,11 +38,12 @@ describe('#Reports API', () => {
it('#getAccountSummary', () => {
reportsAPI.getAccountSummary(1621103400, 1621621800);
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/account_summary',
'/api/v2/reports/summary',
{
params: {
since: 1621103400,
until: 1621621800,
type: 'account'
},
}
);

View file

@ -28,4 +28,16 @@ class Label < ApplicationRecord
before_validation do
self.title = title.downcase if attribute_present?('title')
end
def conversations
account.conversations.tagged_with(title)
end
def messages
account.messages.where(conversation_id: conversations.pluck(:id))
end
def events
account.events.where(conversation_id: conversations.pluck(:id))
end
end

View file

@ -0,0 +1,12 @@
<% headers = ['Label Title', 'Conversations count', 'Avg first response time (Minutes)', 'Avg resolution time (Minutes)'] %>
<%= CSV.generate_line headers %>
<% Current.account.labels.each do |label| %>
<% label_report = V2::ReportBuilder.new(Current.account, {
type: :label,
id: label.id,
since: params[:since],
until: params[:until]
}).summary %>
<% row = [ label.title, label_report[:conversations_count], (label_report[:avg_first_response_time]/60).to_i, (label_report[:avg_resolution_time]/60).to_i ] %>
<%= CSV.generate_line row %>
<% end %>

View file

@ -175,12 +175,12 @@ Rails.application.routes.draw do
namespace :v2 do
resources :accounts, only: [], module: :accounts do
resources :reports, only: [] do
resources :reports, only: [:index] do
collection do
get :account
get :account_summary
get :summary
get :agents
get :inboxes
get :labels
end
end
end

View file

@ -5,6 +5,8 @@ describe ::V2::ReportBuilder do
let!(:user) { create(:user, account: account) }
let!(:inbox) { create(:inbox, account: account) }
let(:inbox_member) { create(:inbox_member, user: user, inbox: inbox) }
let!(:label_1) { create(:label, title: 'Label_1', account: account) }
let!(:label_2) { create(:label, title: 'Label_2', account: account) }
# Running jobs inline to calculate the exact metrics
around do |test|
@ -17,35 +19,42 @@ describe ::V2::ReportBuilder do
end
describe '#timeseries' do
context 'when report type is account' do
before do
10.times do
conversation = create(:conversation, account: account,
inbox: inbox, assignee: user,
created_at: Time.zone.today)
create_list(:message, 5, message_type: 'outgoing',
account: account, inbox: inbox,
conversation: conversation, created_at: Time.zone.today + 2.hours)
create_list(:message, 2, message_type: 'incoming',
account: account, inbox: inbox,
conversation: conversation,
created_at: Time.zone.today + 3.hours)
end
5.times do
conversation = create(:conversation, account: account,
inbox: inbox, assignee: user,
created_at: (Time.zone.today - 2.days))
create_list(:message, 3, message_type: 'outgoing',
account: account, inbox: inbox,
conversation: conversation,
created_at: (Time.zone.today - 2.days))
create_list(:message, 1, message_type: 'incoming',
account: account, inbox: inbox,
conversation: conversation,
created_at: (Time.zone.today - 2.days))
end
before do
10.times do
conversation = create(:conversation, account: account,
inbox: inbox, assignee: user,
created_at: Time.zone.today)
create_list(:message, 5, message_type: 'outgoing',
account: account, inbox: inbox,
conversation: conversation, created_at: Time.zone.today + 2.hours)
create_list(:message, 2, message_type: 'incoming',
account: account, inbox: inbox,
conversation: conversation,
created_at: Time.zone.today + 3.hours)
conversation.update_labels('label_1')
conversation.label_list
conversation.save!
end
5.times do
conversation = create(:conversation, account: account,
inbox: inbox, assignee: user,
created_at: (Time.zone.today - 2.days))
create_list(:message, 3, message_type: 'outgoing',
account: account, inbox: inbox,
conversation: conversation,
created_at: (Time.zone.today - 2.days))
create_list(:message, 1, message_type: 'incoming',
account: account, inbox: inbox,
conversation: conversation,
created_at: (Time.zone.today - 2.days))
conversation.update_labels('label_2')
conversation.label_list
conversation.save!
end
end
context 'when report type is account' do
it 'return conversations count' do
params = {
metric: 'conversations_count',
@ -139,5 +148,105 @@ describe ::V2::ReportBuilder do
expect(metrics[:resolutions_count]).to be 0
end
end
context 'when report type is label' do
it 'return conversations count' do
params = {
metric: 'conversations_count',
type: :label,
id: label_2.id,
since: (Time.zone.today - 3.days).to_time.to_i.to_s,
until: Time.zone.today.to_time.to_i.to_s
}
builder = V2::ReportBuilder.new(account, params)
metrics = builder.timeseries
expect(metrics[Time.zone.today - 2.days]).to be 5
end
it 'return incoming messages count' do
params = {
metric: 'incoming_messages_count',
type: :label,
id: label_1.id,
since: (Time.zone.today - 3.days).to_time.to_i.to_s,
until: Time.zone.today.to_time.to_i.to_s
}
builder = V2::ReportBuilder.new(account, params)
metrics = builder.timeseries
expect(metrics[Time.zone.today]).to be 20
expect(metrics[Time.zone.today - 2.days]).to be 5
end
it 'return outgoing messages count' do
params = {
metric: 'outgoing_messages_count',
type: :label,
id: label_1.id,
since: (Time.zone.today - 3.days).to_time.to_i.to_s,
until: Time.zone.today.to_time.to_i.to_s
}
builder = V2::ReportBuilder.new(account, params)
metrics = builder.timeseries
expect(metrics[Time.zone.today]).to be 50
expect(metrics[Time.zone.today - 2.days]).to be 15
end
it 'return resolutions count' do
params = {
metric: 'resolutions_count',
type: :label,
id: label_2.id,
since: (Time.zone.today - 3.days).to_time.to_i.to_s,
until: Time.zone.today.to_time.to_i.to_s
}
conversations = account.conversations.where('created_at < ?', 1.day.ago)
conversations.each(&:resolved!)
builder = V2::ReportBuilder.new(account, params)
metrics = builder.timeseries
expect(metrics[Time.zone.today - 2.days]).to be 5
end
it 'returns average first response time' do
FactoryBot.create(:event, conversation: label_2.conversations.last, account: account, name: 'first_response')
params = {
metric: 'avg_first_response_time',
type: :label,
id: label_2.id,
since: (Time.zone.today - 3.days).to_time.to_i.to_s,
until: Time.zone.today.to_time.to_i.to_s
}
builder = V2::ReportBuilder.new(account, params)
metrics = builder.timeseries
expect(metrics[Time.zone.today].to_f).to be 0.15e1
end
it 'returns summary' do
params = {
type: :label,
id: label_2.id,
since: (Time.zone.today - 3.days).to_time.to_i.to_s,
until: Time.zone.today.to_time.to_i.to_s
}
builder = V2::ReportBuilder.new(account, params)
metrics = builder.summary
expect(metrics[:conversations_count]).to be 5
expect(metrics[:incoming_messages_count]).to be 25
expect(metrics[:outgoing_messages_count]).to be 65
expect(metrics[:avg_resolution_time]).to be 0
expect(metrics[:resolutions_count]).to be 0
end
end
end
end

View file

@ -16,7 +16,7 @@ RSpec.describe 'Reports API', type: :request do
describe 'GET /api/v2/accounts/:account_id/reports/account' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/account"
get "/api/v2/accounts/#{account.id}/reports"
expect(response).to have_http_status(:unauthorized)
end
@ -31,7 +31,7 @@ RSpec.describe 'Reports API', type: :request do
}
it 'returns unauthorized for agents' do
get "/api/v2/accounts/#{account.id}/reports/account",
get "/api/v2/accounts/#{account.id}/reports",
params: params,
headers: agent.create_new_auth_token,
as: :json
@ -40,7 +40,7 @@ RSpec.describe 'Reports API', type: :request do
end
it 'return timeseries metrics' do
get "/api/v2/accounts/#{account.id}/reports/account",
get "/api/v2/accounts/#{account.id}/reports",
params: params,
headers: admin.create_new_auth_token,
as: :json
@ -55,10 +55,10 @@ RSpec.describe 'Reports API', type: :request do
end
end
describe 'GET /api/v2/accounts/:account_id/reports/account_summary' do
describe 'GET /api/v2/accounts/:account_id/reports/summary' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/account_summary"
get "/api/v2/accounts/#{account.id}/reports/summary"
expect(response).to have_http_status(:unauthorized)
end
@ -72,7 +72,7 @@ RSpec.describe 'Reports API', type: :request do
}
it 'returns unauthorized for agents' do
get "/api/v2/accounts/#{account.id}/reports/account_summary",
get "/api/v2/accounts/#{account.id}/reports/summary",
params: params,
headers: agent.create_new_auth_token,
as: :json
@ -81,7 +81,7 @@ RSpec.describe 'Reports API', type: :request do
end
it 'returns summary metrics' do
get "/api/v2/accounts/#{account.id}/reports/account_summary",
get "/api/v2/accounts/#{account.id}/reports/summary",
params: params,
headers: admin.create_new_auth_token,
as: :json
@ -142,7 +142,7 @@ RSpec.describe 'Reports API', type: :request do
until: Time.zone.today.to_time.to_i.to_s
}
it 'returns unauthorized for agents' do
it 'returns unauthorized for inboxes' do
get "/api/v2/accounts/#{account.id}/reports/inboxes",
params: params,
headers: agent.create_new_auth_token
@ -159,4 +159,37 @@ RSpec.describe 'Reports API', type: :request do
end
end
end
describe 'GET /api/v2/accounts/:account_id/reports/labels' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/labels.csv"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
params = {
since: 30.days.ago.to_i.to_s,
until: Time.zone.today.to_time.to_i.to_s
}
it 'returns unauthorized for labels' do
get "/api/v2/accounts/#{account.id}/reports/labels.csv",
params: params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
it 'returns summary' do
get "/api/v2/accounts/#{account.id}/reports/labels.csv",
params: params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
end
end
end
end

View file

@ -140,3 +140,12 @@ extended_message:
- $ref: '#/definitions/generic_id'
- $ref: '#/definitions/message'
- $ref: ./resource/extension/message/with_source_sender.yml
## report list
report:
type: array
description: 'array of conversation count based on date'
items:
allOf:
- $ref: './resource/report.yml'

View file

@ -0,0 +1,6 @@
type: object
properties:
value:
type: number
timestamp:
type: string

View file

@ -63,6 +63,7 @@ x-tagGroups:
- Profile
- Teams
- Custom Filter
- Reports
- name: Public
tags:
- Contacts API

View file

@ -31,6 +31,12 @@ platform_user_id:
custom_filter_id:
$ref: ./custom_filter_id.yml
report_type:
$ref: ./report_type.yml
report_metric:
$ref: ./report_metric.yml
public_inbox_identifier:
$ref: ./public/inbox_identifier.yml

View file

@ -0,0 +1,7 @@
in: query
name: metric
schema:
type: string
enum: [conversations_count, incoming_messages_count, outgoing_messages_count, avg_first_response_time, avg_resolution_time, resolutions_count]
required: true
description: The type of metric

View file

@ -0,0 +1,7 @@
in: query
name: report_type
schema:
type: string
enum: [account,agent,inbox,label]
required: true
description: Type of report

View file

@ -269,7 +269,7 @@ public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversat
delete:
$ref: ./teams/delete.yml
### Custom Filters
### Custom Filters goes here
# Teams
/api/v1/accounts/{account_id}/custom_filters:
@ -296,3 +296,52 @@ public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversat
$ref: ./custom_filters/update.yml
delete:
$ref: ./custom_filters/delete.yml
### Reports
# List
/api/v1/accounts/{id}/reports:
parameters:
- $ref: '#/parameters/account_id'
- $ref: '#/parameters/report_metric'
- $ref: '#/parameters/report_type'
- in: query
name: id
schema:
type: string
description: The Id of specific object in case of agent/inbox/label
- in: query
name: since
schema:
type: string
description: The timestamp from where report should start.
- in: query
name: until
schema:
type: string
description: The timestamp from where report should stop.
get:
$ref: './reports/index.yml'
# Summary
/api/v1/accounts/{id}/reports/summary:
parameters:
- $ref: '#/parameters/account_id'
- $ref: '#/parameters/report_type'
- in: query
name: id
schema:
type: string
description: The Id of specific object in case of agent/inbox/label
- in: query
name: since
schema:
type: string
description: The timestamp from where report should start.
- in: query
name: until
schema:
type: string
description: The timestamp from where report should stop.
get:
$ref: './reports/summary.yml'

View file

@ -0,0 +1,17 @@
tags:
- Reports
operationId: list-all-conversation-statistics
summary: Get Account reports
description: Get Account reports for a specific type, metric and date range
responses:
200:
description: Success
schema:
type: array
description: 'Array of date based conversation statistics'
items:
$ref: '#/definitions/report'
404:
description: reports not found
403:
description: Access denied

View file

@ -0,0 +1,17 @@
tags:
- Reports
operationId: list-all-conversation-statistics-summary
summary: Get Account reports summary
description: Get Account reports summary for a specific type and date range
responses:
200:
description: Success
schema:
type: array
description: 'Array of date based conversation statistics'
items:
$ref: '#/definitions/report'
404:
description: reports not found
403:
description: Access denied

View file

@ -2757,6 +2757,129 @@
}
}
}
},
"/api/v1/accounts/{id}/reports": {
"parameters": [
{
"$ref": "#/parameters/account_id"
},
{
"$ref": "#/parameters/report_metric"
},
{
"$ref": "#/parameters/report_type"
},
{
"in": "query",
"name": "id",
"schema": {
"type": "string"
},
"description": "The Id of specific object in case of agent/inbox/label"
},
{
"in": "query",
"name": "since",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should start."
},
{
"in": "query",
"name": "until",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should stop."
}
],
"get": {
"tags": [
"Reports"
],
"operationId": "list-all-conversation-statistics",
"summary": "Get Account reports",
"description": "Get Account reports for a specific type, metric and date range",
"responses": {
"200": {
"description": "Success",
"schema": {
"type": "array",
"description": "Array of date based conversation statistics",
"items": {
"$ref": "#/definitions/report"
}
}
},
"404": {
"description": "reports not found"
},
"403": {
"description": "Access denied"
}
}
}
},
"/api/v1/accounts/{id}/reports/summary": {
"parameters": [
{
"$ref": "#/parameters/account_id"
},
{
"$ref": "#/parameters/report_type"
},
{
"in": "query",
"name": "id",
"schema": {
"type": "string"
},
"description": "The Id of specific object in case of agent/inbox/label"
},
{
"in": "query",
"name": "since",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should start."
},
{
"in": "query",
"name": "until",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should stop."
}
],
"get": {
"tags": [
"Reports"
],
"operationId": "list-all-conversation-statistics-summary",
"summary": "Get Account reports summary",
"description": "Get Account reports summary for a specific type and date range",
"responses": {
"200": {
"description": "Success",
"schema": {
"type": "array",
"description": "Array of date based conversation statistics",
"items": {
"$ref": "#/definitions/report"
}
}
},
"404": {
"description": "reports not found"
},
"403": {
"description": "Access denied"
}
}
}
}
},
"definitions": {
@ -3844,6 +3967,25 @@
}
}
]
},
"report": {
"type": "array",
"description": "array of conversation count based on date",
"items": {
"allOf": [
{
"type": "object",
"properties": {
"value": {
"type": "number"
},
"timestamp": {
"type": "string"
}
}
}
]
}
}
},
"parameters": {
@ -3952,6 +4094,38 @@
"required": true,
"description": "The numeric ID of the custom filter"
},
"report_type": {
"in": "query",
"name": "report_type",
"schema": {
"type": "string",
"enum": [
"account",
"agent",
"inbox",
"label"
]
},
"required": true,
"description": "Type of report"
},
"report_metric": {
"in": "query",
"name": "metric",
"schema": {
"type": "string",
"enum": [
"conversations_count",
"incoming_messages_count",
"outgoing_messages_count",
"avg_first_response_time",
"avg_resolution_time",
"resolutions_count"
]
},
"required": true,
"description": "The type of metric"
},
"public_inbox_identifier": {
"in": "path",
"name": "inbox_identifier",
@ -3994,7 +4168,8 @@
"Integrations",
"Profile",
"Teams",
"Custom Filter"
"Custom Filter",
"Reports"
]
},
{