docbrown/app/services/email_digest_article_collector.rb
Jamie Gaskins 5a0a46887c
Instrument EmailDigestArticleCollector (#13153)
* Instrument EmailDigestArticleCollector

On DEV, since we skip this with sidekiq-cron and instead process them
inline using Heroku Scheduler (which is not instrumented), we miss out
on all instrumentation for this process and have no idea how long it
takes to process each user.

With this instrumentation in place, we can see how long it takes to
process each user and aggregate them to see how long it takes to process
all users.

This commit also adds an Instrumentation mixin that we can use to
instrument blocks easily:

    instrument "MyClass.my_method", tags: { article: article.id } do |span|
      # ...
    end

* Add Instrumentation mixin

Oops, completely forgot to commit this file
2021-03-29 09:33:32 -04:00

98 lines
3.1 KiB
Ruby

class EmailDigestArticleCollector
include Instrumentation
ARTICLES_TO_SEND = "EmailDigestArticleCollector#articles_to_send".freeze
def initialize(user)
@user = user
end
def articles_to_send
# rubocop:disable Metrics/BlockLength
instrument ARTICLES_TO_SEND, tags: %W[user_id:#{@user.id}] do
return [] unless should_receive_email?
articles = if user_has_followings?
experience_level_rating = (@user.experience_level || 5)
experience_level_rating_min = experience_level_rating - 3.6
experience_level_rating_max = experience_level_rating + 3.6
@user.followed_articles
.select(:title, :description, :path)
.published
.where("published_at > ?", cutoff_date)
.where(email_digest_eligible: true)
.where.not(user_id: @user.id)
.where("score > ?", 12)
.where("experience_level_rating > ? AND experience_level_rating < ?",
experience_level_rating_min, experience_level_rating_max)
.order(score: :desc)
.limit(6)
else
Article.select(:title, :description, :path)
.published
.where("published_at > ?", cutoff_date)
.where(featured: true, email_digest_eligible: true)
.where.not(user_id: @user.id)
.where("score > ?", 25)
.order(score: :desc)
.limit(6)
end
articles.length < 3 ? [] : articles
end
# rubocop:enable Metrics/BlockLength
end
private
def should_receive_email?
return true unless last_email_sent_at
# Has it been at least x days since @user received an email?
Time.current - last_email_sent_at >= days_until_next_email
end
def days_until_next_email
# Relies on hyperbolic tangent function to model the frequency of the digest email
max_day = SiteConfig.periodic_email_digest_max
min_day = SiteConfig.periodic_email_digest_min
result = max_day * (1 - Math.tanh(2 * open_rate))
result = result.round
[result, min_day].max
end
def open_rate
email_count = last_user_emails.count
# Will stick with 50% open rate if @user has no/not-enough email digest history
return 0.5 if email_count < 10
past_opened_emails_count = last_user_emails.count { |msg| msg.opened_at.present? }
past_opened_emails_count / email_count
end
def last_email_sent_at
last_user_emails.last&.sent_at
end
def cutoff_date
a_few_days_ago = 4.days.ago.utc
return a_few_days_ago unless last_email_sent_at
[a_few_days_ago, last_email_sent_at].max
end
def user_has_followings?
@user.following_users_count.positive? || @user.cached_followed_tag_names.any?
end
def last_user_emails
@last_user_emails ||= @user
.email_messages
.select(:sent_at, :opened_at)
.where(mailer: "DigestMailer#digest_email")
.limit(10)
end
end