diff options
Diffstat (limited to 'lib/gitlab/experimentation.rb')
-rw-r--r-- | lib/gitlab/experimentation.rb | 93 |
1 files changed, 93 insertions, 0 deletions
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb new file mode 100644 index 00000000000..895755376ee --- /dev/null +++ b/lib/gitlab/experimentation.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +# == Experimentation +# +# Utility module used for A/B testing experimental features. Define your experiments in the `EXPERIMENTS` constant. +# The feature_toggle and environment keys are optional. If the feature_toggle is not set, a feature with the name of +# the experiment will be checked, with a default value of true. The enabled_ratio is required and should be +# the ratio for the number of users for which this experiment is enabled. For example: a ratio of 0.1 will +# enable the experiment for 10% of the users (determined by the `experimentation_subject_index`). +# +module Gitlab + module Experimentation + EXPERIMENTS = { + signup_flow: { + feature_toggle: :experimental_separate_sign_up_flow, + environment: ::Gitlab.dev_env_or_com?, + enabled_ratio: 0.1 + } + }.freeze + + # Controller concern that checks if an experimentation_subject_id cookie is present and sets it if absent. + # Used for A/B testing of experimental features. Exposes the `experiment_enabled?(experiment_name)` method + # to controllers and views. + # + module ControllerConcern + extend ActiveSupport::Concern + + included do + before_action :set_experimentation_subject_id_cookie + helper_method :experiment_enabled? + end + + def set_experimentation_subject_id_cookie + return if cookies[:experimentation_subject_id].present? + + cookies.permanent.signed[:experimentation_subject_id] = { + value: SecureRandom.uuid, + domain: :all, + secure: ::Gitlab.config.gitlab.https + } + end + + def experiment_enabled?(experiment_key) + Experimentation.enabled?(experiment_key, experimentation_subject_index) + end + + private + + def experimentation_subject_index + experimentation_subject_id = cookies.signed[:experimentation_subject_id] + return if experimentation_subject_id.blank? + + experimentation_subject_id.delete('-').hex % 100 + end + end + + class << self + def experiment(key) + Experiment.new(EXPERIMENTS[key].merge(key: key)) + end + + def enabled?(experiment_key, experimentation_subject_index) + return false unless EXPERIMENTS.key?(experiment_key) + + experiment = experiment(experiment_key) + + experiment.feature_toggle_enabled? && + experiment.enabled_for_environment? && + experiment.enabled_for_experimentation_subject?(experimentation_subject_index) + end + end + + Experiment = Struct.new(:key, :feature_toggle, :environment, :enabled_ratio, keyword_init: true) do + def feature_toggle_enabled? + return Feature.enabled?(key, default_enabled: true) if feature_toggle.nil? + + Feature.enabled?(feature_toggle) + end + + def enabled_for_environment? + return true if environment.nil? + + environment + end + + def enabled_for_experimentation_subject?(experimentation_subject_index) + return false if enabled_ratio.nil? || experimentation_subject_index.blank? + + experimentation_subject_index <= enabled_ratio * 100 + end + end + end +end |