summaryrefslogtreecommitdiff
path: root/lib/gitlab/experimentation.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gitlab/experimentation.rb')
-rw-r--r--lib/gitlab/experimentation.rb93
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