docbrown/app/models/ab_experiment.rb
Jeremy Friesen 6269aa4108
Proposing a new feed experiment (#15789)
* Proposing a new feed experiment

This commit involves tweaking a few subtle aspects of the challenger:

1) Remove the sort by published date on the relevance score.  The
   articles will now be listed in the order of perceived relevance.
2) Disable a few levers that did little.  There really aren't any
   `articles.featured = true` articles in the database.
3) Gently increase the weight of each comment in a linear manner.
4) Give a little more weight to posts that don't have followed tags.

In addition, it involves renaming the conversions to better convey their
implementation.  This renaming helps create more descriptive labels for
the test results at `/admin/abtests`.

This commit also adds logic to repurpose the AbExperiment feed_strategy
logic; I envision adjusting the weighted feed strategy levers with some
frequency.

Per discussions, I'm also disabling the "not logged in" feed testing.
We'll use the LargeForemExperimental for this.

* Flipping attribute name to positive

Mentally "not disabled" is harder to parse than "enabled".  This change
helps with setting an optimistic attribute.

* Bump for travis

* Bump for travis

* Removing file committed by mistake

* Extracting method
2022-01-03 14:38:14 -05:00

97 lines
4 KiB
Ruby

# This object wraps the FieldTest logic to provide a bit of insulation
# between the FieldTest implementation and the application logic.
#
# This is a SimpleDelegator because the field_test method requires
# controller context when the participating user is nil.
#
# Use the AbExperiment.get method to interact with this class.
class AbExperiment < SimpleDelegator
# Don't allow someone to circumvent the .get class method. I want
# to follow the Law of Demeter and require that we us the @api
# public
private_class_method :new
# Sometimes we might want to repurpose the same AbExperiment logic
# for different experiments. This provides the tooling for that
# exact thing.
EXPERIMENT_TO_METHOD_NAME_MAP = {
feed_strategy_round_2: :feed_strategy
}.freeze
# This method helps us leverage existing methods for different
# experiments.
#
# @param experiment [Symbol] the name of the experiment
# @return [Symbol] the method name associated with this experiment.
#
# @see EXPERIMENT_TO_METHOD_NAME_MAP
def self.method_name_for(experiment)
EXPERIMENT_TO_METHOD_NAME_MAP.fetch(experiment, experiment)
end
# @api public
#
# A convenience method to insulate against the implementation
# details of the field_test gem.
#
# @param experiment [Symbol] the named method we'll call for an
# experiment. It should be a method name defined on this
# object.
# @param controller [ApplicationController] the request context of
# the experiment.
# @note We need the controller object due to the implementation of
# the `field_test` method. The `field_test` method is defined
# in the FieldTest::Helpers module. If we have a user, the
# field_test method works great with just the
# FieldTest::Helpers methods. However, if we don't have a
# user, then the field_test method calls
# `field_test_participant` which is defined in the
# `FieldTest::Controller` (which calls the `request` and
# `cookies`).
# @param user [User] who are we running the experiment with
# @param config [Hash] container for possible ENV override of strategy.
# @param default_value [Object] the caller is making decisions based
# on a configured set of values. They know which one they
# likely want. Let them give us a hint.
#
# @return [Object] the experimenter should know what it wants
#
# @see config/field_test.yml file for configured experiments.
#
# @note You can force a named strategy by setting an ENV variable.
# This forced strategy might be super useful for anyone performing
# QA testing on AB Testing scenarios.
#
# @todo If we make heavy use of this class, consider guarding for
# valid experiment methods.
def self.get(experiment:, controller:, user:, default_value:, config: ApplicationConfig)
method_name = method_name_for(experiment)
new(controller: controller)
.public_send(method_name, user: user, default_value: default_value, experiment: experiment, config: config)
end
# @api private
# @param controller [ApplicationController] the current controller
# that's handling the current request.
def initialize(controller:)
super(controller)
end
# @api private
# @note Called via AbExperiment.get
def feed_strategy(user:, config:, default_value:, experiment: :feed_strategy)
return default_value.inquiry unless FeatureFlag.accessible?(:ab_experiment_feed_strategy)
(config["AB_EXPERIMENT_FEED_STRATEGY"] || field_test(experiment, participant: user)).inquiry
rescue FieldTest::ExperimentNotFound
# rubocop:disable Layout/LineLength
Rails.logger.warn do
"Upstream request #{experiment.inspect} experiment. There are no registered #{experiment.inspect} experiments. Using the default value of #{default_value.inspect} for #{experiment.inspect} experiment."
end
# rubocop:enable Layout/LineLength
# Because we should have a fall back plan in case the field test
# has an odd configuration.
default_value.inquiry
end
end