VRFS-1483 email error handling

This commit is contained in:
Jonathan Kolyer 2014-04-05 18:52:12 +00:00
parent 4e95c7cbd5
commit e59d9cbe7a
12 changed files with 232 additions and 107 deletions

View File

@ -70,6 +70,7 @@ gem 'sendgrid', '1.2.0'
gem 'geokit-rails'
gem 'postgres_ext', '1.0.0'
gem 'resque_mailer'
gem 'rest-client'
group :libv8 do
gem 'libv8', "~> 3.11.8"

View File

@ -1,6 +1,6 @@
ActiveAdmin.register JamRuby::EmailBatch, :as => 'Batch Emails' do
menu :label => 'Emails'
menu :label => 'Batch Emails', :parent => 'Email'
config.sort_order = 'updated_at DESC'
config.batch_actions = false
@ -10,40 +10,43 @@ ActiveAdmin.register JamRuby::EmailBatch, :as => 'Batch Emails' do
form :partial => 'form'
index do
column 'Subject' do |pp| pp.subject end
column 'Updated' do |pp| pp.updated_at end
column 'From' do |pp| pp.from_email end
column 'Status' do |pp| pp.aasm_state end
column 'Test Emails' do |pp| pp.test_emails end
column 'Email Count' do |pp| pp.qualified_count end
column 'Send Count' do |pp| pp.sent_count end
column 'Started' do |pp| pp.started_at end
column 'Completed' do |pp| pp.completed_at end
column 'Send Test' do |pp|
link_to("Test Batch (#{pp.test_count})",
batch_test_admin_batch_email_path(pp.id),
:confirm => "Run test batch with #{pp.test_count} emails?")
column 'Subject' do |bb| bb.subject end
column 'Updated' do |bb| bb.updated_at end
column 'From' do |bb| bb.from_email end
column 'Status' do |bb| bb.aasm_state end
column 'Test Emails' do |bb| bb.test_emails end
column 'Email Count' do |bb| bb.candidate_count end
column 'Sent Count' do |bb| bb.sent_count end
column 'Started' do |bb| bb.started_at end
column 'Completed' do |bb| bb.completed_at end
column 'Send Test' do |bb|
bb.can_run_test? ? link_to("Test Batch (#{bb.test_count})",
batch_test_admin_batch_email_path(bb.id),
:confirm => "Run test batch with #{bb.test_count} emails?") : ''
end
column 'Send Live' do |pp|
link_to("Live Batch (#{User.email_opt_in.count})",
batch_send_admin_batch_email_path(pp.id),
:confirm => "Run LIVE batch with #{User.email_opt_in.count} emails?")
column 'Send Live' do |bb|
bb.can_run_batch? ? link_to("Live Batch (#{User.email_opt_in.count})",
batch_send_admin_batch_email_path(bb.id),
:confirm => "Run LIVE batch with #{User.email_opt_in.count} emails?") : ''
end
column 'Clone' do |bb|
link_to("Clone", batch_clone_admin_batch_email_path(bb.id))
end
default_actions
end
action_item :only => :show do
link_to("Send Test Batch (#{resource.test_count})",
batch_test_admin_batch_email_path(resource.id),
:confirm => "Run test batch with #{resource.test_count} emails?")
end
# action_item :only => :show do
# link_to("Send Test Batch (#{resource.test_count})",
# batch_test_admin_batch_email_path(resource.id),
# :confirm => "Run test batch with #{resource.test_count} emails?")
# end
action_item :only => :show do
link_to("Send Live Batch (#{User.email_opt_in.count})",
batch_send_admin_batch_email_path(resource.id),
:confirm => "Run LIVE batch with #{User.email_opt_in.count} emails?")
end
# action_item :only => :show do
# link_to("Send Live Batch (#{User.email_opt_in.count})",
# batch_send_admin_batch_email_path(resource.id),
# :confirm => "Run LIVE batch with #{User.email_opt_in.count} emails?")
# end
show :title => 'Batch Email' do |obj|
panel 'Email Contents' do
@ -59,9 +62,7 @@ ActiveAdmin.register JamRuby::EmailBatch, :as => 'Batch Emails' do
panel 'Sending Parameters' do
attributes_table_for obj do
row 'State' do |obj| obj.aasm_state end
row 'User Count' do |obj|
obj.qualified_count ? obj.qualified_count : User.email_opt_in.count
end
row 'Opt-in Count' do |obj| obj.opting_in_count end
row 'Sent Count' do |obj| obj.sent_count end
row 'Started' do |obj| obj.started_at end
row 'Completed' do |obj| obj.completed_at end
@ -103,4 +104,10 @@ ActiveAdmin.register JamRuby::EmailBatch, :as => 'Batch Emails' do
redirect_to admin_batch_email_path(batch.id)
end
member_action :batch_clone, :method => :get do
batch = EmailBatch.find(params[:id])
batch.clone
redirect_to edit_admin_batch_email_path(batch.id)
end
end

