feat: label reports overview (#11194)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
105
lib/seeders/reports/conversation_creator.rb
Normal file
105
lib/seeders/reports/conversation_creator.rb
Normal file
@@ -0,0 +1,105 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'faker'
|
||||
require 'active_support/testing/time_helpers'
|
||||
|
||||
class Seeders::Reports::ConversationCreator
|
||||
include ActiveSupport::Testing::TimeHelpers
|
||||
|
||||
def initialize(account:, resources:)
|
||||
@account = account
|
||||
@contacts = resources[:contacts]
|
||||
@inboxes = resources[:inboxes]
|
||||
@teams = resources[:teams]
|
||||
@labels = resources[:labels]
|
||||
@agents = resources[:agents]
|
||||
@priorities = [nil, 'urgent', 'high', 'medium', 'low']
|
||||
end
|
||||
|
||||
def create_conversation(created_at:)
|
||||
conversation = nil
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
travel_to(created_at) do
|
||||
conversation = build_conversation
|
||||
conversation.save!
|
||||
|
||||
add_labels_to_conversation(conversation)
|
||||
create_messages_for_conversation(conversation)
|
||||
resolve_conversation_if_needed(conversation)
|
||||
end
|
||||
|
||||
travel_back
|
||||
end
|
||||
|
||||
conversation
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_conversation
|
||||
contact = @contacts.sample
|
||||
inbox = @inboxes.sample
|
||||
|
||||
contact_inbox = find_or_create_contact_inbox(contact, inbox)
|
||||
assignee = select_assignee(inbox)
|
||||
team = select_team
|
||||
priority = @priorities.sample
|
||||
|
||||
contact_inbox.conversations.new(
|
||||
account: @account,
|
||||
inbox: inbox,
|
||||
contact: contact,
|
||||
assignee: assignee,
|
||||
team: team,
|
||||
priority: priority
|
||||
)
|
||||
end
|
||||
|
||||
def find_or_create_contact_inbox(contact, inbox)
|
||||
inbox.contact_inboxes.find_or_create_by!(
|
||||
contact: contact,
|
||||
source_id: SecureRandom.hex
|
||||
)
|
||||
end
|
||||
|
||||
def select_assignee(inbox)
|
||||
rand(10) < 8 ? inbox.members.sample : nil
|
||||
end
|
||||
|
||||
def select_team
|
||||
rand(10) < 7 ? @teams.sample : nil
|
||||
end
|
||||
|
||||
def add_labels_to_conversation(conversation)
|
||||
labels_to_add = @labels.sample(rand(5..20))
|
||||
conversation.update_labels(labels_to_add.map(&:title))
|
||||
end
|
||||
|
||||
def create_messages_for_conversation(conversation)
|
||||
message_creator = Seeders::Reports::MessageCreator.new(
|
||||
account: @account,
|
||||
agents: @agents,
|
||||
conversation: conversation
|
||||
)
|
||||
message_creator.create_messages
|
||||
end
|
||||
|
||||
def resolve_conversation_if_needed(conversation)
|
||||
return unless rand < 0.7
|
||||
|
||||
resolution_delay = rand((30.minutes)..(24.hours))
|
||||
travel(resolution_delay)
|
||||
conversation.update!(status: :resolved)
|
||||
|
||||
trigger_conversation_resolved_event(conversation)
|
||||
end
|
||||
|
||||
def trigger_conversation_resolved_event(conversation)
|
||||
event_data = { conversation: conversation }
|
||||
|
||||
ReportingEventListener.instance.conversation_resolved(
|
||||
Events::Base.new('conversation_resolved', Time.current, event_data)
|
||||
)
|
||||
end
|
||||
end
|
||||
141
lib/seeders/reports/message_creator.rb
Normal file
141
lib/seeders/reports/message_creator.rb
Normal file
@@ -0,0 +1,141 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'faker'
|
||||
require 'active_support/testing/time_helpers'
|
||||
|
||||
class Seeders::Reports::MessageCreator
|
||||
include ActiveSupport::Testing::TimeHelpers
|
||||
|
||||
MESSAGES_PER_CONVERSATION = 5
|
||||
|
||||
def initialize(account:, agents:, conversation:)
|
||||
@account = account
|
||||
@agents = agents
|
||||
@conversation = conversation
|
||||
end
|
||||
|
||||
def create_messages
|
||||
message_count = rand(MESSAGES_PER_CONVERSATION..MESSAGES_PER_CONVERSATION + 5)
|
||||
first_agent_reply = true
|
||||
|
||||
message_count.times do |i|
|
||||
message = create_single_message(i)
|
||||
first_agent_reply = handle_reply_tracking(message, i, first_agent_reply)
|
||||
end
|
||||
end
|
||||
|
||||
def create_single_message(index)
|
||||
is_incoming = index.even?
|
||||
add_realistic_delay(index, is_incoming) if index.positive?
|
||||
create_message(is_incoming)
|
||||
end
|
||||
|
||||
def handle_reply_tracking(message, index, first_agent_reply)
|
||||
return first_agent_reply if index.even? # Skip incoming messages
|
||||
|
||||
handle_agent_reply_events(message, first_agent_reply)
|
||||
false # No longer first reply after any agent message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_realistic_delay(_message_index, is_incoming)
|
||||
delay = calculate_message_delay(is_incoming)
|
||||
travel(delay)
|
||||
end
|
||||
|
||||
def calculate_message_delay(is_incoming)
|
||||
if is_incoming
|
||||
# Customer response time: 1 minute to 4 hours
|
||||
rand((1.minute)..(4.hours))
|
||||
elsif business_hours_active?(Time.current)
|
||||
# Agent response time varies by business hours
|
||||
rand((30.seconds)..(30.minutes))
|
||||
else
|
||||
rand((1.hour)..(8.hours))
|
||||
end
|
||||
end
|
||||
|
||||
def create_message(is_incoming)
|
||||
if is_incoming
|
||||
create_incoming_message
|
||||
else
|
||||
create_outgoing_message
|
||||
end
|
||||
end
|
||||
|
||||
def create_incoming_message
|
||||
@conversation.messages.create!(
|
||||
account: @account,
|
||||
inbox: @conversation.inbox,
|
||||
message_type: :incoming,
|
||||
content: generate_message_content,
|
||||
sender: @conversation.contact
|
||||
)
|
||||
end
|
||||
|
||||
def create_outgoing_message
|
||||
sender = @conversation.assignee || @agents.sample
|
||||
|
||||
@conversation.messages.create!(
|
||||
account: @account,
|
||||
inbox: @conversation.inbox,
|
||||
message_type: :outgoing,
|
||||
content: generate_message_content,
|
||||
sender: sender
|
||||
)
|
||||
end
|
||||
|
||||
def generate_message_content
|
||||
Faker::Lorem.paragraph(sentence_count: rand(1..5))
|
||||
end
|
||||
|
||||
def handle_agent_reply_events(message, is_first_reply)
|
||||
if is_first_reply
|
||||
trigger_first_reply_event(message)
|
||||
else
|
||||
trigger_reply_event(message)
|
||||
end
|
||||
end
|
||||
|
||||
def business_hours_active?(time)
|
||||
weekday = time.wday
|
||||
hour = time.hour
|
||||
weekday.between?(1, 5) && hour.between?(9, 17)
|
||||
end
|
||||
|
||||
def trigger_first_reply_event(message)
|
||||
event_data = {
|
||||
message: message,
|
||||
conversation: message.conversation
|
||||
}
|
||||
|
||||
ReportingEventListener.instance.first_reply_created(
|
||||
Events::Base.new('first_reply_created', Time.current, event_data)
|
||||
)
|
||||
end
|
||||
|
||||
def trigger_reply_event(message)
|
||||
waiting_since = calculate_waiting_since(message)
|
||||
|
||||
event_data = {
|
||||
message: message,
|
||||
conversation: message.conversation,
|
||||
waiting_since: waiting_since
|
||||
}
|
||||
|
||||
ReportingEventListener.instance.reply_created(
|
||||
Events::Base.new('reply_created', Time.current, event_data)
|
||||
)
|
||||
end
|
||||
|
||||
def calculate_waiting_since(message)
|
||||
last_customer_message = message.conversation.messages
|
||||
.where(message_type: :incoming)
|
||||
.where('created_at < ?', message.created_at)
|
||||
.order(:created_at)
|
||||
.last
|
||||
|
||||
last_customer_message&.created_at || message.conversation.created_at
|
||||
end
|
||||
end
|
||||
234
lib/seeders/reports/report_data_seeder.rb
Normal file
234
lib/seeders/reports/report_data_seeder.rb
Normal file
@@ -0,0 +1,234 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Reports Data Seeder
|
||||
#
|
||||
# Generates realistic test data for performance testing of reports and analytics.
|
||||
# Creates conversations, messages, contacts, agents, teams, and labels with proper
|
||||
# reporting events (first response times, resolution times, etc.) using time travel
|
||||
# to generate historical data with realistic timestamps.
|
||||
#
|
||||
# Usage:
|
||||
# ACCOUNT_ID=1 ENABLE_ACCOUNT_SEEDING=true bundle exec rake db:seed:reports_data
|
||||
#
|
||||
# This will create:
|
||||
# - 1000 conversations with realistic message exchanges
|
||||
# - 100 contacts with realistic profiles
|
||||
# - 20 agents assigned to teams and inboxes
|
||||
# - 5 teams with realistic distribution
|
||||
# - 30 labels with random assignments
|
||||
# - 3 inboxes with agent assignments
|
||||
# - Realistic reporting events with historical timestamps
|
||||
#
|
||||
# Note: This seeder clears existing data for the account before seeding.
|
||||
|
||||
require 'faker'
|
||||
require_relative 'conversation_creator'
|
||||
require_relative 'message_creator'
|
||||
|
||||
# rubocop:disable Rails/Output
|
||||
class Seeders::Reports::ReportDataSeeder
|
||||
include ActiveSupport::Testing::TimeHelpers
|
||||
|
||||
TOTAL_CONVERSATIONS = 1000
|
||||
TOTAL_CONTACTS = 100
|
||||
TOTAL_AGENTS = 20
|
||||
TOTAL_TEAMS = 5
|
||||
TOTAL_LABELS = 30
|
||||
TOTAL_INBOXES = 3
|
||||
MESSAGES_PER_CONVERSATION = 5
|
||||
START_DATE = 3.months.ago # rubocop:disable Rails/RelativeDateConstant
|
||||
END_DATE = Time.current
|
||||
|
||||
def initialize(account:)
|
||||
raise 'Account Seeding is not allowed.' unless ENV.fetch('ENABLE_ACCOUNT_SEEDING', !Rails.env.production?)
|
||||
|
||||
@account = account
|
||||
@teams = []
|
||||
@agents = []
|
||||
@labels = []
|
||||
@inboxes = []
|
||||
@contacts = []
|
||||
end
|
||||
|
||||
def perform!
|
||||
puts "Starting reports data seeding for account: #{@account.name}"
|
||||
|
||||
# Clear existing data
|
||||
clear_existing_data
|
||||
|
||||
create_teams
|
||||
create_agents
|
||||
create_labels
|
||||
create_inboxes
|
||||
create_contacts
|
||||
|
||||
create_conversations
|
||||
|
||||
puts "Completed reports data seeding for account: #{@account.name}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def clear_existing_data
|
||||
puts "Clearing existing data for account: #{@account.id}"
|
||||
@account.teams.destroy_all
|
||||
@account.conversations.destroy_all
|
||||
@account.labels.destroy_all
|
||||
@account.inboxes.destroy_all
|
||||
@account.contacts.destroy_all
|
||||
@account.agents.destroy_all
|
||||
@account.reporting_events.destroy_all
|
||||
end
|
||||
|
||||
def create_teams
|
||||
TOTAL_TEAMS.times do |i|
|
||||
team = @account.teams.create!(
|
||||
name: "#{Faker::Company.industry} Team #{i + 1}"
|
||||
)
|
||||
@teams << team
|
||||
print "\rCreating teams: #{i + 1}/#{TOTAL_TEAMS}"
|
||||
end
|
||||
|
||||
print "\n"
|
||||
end
|
||||
|
||||
def create_agents
|
||||
TOTAL_AGENTS.times do |i|
|
||||
user = create_single_agent(i)
|
||||
assign_agent_to_teams(user)
|
||||
@agents << user
|
||||
print "\rCreating agents: #{i + 1}/#{TOTAL_AGENTS}"
|
||||
end
|
||||
|
||||
print "\n"
|
||||
end
|
||||
|
||||
def create_single_agent(index)
|
||||
random_suffix = SecureRandom.hex(4)
|
||||
user = User.create!(
|
||||
name: Faker::Name.name,
|
||||
email: "agent_#{index + 1}_#{random_suffix}@#{@account.domain || 'example.com'}",
|
||||
password: 'Password1!.',
|
||||
confirmed_at: Time.current
|
||||
)
|
||||
user.skip_confirmation!
|
||||
user.save!
|
||||
|
||||
AccountUser.create!(
|
||||
account_id: @account.id,
|
||||
user_id: user.id,
|
||||
role: :agent
|
||||
)
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
def assign_agent_to_teams(user)
|
||||
teams_to_assign = @teams.sample(rand(1..3))
|
||||
teams_to_assign.each do |team|
|
||||
TeamMember.create!(
|
||||
team_id: team.id,
|
||||
user_id: user.id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def create_labels
|
||||
TOTAL_LABELS.times do |i|
|
||||
label = @account.labels.create!(
|
||||
title: "Label-#{i + 1}-#{Faker::Lorem.word}",
|
||||
description: Faker::Company.catch_phrase,
|
||||
color: Faker::Color.hex_color
|
||||
)
|
||||
@labels << label
|
||||
print "\rCreating labels: #{i + 1}/#{TOTAL_LABELS}"
|
||||
end
|
||||
|
||||
print "\n"
|
||||
end
|
||||
|
||||
def create_inboxes
|
||||
TOTAL_INBOXES.times do |_i|
|
||||
inbox = create_single_inbox
|
||||
assign_agents_to_inbox(inbox)
|
||||
@inboxes << inbox
|
||||
print "\rCreating inboxes: #{@inboxes.size}/#{TOTAL_INBOXES}"
|
||||
end
|
||||
|
||||
print "\n"
|
||||
end
|
||||
|
||||
def create_single_inbox
|
||||
channel = Channel::WebWidget.create!(
|
||||
website_url: "https://#{Faker::Internet.domain_name}",
|
||||
account_id: @account.id
|
||||
)
|
||||
|
||||
@account.inboxes.create!(
|
||||
name: "#{Faker::Company.name} Website",
|
||||
channel: channel
|
||||
)
|
||||
end
|
||||
|
||||
def assign_agents_to_inbox(inbox)
|
||||
agents_to_assign = if @inboxes.empty?
|
||||
# First inbox gets all agents to ensure coverage
|
||||
@agents
|
||||
else
|
||||
# Subsequent inboxes get random selection with some overlap
|
||||
min_agents = [@agents.size / TOTAL_INBOXES, 10].max
|
||||
max_agents = [(@agents.size * 0.8).to_i, 50].min
|
||||
@agents.sample(rand(min_agents..max_agents))
|
||||
end
|
||||
|
||||
agents_to_assign.each do |agent|
|
||||
InboxMember.create!(inbox: inbox, user: agent)
|
||||
end
|
||||
end
|
||||
|
||||
def create_contacts
|
||||
TOTAL_CONTACTS.times do |i|
|
||||
contact = @account.contacts.create!(
|
||||
name: Faker::Name.name,
|
||||
email: Faker::Internet.email,
|
||||
phone_number: Faker::PhoneNumber.cell_phone_in_e164,
|
||||
identifier: SecureRandom.uuid,
|
||||
additional_attributes: {
|
||||
company: Faker::Company.name,
|
||||
city: Faker::Address.city,
|
||||
country: Faker::Address.country,
|
||||
customer_since: Faker::Date.between(from: 2.years.ago, to: Time.zone.today)
|
||||
}
|
||||
)
|
||||
@contacts << contact
|
||||
|
||||
print "\rCreating contacts: #{i + 1}/#{TOTAL_CONTACTS}"
|
||||
end
|
||||
|
||||
print "\n"
|
||||
end
|
||||
|
||||
def create_conversations
|
||||
conversation_creator = Seeders::Reports::ConversationCreator.new(
|
||||
account: @account,
|
||||
resources: {
|
||||
contacts: @contacts,
|
||||
inboxes: @inboxes,
|
||||
teams: @teams,
|
||||
labels: @labels,
|
||||
agents: @agents
|
||||
}
|
||||
)
|
||||
|
||||
TOTAL_CONVERSATIONS.times do |i|
|
||||
created_at = Faker::Time.between(from: START_DATE, to: END_DATE)
|
||||
conversation_creator.create_conversation(created_at: created_at)
|
||||
|
||||
completion_percentage = ((i + 1).to_f / TOTAL_CONVERSATIONS * 100).round
|
||||
print "\rCreating conversations: #{i + 1}/#{TOTAL_CONVERSATIONS} (#{completion_percentage}%)"
|
||||
end
|
||||
|
||||
print "\n"
|
||||
end
|
||||
end
|
||||
# rubocop:enable Rails/Output
|
||||
24
lib/tasks/seed_reports_data.rake
Normal file
24
lib/tasks/seed_reports_data.rake
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace :db do
|
||||
namespace :seed do
|
||||
desc 'Seed test data for reports with conversations, contacts, agents, teams, and realistic reporting events'
|
||||
task reports_data: :environment do
|
||||
if ENV['ACCOUNT_ID'].blank?
|
||||
puts 'Please provide an ACCOUNT_ID environment variable'
|
||||
puts 'Usage: ACCOUNT_ID=1 ENABLE_ACCOUNT_SEEDING=true bundle exec rake db:seed:reports_data'
|
||||
exit 1
|
||||
end
|
||||
|
||||
ENV['ENABLE_ACCOUNT_SEEDING'] = 'true' if ENV['ENABLE_ACCOUNT_SEEDING'].blank?
|
||||
|
||||
account_id = ENV.fetch('ACCOUNT_ID', nil)
|
||||
account = Account.find(account_id)
|
||||
|
||||
puts "Starting reports data seeding for account: #{account.name} (ID: #{account.id})"
|
||||
|
||||
seeder = Seeders::Reports::ReportDataSeeder.new(account: account)
|
||||
seeder.perform!
|
||||
|
||||
puts "Finished seeding reports data for account: #{account.name}"
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user