This change introduces several things things: 1. Fixes the performance of the `/admin/abtests` page; instead of rendering all of the experiment results, just render the overview. 2. Adjust the show page for an experiment to show the summary of results. 3. Favor the existing "started_at" and "ended_at" attributes of FieldTest (see https://github.com/ankane/field_test#config) 4. Update the field test experiments to include the `started_at` and `ended_at` attributes. 5. Skip processing all non-active experiments in our conversion handler. Why are there no tests? This is a page that is Admin only and is only visible for reporting purposes. So, it's not quite worth writing tests as you'd need lots of data. So we'll assume the logic from the upstream [field_test gem][1] is adequate. Closes forem/forem#17869 [1]:https://github.com/ankane/field_test
169 lines
7.3 KiB
Ruby
169 lines
7.3 KiB
Ruby
class AbExperiment
|
|
# Responsible for checking if a given :user has "accomplished" the state :goal for any of the
|
|
# active :experiments. We scope our tests to events that happened on or after the experiment's
|
|
# start date.
|
|
#
|
|
# @note It is required that each experiment have a start date (in CCYY-MM-DD format).
|
|
class GoalConversionHandler
|
|
include FieldTest::Helpers
|
|
|
|
# The constant says "publishes post"; we're not actually concerned with the state change (from
|
|
# unpublished to published) but instead counting the number of published articles.
|
|
USER_PUBLISHES_POST_GOAL = "user_publishes_post".freeze
|
|
USER_CREATES_PAGEVIEW_GOAL = "user_creates_pageview".freeze
|
|
USER_CREATES_COMMENT_GOAL = "user_creates_comment".freeze
|
|
USER_CREATES_ARTICLE_REACTION_GOAL = "user_creates_article_reaction".freeze
|
|
|
|
def self.call(...)
|
|
new(...).call
|
|
end
|
|
|
|
def initialize(user:, goal:, experiments:, start_date: nil)
|
|
@user = user
|
|
@goal = goal
|
|
@experiments = experiments
|
|
@start_date = start_date&.beginning_of_day
|
|
end
|
|
|
|
attr_reader :experiments, :user, :goal
|
|
|
|
def call
|
|
# It's okay that there are no experiments.
|
|
return if experiments.nil?
|
|
|
|
experiments.each do |key, data|
|
|
# We've already declared a winner, let's not do any of the processing
|
|
next if data.key?("winner")
|
|
|
|
experiment_start_date = @start_date || data.fetch("started_at").beginning_of_day
|
|
experiment = key.to_sym
|
|
convert(experiment: experiment, experiment_start_date: experiment_start_date)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def convert(experiment:, experiment_start_date:)
|
|
case goal
|
|
# We have special conditional goals for some where we look for past events for cummulative wins
|
|
# Otherwise we convert the goal as given.
|
|
when USER_CREATES_PAGEVIEW_GOAL
|
|
convert_pageview_goal(experiment: experiment, experiment_start_date: experiment_start_date)
|
|
when USER_CREATES_COMMENT_GOAL # comments goal. Only page views and comments are currently active.
|
|
convert_comment_goal(experiment: experiment, experiment_start_date: experiment_start_date)
|
|
when USER_PUBLISHES_POST_GOAL
|
|
convert_post_goal(experiment: experiment, experiment_start_date: experiment_start_date)
|
|
when USER_CREATES_ARTICLE_REACTION_GOAL
|
|
convert_reaction_goal(experiment: experiment, experiment_start_date: experiment_start_date)
|
|
else
|
|
field_test_converted(experiment, participant: user, goal: goal) # base single comment goal.
|
|
end
|
|
end
|
|
|
|
def convert_pageview_goal(experiment:, experiment_start_date:)
|
|
# TODO: Remove once we know that this test is not over-heating the application. That would be a
|
|
# few days after the deploy to DEV of this change.
|
|
if FeatureFlag.accessible?(:field_test_event_single_create_pageview)
|
|
field_test_converted(experiment, participant: user, goal: goal) # base is someone viewed a page
|
|
end
|
|
pageview_goal(experiment,
|
|
[7.days.ago, experiment_start_date].max,
|
|
"DATE(created_at)",
|
|
4,
|
|
"user_views_pages_on_at_least_four_different_days_within_a_week")
|
|
pageview_goal(experiment,
|
|
[24.hours.ago, experiment_start_date].max,
|
|
"DATE_PART('hour', created_at)",
|
|
4,
|
|
"user_views_pages_on_at_least_four_different_hours_within_a_day")
|
|
pageview_goal(experiment,
|
|
[14.days.ago, experiment_start_date].max,
|
|
"DATE(created_at)",
|
|
9,
|
|
"user_views_pages_on_at_least_nine_different_days_within_two_weeks")
|
|
pageview_goal(experiment,
|
|
[5.days.ago, experiment_start_date].max,
|
|
"DATE_PART('hour', created_at)",
|
|
12,
|
|
"user_views_pages_on_at_least_twelve_different_hours_within_five_days")
|
|
end
|
|
|
|
def convert_comment_goal(experiment:, experiment_start_date:)
|
|
field_test_converted(experiment, participant: user, goal: goal) # base single comment goal.
|
|
comment_goal(experiment,
|
|
[7.days.ago, experiment_start_date].max,
|
|
"DATE(created_at)",
|
|
4,
|
|
"user_creates_comment_on_at_least_four_different_days_within_a_week")
|
|
end
|
|
|
|
def convert_post_goal(experiment:, experiment_start_date:)
|
|
field_test_converted(experiment, participant: user, goal: goal) # base is we created a post
|
|
post_goal_with_group(experiment,
|
|
[7.days.ago, experiment_start_date].max,
|
|
"DATE(published_at)",
|
|
4,
|
|
"user_publishes_post_on_four_different_days_within_a_week")
|
|
post_goal(experiment,
|
|
[7.days.ago, experiment_start_date].max,
|
|
2,
|
|
"user_publishes_post_at_least_two_times_within_week")
|
|
post_goal(experiment,
|
|
[14.days.ago, experiment_start_date].max,
|
|
2,
|
|
"user_publishes_post_at_least_two_times_within_two_weeks")
|
|
end
|
|
|
|
def convert_reaction_goal(experiment:, experiment_start_date:)
|
|
field_test_converted(experiment, participant: user, goal: goal) # base is we created a post
|
|
reaction_goal(experiment,
|
|
[7.days.ago, experiment_start_date].max,
|
|
"DATE(created_at)",
|
|
4,
|
|
"user_creates_article_reaction_on_four_different_days_within_a_week")
|
|
end
|
|
|
|
def pageview_goal(experiment, time_start, group_value, min_count, goal)
|
|
page_view_counts = user.page_views.where("created_at > ?", time_start)
|
|
.group(group_value).count.values
|
|
page_view_counts.delete(0)
|
|
return unless page_view_counts.size >= min_count
|
|
|
|
field_test_converted(experiment, participant: user, goal: goal)
|
|
end
|
|
|
|
def post_goal(experiment, time_start, min_count, goal)
|
|
return unless user.articles.published.where("published_at > ?", time_start).count >= min_count
|
|
|
|
field_test_converted(experiment, participant: user, goal: goal)
|
|
end
|
|
|
|
def post_goal_with_group(experiment, time_start, group_value, min_count, goal)
|
|
post_publication_counts = user.articles.published.where("published_at > ?", time_start)
|
|
.group(group_value).count.values
|
|
return unless post_publication_counts.size >= min_count
|
|
|
|
field_test_converted(experiment, participant: user, goal: goal)
|
|
end
|
|
|
|
def comment_goal(experiment, time_start, group_value, min_count, goal)
|
|
comment_counts = user.comments.where("created_at > ?", time_start)
|
|
.group(group_value).count.values
|
|
comment_counts.delete(0)
|
|
return unless comment_counts.size >= min_count
|
|
|
|
field_test_converted(experiment, participant: user, goal: goal)
|
|
end
|
|
|
|
def reaction_goal(experiment, time_start, group_value, min_count, goal)
|
|
reaction_counts = user.reactions
|
|
.only_articles # as per `Reaction#record_field_test_event` we only record reactions to articles
|
|
.public_category # as per `Reaction#record_field_test_event` we only record public category reactions
|
|
.where("created_at > ?", time_start)
|
|
.group(group_value).count.values
|
|
return unless reaction_counts.size >= min_count
|
|
|
|
field_test_converted(experiment, participant: user, goal: goal)
|
|
end
|
|
end
|
|
end
|