Merged in fix_plg_email_delivery_timeing_issue (pull request #70)

fix PLG email timing

Approved-by: Seth Call
This commit is contained in:
Nuwan Chaturanga 2025-10-17 13:16:50 +00:00 committed by Seth Call
commit 6a6e4cde09
11 changed files with 95 additions and 57 deletions

View File

@ -3,7 +3,14 @@
execute "ALTER TABLE users ADD COLUMN trial_expires_reminder1_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN trial_expires_reminder2_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN trial_expires_reminder3_sent_at TIMESTAMP"
end
# slide in some more production indexes
execute "CREATE INDEX index_users_on_first_music_session_at ON users USING btree (first_music_session_at)"
# subscription_sync_code
execute "CREATE INDEX index_users_on_subscription_sync_code ON users USING btree (subscription_sync_code)"
# first_certified_gear_at
execute "CREATE INDEX index_users_on_first_certified_gear_at ON users USING btree (first_certified_gear_at)"
end
def self.down
execute "ALTER TABLE users DROP COLUMN trial_expires_reminder1_sent_at"
execute "ALTER TABLE users DROP COLUMN trial_expires_reminder2_sent_at"

View File

@ -436,7 +436,7 @@ module JamRuby
sendgrid_substitute('@USERID', [user.id])
sendgrid_unique_args :type => "profile_complete_reminder2"
mail(:to => user.email, :subject => "Take 2 minutes to fill out your JamKazam profile now") do |format|
mail(:to => user.email, :subject => I18n.t('user_mailer.profile_complete_reminder2.subject')) do |format|
format.text
format.html { render layout: "user_mailer_beta" }
end
@ -448,7 +448,7 @@ module JamRuby
sendgrid_substitute('@USERID', [user.id])
sendgrid_unique_args :type => "profile_complete_reminder3"
mail(:to => user.email, :subject => "Last reminder to update your JamKazam profile") do |format|
mail(:to => user.email, :subject => I18n.t('user_mailer.profile_complete_reminder3.subject')) do |format|
format.text
format.html { render layout: "user_mailer_beta" }
end

View File

@ -4,20 +4,22 @@ module JamRuby
def self.send_reminders
begin
cutoff_date = Date.parse(Rails.application.config.profile_complete_reminders_effective_from_date) # Define a cutoff date for the profile completion emails
#If the user has not updated their profile 1 day after signup, then send reminder email1
reminder1_users.find_each do |user|
reminder1_users(cutoff_date).find_each do |user|
puts "reminder1_users user: #{user.id}"
UserMailer.profile_complete_reminder1(user).deliver_now
User.where(id: user.id).update_all(profile_complete_reminder1_sent_at: Time.now)
end
#If the user has not updated their profile 3 days after signup, then send reminder email2
reminder2_users.find_each do |user|
reminder2_users(cutoff_date).find_each do |user|
UserMailer.profile_complete_reminder2(user).deliver_now
User.where(id: user.id).update_all(profile_complete_reminder2_sent_at: Time.now)
end
#If the user has not updated their profile 5 days after signup, then send reminder email3
reminder3_users.find_each do |user|
reminder3_users(cutoff_date).find_each do |user|
UserMailer.profile_complete_reminder3(user).deliver_now
User.where(id: user.id).update_all(profile_complete_reminder3_sent_at: Time.now)
end
@ -31,18 +33,20 @@ module JamRuby
User.where("users.profile_completed_at IS NULL AND users.subscribe_email = ?", true)
end
def self.reminder1_users
EmailProfileReminder.prospect_users.where("users.created_at < ? AND users.profile_complete_reminder1_sent_at IS NULL", 1.day.ago)
def self.reminder1_users(cutoff_date)
# ensure that the user has had the account for at least one day
puts "reminder1_users cutoff_date: #{cutoff_date}"
EmailProfileReminder.prospect_users.where("users.created_at > ? AND users.profile_complete_reminder1_sent_at IS NULL AND (NOW() - users.created_at) > INTERVAL '1 day'", cutoff_date)
end
def self.reminder2_users
EmailProfileReminder.prospect_users.where("users.created_at < ? AND users.profile_complete_reminder1_sent_at IS NOT NULL AND users.profile_complete_reminder2_sent_at IS NULL", 3.days.ago)
def self.reminder2_users(cutoff_date)
# ensure that the user has had the account for at least three days and guard against rapid back-to-back emails
EmailProfileReminder.prospect_users.where("users.created_at > ? AND users.profile_complete_reminder1_sent_at < ? AND users.profile_complete_reminder1_sent_at IS NOT NULL AND users.profile_complete_reminder2_sent_at IS NULL", cutoff_date, 2.days.ago)
end
def self.reminder3_users
EmailProfileReminder.prospect_users.where("users.created_at < ? AND users.profile_complete_reminder2_sent_at IS NOT NULL AND users.profile_complete_reminder3_sent_at IS NULL", 5.days.ago)
def self.reminder3_users(cutoff_date)
# ensure that user has had the profile for 5 days and guard against rapid back-to-back emails
EmailProfileReminder.prospect_users.where("users.created_at > ? AND users.profile_complete_reminder2_sent_at < ? AND users.profile_complete_reminder2_sent_at IS NOT NULL AND users.profile_complete_reminder3_sent_at IS NULL", cutoff_date, 2.days.ago)
end
end
end

View File

@ -28,19 +28,19 @@ module JamRuby
end
def self.prospect_users
User.where("users.first_certified_gear_at IS NULL")
User.where("users.first_certified_gear_at IS NULL AND users.subscribe_email = ?", true)
end
def self.reminder1_users(cutoff_date)
GearSetupReminder.prospect_users.where("users.created_at < ? AND users.created_at > ? AND users.gear_setup_reminder1_sent_at IS NULL", 1.day.ago, cutoff_date)
GearSetupReminder.prospect_users.where("users.created_at > ? AND users.gear_setup_reminder1_sent_at IS NULL AND (NOW() - users.created_at) > INTERVAL '1 day'", cutoff_date)
end
def self.reminder2_users(cutoff_date)
GearSetupReminder.prospect_users.where("users.created_at < ? AND users.created_at > ? AND users.gear_setup_reminder1_sent_at IS NOT NULL AND users.gear_setup_reminder2_sent_at IS NULL", 3.days.ago, cutoff_date)
GearSetupReminder.prospect_users.where("users.created_at > ? AND users.gear_setup_reminder1_sent_at IS NOT NULL AND users.gear_setup_reminder2_sent_at IS NULL AND users.gear_setup_reminder1_sent_at < ?", cutoff_date, 2.days.ago)
end
def self.reminder3_users(cutoff_date)
GearSetupReminder.prospect_users.where("users.created_at < ? AND users.created_at > ? AND users.gear_setup_reminder2_sent_at IS NOT NULL AND users.gear_setup_reminder3_sent_at IS NULL", 5.days.ago, cutoff_date)
GearSetupReminder.prospect_users.where("users.created_at > ? AND users.gear_setup_reminder2_sent_at IS NOT NULL AND users.gear_setup_reminder3_sent_at IS NULL AND users.gear_setup_reminder2_sent_at < ?", cutoff_date, 2.days.ago)
end
end

View File

@ -36,11 +36,11 @@ module JamRuby
end
def self.reminder2_users(cutoff_date)
GroupSessionReminder.prospect_users.where("users.created_at > ? AND users.group_session_reminder1_sent_at IS NOT NULL AND users.group_session_reminder2_sent_at IS NULL AND users.first_music_session_at < ?", cutoff_date, 3.days.ago)
GroupSessionReminder.prospect_users.where("users.created_at > ? AND users.group_session_reminder1_sent_at IS NOT NULL AND users.group_session_reminder2_sent_at IS NULL AND users.first_music_session_at < ? AND group_session_reminder1_sent_at < ?", cutoff_date, 3.days.ago, 1.day.ago)
end
def self.reminder3_users(cutoff_date)
GroupSessionReminder.prospect_users.where("users.created_at > ? AND users.group_session_reminder2_sent_at IS NOT NULL AND users.group_session_reminder3_sent_at IS NULL AND users.first_music_session_at < ?", cutoff_date, 5.days.ago)
GroupSessionReminder.prospect_users.where("users.created_at > ? AND users.group_session_reminder2_sent_at IS NOT NULL AND users.group_session_reminder3_sent_at IS NULL AND users.first_music_session_at < ? AND group_session_reminder2_sent_at < ?", cutoff_date, 5.days.ago, 1.day.ago)
end
end

View File

@ -27,19 +27,19 @@ module JamRuby
end
def self.prospect_users
User.where("users.first_music_session_at IS NULL")
User.where("users.first_music_session_at IS NULL AND first_certified_gear_at IS NOT NULL")
end
def self.reminder1_users(cutoff_date)
TestGearReminder.prospect_users.where("users.first_certified_gear_at < ? AND users.created_at >= ? AND users.test_gear_reminder1_sent_at IS NULL", 1.day.ago, cutoff_date)
TestGearReminder.prospect_users.where("users.created_at > ? AND users.test_gear_reminder1_sent_at IS NULL AND (NOW() - users.first_certified_gear_at) > INTERVAL '1 day'", cutoff_date)
end
def self.reminder2_users(cutoff_date)
TestGearReminder.prospect_users.where("users.first_certified_gear_at < ? AND users.created_at >= ? AND users.test_gear_reminder1_sent_at IS NOT NULL AND users.test_gear_reminder2_sent_at IS NULL", 3.days.ago, cutoff_date)
TestGearReminder.prospect_users.where("users.created_at > ? AND users.test_gear_reminder1_sent_at IS NOT NULL AND users.test_gear_reminder2_sent_at IS NULL AND users.test_gear_reminder1_sent_at < ?", cutoff_date, 2.days.ago)
end
def self.reminder3_users(cutoff_date)
TestGearReminder.prospect_users.where("users.first_certified_gear_at < ? AND users.created_at > ? AND users.test_gear_reminder2_sent_at IS NOT NULL AND users.test_gear_reminder3_sent_at IS NULL", 5.days.ago, cutoff_date)
TestGearReminder.prospect_users.where("users.created_at > ? AND users.test_gear_reminder2_sent_at IS NOT NULL AND users.test_gear_reminder3_sent_at IS NULL AND users.test_gear_reminder2_sent_at < ?", cutoff_date, 2.days.ago)
end
end
end

View File

@ -8,24 +8,26 @@ module JamRuby
class TrialExpiresReminder
@@log = Logging.logger[TrialExpiresReminder]
FIRST_NOTIFICATION_CHECK = 1.day.ago.to_s
SECOND_NOTIFICATION_CHECK = 5.days.ago.to_s
THIRD_NOTIFICATION_CHECK = 9.days.ago
FIRST_NOTIFICATION_CHECK = 1.day.ago
# unused here, but just as a form of simple documentation...
# SECOND_NOTIFICATION_CHECK = 4.days (from the previous reminder)
# THIRD_NOTIFICATION_CHECK = 4.days (from the previous reminder)
def self.prospect_users(cutoff_date)
User.where("(users.subscription_trial_ends_at IS NOT NULL AND users.subscription_trial_ends_at > ?)", cutoff_date)
User.where("(users.subscription_trial_ends_at IS NOT NULL AND users.created_at > ?)", cutoff_date)
end
# trial_ended | in_trial
def self.reminder1_users(cutoff_date)
prospect_users(cutoff_date).where("users.subscription_last_checked_at < ? AND users.subscription_trial_ends_at < ? AND users.trial_expires_reminder1_sent_at IS NULL", 1.day.ago, FIRST_NOTIFICATION_CHECK)
prospect_users(cutoff_date).where("users.subscription_sync_code = 'trial_ended' AND users.subscription_trial_ends_at < ? AND users.trial_expires_reminder1_sent_at IS NULL", FIRST_NOTIFICATION_CHECK)
end
def self.reminder2_users(cutoff_date)
prospect_users(cutoff_date).where("users.subscription_last_checked_at < ? AND users.subscription_trial_ends_at < ? AND trial_expires_reminder1_sent_at IS NOT NULL AND users.trial_expires_reminder2_sent_at IS NULL", 1.day.ago, SECOND_NOTIFICATION_CHECK)
prospect_users(cutoff_date).where("users.subscription_sync_code = 'trial_ended' AND trial_expires_reminder1_sent_at IS NOT NULL AND users.trial_expires_reminder2_sent_at IS NULL AND (NOW() - trial_expires_reminder1_sent_at) > INTERVAL '4 days'")
end
def self.reminder3_users(cutoff_date)
prospect_users(cutoff_date).where("users.subscription_last_checked_at < ? AND users.subscription_trial_ends_at < ? AND trial_expires_reminder2_sent_at IS NOT NULL AND users.trial_expires_reminder3_sent_at IS NULL", 1.day.ago, THIRD_NOTIFICATION_CHECK)
prospect_users(cutoff_date).where("users.subscription_sync_code = 'trial_ended' AND trial_expires_reminder2_sent_at IS NOT NULL AND users.trial_expires_reminder3_sent_at IS NULL AND (NOW() - trial_expires_reminder2_sent_at) > INTERVAL '4 days'")
end
def self.send_reminders

View File

@ -19,13 +19,19 @@ describe TrialExpiresReminder do
ActionMailer::Base.deliveries.clear
end
def no_more_emails_sent
UserMailer.deliveries.clear
TrialExpiresReminder.send_reminders
expect(ActionMailer::Base.deliveries.count).to eq(0)
end
it "sends reminder emails to users whose trials are about to expire" do
user1.subscription_trial_ends_at = 1.days.from_now
user1.subscription_last_checked_at = 2.days.ago
user1.subscription_sync_code = 'trial_ended'
user1.save!
user2.subscription_trial_ends_at = 1.days.ago
user2.subscription_last_checked_at = 2.days.ago
user2.subscription_trial_ends_at = 2.days.ago
user2.subscription_sync_code = 'trial_ended'
user2.save!
TrialExpiresReminder.send_reminders
@ -36,11 +42,14 @@ describe TrialExpiresReminder do
expect(ActionMailer::Base.deliveries.last.subject).to include("Your free gold trial has expired, but you have great options to keep playing!")
expect(user2.reload.trial_expires_reminder1_sent_at).not_to be_nil
no_more_emails_sent
end
it "does not send reminder emails to users who have already received them" do
user1.subscription_trial_ends_at = 1.days.ago
user1.subscription_last_checked_at = 2.days.ago
user1.reload
user1.subscription_trial_ends_at = 1.days.ago + 1.hour
user1.subscription_sync_code = 'trial_ended'
user1.trial_expires_reminder1_sent_at = Time.now
user1.save!
@ -50,31 +59,38 @@ describe TrialExpiresReminder do
end
it "sends the second reminder email to users whose trials are about to expire" do
user1.subscription_trial_ends_at = 4.days.ago
user1.subscription_last_checked_at = 1.days.ago
user1.trial_expires_reminder1_sent_at = Time.now
user1.reload
user2.reload
# pretend that the first reminder email was sent 2 days ago
user1.subscription_trial_ends_at = 4.days.ago + 1.hour
user1.subscription_sync_code = 'trial_ended'
user1.trial_expires_reminder1_sent_at = 5.days.ago
user1.save!
user2.subscription_trial_ends_at = 5.days.ago
user2.subscription_last_checked_at = 1.days.ago
user2.subscription_trial_ends_at = 4.days.ago + 1.hour
user2.subscription_sync_code = 'trial_ended'
user2.trial_expires_reminder1_sent_at = Time.now
user2.save!
TrialExpiresReminder.send_reminders
expect(ActionMailer::Base.deliveries.count).to eq(1)
expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include(user2.email)
expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include(user1.email)
# Check if the second reminder email was sent by verifying the subject
expect(ActionMailer::Base.deliveries.last.subject).to include("Dont forget to check your options to keep playing")
expect(user2.reload.trial_expires_reminder2_sent_at).not_to be_nil
expect(user1.reload.trial_expires_reminder2_sent_at).not_to be_nil
no_more_emails_sent
end
it "sends the third reminder email to users whose trials are about to expire" do
user1.subscription_trial_ends_at = 10.days.ago
user1.subscription_last_checked_at = 1.days.ago
user1.trial_expires_reminder1_sent_at = 6.days.ago
user1.trial_expires_reminder2_sent_at = 4.days.ago
user1.reload
user1.subscription_trial_ends_at = 10.days.ago + 1.hour
user1.subscription_sync_code = 'trial_ended'
user1.trial_expires_reminder1_sent_at = 8.days.ago
user1.trial_expires_reminder2_sent_at = 9.days.ago
user1.save!
TrialExpiresReminder.send_reminders
@ -85,22 +101,28 @@ describe TrialExpiresReminder do
expect(ActionMailer::Base.deliveries.last.subject).to include("One last reminder!")
expect(user1.reload.trial_expires_reminder3_sent_at).not_to be_nil
no_more_emails_sent
end
it "sends first and second and third reminder emails to users whose trials are about to expire" do
user1.reload
user2.reload
user3.reload
user1.subscription_trial_ends_at = 2.days.ago
user1.subscription_last_checked_at = 1.days.ago
user1.subscription_sync_code = 'trial_ended'
user1.save!
user2.subscription_trial_ends_at = 6.days.ago
user2.subscription_last_checked_at = 1.days.ago
user2.trial_expires_reminder1_sent_at = 4.days.ago
user2.subscription_trial_ends_at = 2.days.ago
user2.trial_expires_reminder1_sent_at = 5.days.ago
user2.subscription_sync_code = 'trial_ended'
user2.save!
user3.subscription_trial_ends_at = 10.days.ago
user3.subscription_last_checked_at = 2.days.ago
user3.trial_expires_reminder1_sent_at = 6.days.ago
user3.trial_expires_reminder2_sent_at = 4.days.ago
user3.subscription_trial_ends_at = 2.days.ago
user3.trial_expires_reminder1_sent_at = 8.days.ago
user3.trial_expires_reminder2_sent_at = 9.days.ago
user3.subscription_sync_code = 'trial_ended'
user3.save!
TrialExpiresReminder.send_reminders
@ -112,5 +134,6 @@ describe TrialExpiresReminder do
expect(ActionMailer::Base.deliveries.count).to eq(3)
expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include(user1.email, user2.email, user3.email)
no_more_emails_sent
end
end

View File

@ -523,6 +523,7 @@ if defined?(Bundler)
config.signup_survey_url = "https://www.surveymonkey.com/r/WVBKLYL"
config.signup_survey_cutoff_date = "2025-06-10"
config.profile_complete_reminders_effective_from_date = "2025-06-10"
config.gear_setup_reminders_effective_from_date = "2025-06-10"
config.test_gear_reminders_effective_from_date = "2025-07-24"
config.group_session_reminders_effective_from_date = "2025-08-12"

View File

@ -128,6 +128,7 @@ SampleApp::Application.configure do
config.send_user_match_mail_only_to_jamkazam_team = false
config.signup_survey_url = "https://www.surveymonkey.com/r/WVBKLYL"
config.signup_survey_cutoff_date = "2025-06-10"
config.profile_complete_reminders_effective_from_date = "2025-06-10"
config.gear_setup_reminders_effective_from_date = "2025-06-10"
config.test_gear_reminders_effective_from_date = "2025-07-24"
config.group_session_reminders_effective_from_date = "2025-08-12"

View File

@ -84,7 +84,7 @@ en:
regards: "Best Regards,"
signature: "JamKazam Team"
profile_complete_reminder2:
subject: "Complete your JamKazam profile"
subject: "Take 2 minutes to fill out your JamKazam profile now"
greeting: "Hello"
paragraph1: "We share your profile with JamKazam members who may be a good fit to play music with you during your first week on the platform via an automated email feature. Dont miss this opportunity to connect. Click the button below to update your profile now, before its shared with others!"
update_profile: "Update Profile"
@ -92,7 +92,7 @@ en:
regards: "Best Regards,"
signature: "JamKazam Team"
profile_complete_reminder3:
subject: "Complete your JamKazam profile"
subject: "Last reminder to update your JamKazam profile"
greeting: "Hello"
paragraph1: "Your profile is your key to connecting with other musicians on JamKazam. It lets others know what instruments you play at what level of proficiency and what kinds of music you like to play. This lets our existing community find and reach out to you to connect and play, and this information also powers our automated matchmaking features that make recommendations to you. If you think you might want to play music live online on JamKazam, please click the button below now to fill out your profile!"
update_profile: "Update Profile"