View File

@ -0,0 +1,33 @@
ActiveAdmin.register JamRuby::EmailError, :as => 'Batch Email Errors' do
menu :label => 'Batch Errors', :parent => 'Email'
config.batch_actions = false
config.filters = false
config.clear_action_items!
index do
column 'Batch' do |eerr|
link_to(truncate(eerr.batch_subject, :length => 40), admin_batch_email_path(eerr.email_batch_id))
end
column 'User' do |eerr|
eerr.user ? link_to(eerr.user.name, batch_action_admin_users_path(eerr.user_id)) : 'N/A'
end
column 'Error Type' do |eerr| eerr.error_type end
column 'Email Address' do |eerr| eerr.email_address end
column 'Status' do |eerr| eerr.status end
column 'Reason' do |eerr| eerr.reason end
column 'Email Date' do |eerr| eerr.email_date end
end
controller do
def scoped_collection
@eerrors ||= end_of_association_chain
.where(['email_batch_id IS NOT NULL'])
.includes([:user, :email_batch])
.order('email_date DESC')
end
end
end

View File

@ -1,31 +1,10 @@
-- CREATE TABLE email_batches (
-- id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
-- subject VARCHAR(256) NOT NULL,
-- body TEXT NOT NULL,
-- from_email VARCHAR(64) NOT NULL default 'support@jamkazam.com',
-- aasm_state VARCHAR(32) NOT NULL default 'pending',
-- test_emails TEXT NOT NULL default '',
-- qualified_count INTEGER NOT NULL default 0,
-- sent_count INTEGER NOT NULL default 0,
-- lock_version INTEGER,
-- started_at TIMESTAMP,
-- completed_at TIMESTAMP,
-- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-- );
CREATE TABLE email_batch_sets (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
email_batch_id VARCHAR(64) REFERENCES email_batches(id) ON DELETE CASCADE,
started_at TIMESTAMP,
user_ids TEXT NOT NULL default '',
batch_count INTEGER,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
@ -40,9 +19,13 @@ CREATE TABLE email_errors (
error_type VARCHAR(32),
email_address VARCHAR(256),
status VARCHAR(32),
email_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
reason TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX email_error_fkidx ON email_errors(email_batch_id);
CREATE INDEX email_error_batch_fkidx ON email_errors(email_batch_id);
CREATE INDEX email_error_user_fkidx ON email_errors(user_id);

View File

@ -6,9 +6,9 @@ CREATE TABLE email_batches (
aasm_state VARCHAR(32) NOT NULL default 'pending',
test_emails TEXT NOT NULL default '',
test_emails TEXT NOT NULL default 'test@jamkazam.com',
qualified_count INTEGER NOT NULL default 0,
candidate_count INTEGER NOT NULL default 0,
sent_count INTEGER NOT NULL default 0,
lock_version INTEGER,
@ -20,19 +20,6 @@ CREATE TABLE email_batches (
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CREATE TABLE email_batch_results (
-- id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
-- email_batch_id VARCHAR(64) REFERENCES email_batches(id) ON DELETE CASCADE,
-- user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE,
-- error_type VARCHAR(32),
-- email_address VARCHAR(256),
-- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-- );
-- ALTER TABLE email_batch_results ADD CONSTRAINT email_batch_uniqkey UNIQUE (email_batch_id);
-- ALTER TABLE email_batch_results ADD CONSTRAINT email_user_uniqkey UNIQUE (user_id);
ALTER TABLE users ALTER COLUMN subscribe_email SET DEFAULT true;
UPDATE users SET subscribe_email = true WHERE subscribe_email = false;

View File

@ -44,6 +44,7 @@ gem 'resque_mailer'
gem 'oj'
gem 'builder'
gem 'fog'
gem 'rest-client'
group :test do
gem 'simplecov', '~> 0.7.1'
@ -52,7 +53,6 @@ group :test do
gem "rspec", "2.11"
gem 'spork', '0.9.0'
gem 'database_cleaner', '0.7.0'
gem 'rest-client'
gem 'faker'
gem 'resque_spec'
end

View File

@ -17,6 +17,7 @@ require "postgres_ext"
require 'builder'
require 'cgi'
require 'resque_mailer'
require 'rest-client'
require "jam_ruby/constants/limits"
require "jam_ruby/constants/notification_types"

View File

@ -2,23 +2,26 @@ module JamRuby
class EmailBatch < ActiveRecord::Base
self.table_name = "email_batches"
has_many :email_batch_sets, :class_name => 'JamRuby::EmailBatchSet', :dependent => :destroy
has_many :email_batch_sets, :class_name => 'JamRuby::EmailBatchSet'
has_many :email_errors, :class_name => 'JamRuby::EmailError'
attr_accessible :from_email, :subject, :test_emails, :body
attr_accessible :lock_version, :qualified_count, :sent_count, :started_at, :completed_at
attr_accessible :lock_version, :candidate_count, :sent_count, :started_at, :completed_at
default_scope :order => 'updated_at DESC'
VAR_FIRST_NAME = '@FIRSTNAME'
VAR_LAST_NAME = '@LASTNAME'
DEFAULT_SENDER = "support@jamkazam.com"
BATCH_SIZE = 1000
BODY_TEMPLATE =<<FOO
Hello #{VAR_FIRST_NAME},
<p>Pellentesque facilisis metus ac cursus varius. Nunc laoreet diam mauris, et rhoncus quam commodo vel. Vestibulum nec diam lobortis, posuere sapien id, faucibus nulla. Vivamus vitae pellentesque massa. Proin quis nibh eu nibh imperdiet porttitor. </p>
<p>Paragraph 1 ... newline whitespace is significant for plain text conversions</p>
<p>Vestibulum mollis enim eu fringilla vulputate. Nam tincidunt, enim eget fringilla blandit, mi neque dictum dolor, non pellentesque libero erat sed massa. Morbi sodales lobortis eros, sed feugiat eros euismod eget. Nulla vulputate lobortis porttitor. </p>
<p>Paragraph 2 ... #{VAR_FIRST_NAME} will be replaced by users first name</p>
<p>Thanks for using JamKazam!</p>
@ -32,13 +35,15 @@ FOO
state :tested
state :batching
state :batched
state :confirming
state :confirmed
state :disabled
event :enable do
transitions :from => :disabled, :to => :pending
end
event :reset do
transitions :from => [:disabled, :testing, :tested, :batching, :batched, :pending], :to => :pending
transitions :from => [:confirming, :confirmed, :disabled, :testing, :tested, :batching, :batched, :pending], :to => :pending
end
event :do_test_run, :before => :running_tests do
transitions :from => [:pending, :tested, :batched], :to => :testing
@ -55,21 +60,40 @@ FOO
event :disable do
transitions :from => [:pending, :tested, :batched], :to => :disabled
end
event :do_confirmation do
transitions :from => [:batched, :tested], :to => :confirming
end
event :did_confirmation do
transitions :from => [:confirming], :to => :confirmed
end
end
def self.new(*args)
oo = super
oo.body = BODY_TEMPLATE
oo.test_emails = "test@jamkazam.com, test@example.com"
oo
end
def self.create_with_params(params)
obj = self.new
params.each { |kk,vv| vv.strip! }
params[:body] = BODY_TEMPLATE if params[:body].empty?
obj.update_with_conflict_validation(params)
obj
end
def can_run_batch?
self.tested? || self.pending?
end
def can_run_test?
self.test_emails.present? && (self.tested? || self.pending?)
end
def deliver_batch
self.perform_event('do_batch_run!')
User.email_opt_in.find_in_batches(batch_size: 1000) do |users|
self.email_batch_sets << EmailBatchSet.deliver_set(self, users.map(&:id))
# BatchMailer.send_batch_email(self.id, users.map(&:id)).deliver
User.email_opt_in.find_in_batches(batch_size: BATCH_SIZE) do |users|
self.email_batch_sets << EmailBatchSet.deliver_set(self.id, users.map(&:id))
end
end
@ -90,7 +114,11 @@ FOO
def send_test_batch
self.perform_event('do_test_run!')
self.email_batch_sets << EmailBatchSet.deliver_test(self)
if 'test'==Rails.env
BatchMailer.send_batch_email_test(batch.id).deliver!
else
BatchMailer.send_batch_email_test(batch.id).deliver
end
end
def merged_body(user)
@ -100,7 +128,7 @@ FOO
def did_send(emails)
self.update_with_conflict_validation({ :sent_count => self.sent_count + emails.size })
if self.sent_count >= self.qualified_count
if self.sent_count >= self.candidate_count
if batching?
self.perform_event('did_batch_run!')
elsif testing?
@ -132,14 +160,14 @@ FOO
end
def running_batch
self.update_with_conflict_validation({:qualified_count => User.email_opt_in.count,
self.update_with_conflict_validation({:candidate_count => User.email_opt_in.count,
:sent_count => 0,
:started_at => Time.now
})
end
def running_tests
self.update_with_conflict_validation({:qualified_count => self.test_count,
self.update_with_conflict_validation({:candidate_count => self.test_count,
:sent_count => 0,
:started_at => Time.now
})
@ -147,11 +175,31 @@ FOO
def ran_tests
self.update_with_conflict_validation({ :completed_at => Time.now })
perform_confirmation
end
def ran_batch
self.update_with_conflict_validation({ :completed_at => Time.now })
perform_confirmation
end
def perform_confirmation
self.perform_event('do_confirmation!')
EmailError.confirm_errors(self)
end
def clone
bb = EmailBatch.new
bb.subject = self.subject
bb.body = self.body
bb.from_email = self.from_email
bb.test_emails = self.test_emails
bb.save!
bb
end
def opting_in_count
0 < candidate_count ? candidate_count : User.email_opt_in.count
end
end

View File

@ -4,32 +4,22 @@ module JamRuby
belongs_to :email_batch, :class_name => 'JamRuby::EmailBatch'
def self.deliver_set(batch, user_ids)
BATCH_SIZE = 1000
def self.deliver_set(batch_id, user_ids)
bset = self.new
bset.email_batch = batch
bset.email_batch_id = batch_id
bset.user_ids = user_ids.join(',')
bset.started_at = Time.now
bset.batch_count = user_ids.size
bset.save!
if 'test'==Rails.env
BatchMailer.send_batch_email(self.email_batch_id, user_ids).deliver!
if 'test' == Rails.env
BatchMailer.send_batch_email(bset.email_batch_id, user_ids).deliver!
else
BatchMailer.send_batch_email(self.email_batch_id, user_ids).deliver
BatchMailer.send_batch_email(bset.email_batch_id, user_ids).deliver
end
bset
end
def self.deliver_test(batch)
bset = self.new
bset.email_batch = batch
bset.started_at = Time.now
bset.save!
if 'test'==Rails.env
BatchMailer.send_batch_email_test(batch.id).deliver!
else
BatchMailer.send_batch_email_test(batch.id).deliver
end
bset
end
end
end

View File

@ -1,11 +1,84 @@
module JamRuby
class EmailError < ActiveRecord::Base
self.table_name = "email_batch_errors"
self.table_name = "email_errors"
belongs_to :email_batch, :class_name => 'JamRuby::EmailBatch'
belongs_to :user, :class_name => 'JamRuby::User'
default_scope :order => 'email_date DESC'
ERR_BOUNCE = :bounce
ERR_INVALID = :invalid
SENDGRID_UNAME = 'jamkazam'
SENDGRID_PASSWD = 'jamjamblueberryjam'
def self.sendgrid_url(resource, action='get', params='')
"https://api.sendgrid.com/api/#{resource}.#{action}.json?api_user=#{EmailError::SENDGRID_UNAME}&api_key=#{EmailError::SENDGRID_PASSWD}&date=1&#{params}"
end
def self.bounce_url(batch)
uu = sendgrid_url('bounces')
uu += "&start_date=#{batch.started_at.strftime('%Y-%m-%d')}&end_date=#{batch.completed_at.strftime('%Y-%m-%d')}" if batch.batched?
uu
end
def self.bounce_errors(batch)
uu = self.bounce_url(batch)
response = RestClient.get(uu)
if 200 == response.code
return JSON.parse(response.body).collect do |jj|
ee = EmailError.new
ee.error_type = 'bounces'
ee.email_batch_id = batch.id
ee.email_address = jj['email']
ee.user_id = User.where(:email => ee.email_address).pluck(:id).first
ee.status = jj['status']
ee.email_date = jj['created']
ee.reason = jj['reason']
ee.save!
RestClient.delete(self.sendgrid_url('bounces', 'delete', "email=#{ee.email_address}"))
ee
end
end
end
def self.invalid_url(batch)
uu = sendgrid_url('invalidemails')
uu += "&start_date=#{batch.started_at.strftime('%Y-%m-%d')}&end_date=#{batch.completed_at.strftime('%Y-%m-%d')}" if batch.batched?
uu
end
def self.invalid_errors(batch)
uu = self.invalid_url(batch)
response = RestClient.get(uu)
if 200 == response.code
return JSON.parse(response.body).collect do |jj|
ee = EmailError.new
ee.error_type = 'invalidemails'
ee.email_batch_id = batch.id
ee.email_address = jj['email']
ee.user_id = User.where(:email => ee.email_address).pluck(:id).first
ee.email_date = jj['created']
ee.reason = jj['reason']
ee.save!
uu =
RestClient.delete(self.sendgrid_url('invalidemails', 'delete', "email=#{ee.email_address}"))
ee
end
end
end
def self.collect_errors(batch)
if batch.batched?
EmailError.bounce_errors(batch)
EmailError.invalid_errors(batch)
end
end
def batch_subject
self.email_batch.try(:subject)
end
end
end

View File

@ -75,6 +75,7 @@ gem 'resque_mailer'
gem 'quiet_assets', :group => :development
gem 'bugsnag'
gem 'multi_json', '1.9.0'
gem 'rest_client'
group :development, :test do
gem 'rspec-rails'
@ -131,4 +132,3 @@ group :package do
gem 'fpm'
end

View File

@ -42,9 +42,11 @@ gem 'resque'
gem 'resque-retry'
gem 'resque-failed-job-mailer'
gem 'resque-lonely_job', '~> 1.0.0'
gem 'resque_mailer'
gem 'geokit'
gem 'geokit-rails', '2.0.1'
gem 'mime-types', '1.25.1'
gem 'rest-client'
group :development do
gem 'pry'