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.rb99
1 files changed, 49 insertions, 50 deletions
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index 6e39776bbd4..94523813662 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -4,9 +4,11 @@
#
# Utility module for A/B testing experimental features. Define your experiments in the `EXPERIMENTS` constant.
# Experiment options:
-# - environment (optional, defaults to enabled for development and GitLab.com)
# - tracking_category (optional, used to set the category when tracking an experiment event)
-# - use_backwards_compatible_subject_index (optional, set this to true if you need backwards compatibility)
+# - use_backwards_compatible_subject_index (optional, set this to true if you need backwards compatibility -- you likely do not need this, see note in the next paragraph.)
+#
+# Using the backwards-compatible subject index (use_backwards_compatible_subject_index option):
+# This option was added when [the calculation of experimentation_subject_index was changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45733/diffs#41af4a6fa5a10c7068559ce21c5188483751d934_157_173). It is not intended to be used by new experiments, it exists merely for the segmentation integrity of in-flight experiments at the time the change was deployed. That is, we want users who were assigned to the "experimental" group or the "control" group before the change to still be in those same groups after the change. See [the original issue](https://gitlab.com/gitlab-org/gitlab/-/issues/270858) and [this related comment](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48110#note_458223745) for more information.
#
# The experiment is controlled by a Feature Flag (https://docs.gitlab.com/ee/development/feature_flags/controls.html),
# which is named "#{experiment_key}_experiment_percentage" and *must* be set with a percentage and not be used for other purposes.
@@ -55,10 +57,6 @@ module Gitlab
tracking_category: 'Growth::Expansion::Experiment::InviteMembersEmptyGroupVersionA',
use_backwards_compatible_subject_index: true
},
- new_create_project_ui: {
- tracking_category: 'Manage::Import::Experiment::NewCreateProjectUi',
- use_backwards_compatible_subject_index: true
- },
contact_sales_btn_in_app: {
tracking_category: 'Growth::Conversion::Experiment::ContactSalesInApp',
use_backwards_compatible_subject_index: true
@@ -67,14 +65,6 @@ module Gitlab
tracking_category: 'Growth::Expansion::Experiment::CustomizeHomepage',
use_backwards_compatible_subject_index: true
},
- invite_email: {
- tracking_category: 'Growth::Acquisition::Experiment::InviteEmail',
- use_backwards_compatible_subject_index: true
- },
- invitation_reminders: {
- tracking_category: 'Growth::Acquisition::Experiment::InvitationReminders',
- use_backwards_compatible_subject_index: true
- },
group_only_trials: {
tracking_category: 'Growth::Conversion::Experiment::GroupOnlyTrials',
use_backwards_compatible_subject_index: true
@@ -82,59 +72,68 @@ module Gitlab
default_to_issues_board: {
tracking_category: 'Growth::Conversion::Experiment::DefaultToIssuesBoard',
use_backwards_compatible_subject_index: true
+ },
+ jobs_empty_state: {
+ tracking_category: 'Growth::Activation::Experiment::JobsEmptyState'
+ },
+ remove_known_trial_form_fields: {
+ tracking_category: 'Growth::Conversion::Experiment::RemoveKnownTrialFormFields'
+ },
+ trimmed_skip_trial_copy: {
+ tracking_category: 'Growth::Conversion::Experiment::TrimmedSkipTrialCopy'
+ },
+ trial_registration_with_social_signin: {
+ tracking_category: 'Growth::Conversion::Experiment::TrialRegistrationWithSocialSigning'
+ },
+ invite_members_empty_project_version_a: {
+ tracking_category: 'Growth::Expansion::Experiment::InviteMembersEmptyProjectVersionA'
}
}.freeze
class << self
- def experiment(key)
- Experiment.new(EXPERIMENTS[key].merge(key: key))
- end
-
- def enabled?(experiment_key)
- return false unless EXPERIMENTS.key?(experiment_key)
+ def get_experiment(experiment_key)
+ return unless EXPERIMENTS.key?(experiment_key)
- experiment = experiment(experiment_key)
- experiment.enabled_for_environment? && experiment.enabled?
+ ::Gitlab::Experimentation::Experiment.new(experiment_key, **EXPERIMENTS[experiment_key])
end
- def enabled_for_attribute?(experiment_key, attribute)
- index = Digest::SHA1.hexdigest(attribute).hex % 100
- enabled_for_value?(experiment_key, index)
- end
+ def active?(experiment_key)
+ experiment = get_experiment(experiment_key)
+ return false unless experiment
- def enabled_for_value?(experiment_key, value)
- enabled?(experiment_key) && experiment(experiment_key).enabled_for_index?(value)
+ experiment.active?
end
- end
- Experiment = Struct.new(
- :key,
- :environment,
- :tracking_category,
- :use_backwards_compatible_subject_index,
- keyword_init: true
- ) do
- def enabled?
- experiment_percentage > 0
- end
+ def in_experiment_group?(experiment_key, subject:)
+ return false if subject.blank?
+ return false unless active?(experiment_key)
- def enabled_for_environment?
- return ::Gitlab.dev_env_or_com? if environment.nil?
+ experiment = get_experiment(experiment_key)
+ return false unless experiment
- environment
+ experiment.enabled_for_index?(index_for_subject(experiment, subject))
end
- def enabled_for_index?(index)
- return false if index.blank?
+ private
- index <= experiment_percentage
- end
+ def index_for_subject(experiment, subject)
+ index = if experiment.use_backwards_compatible_subject_index
+ Digest::SHA1.hexdigest(subject_id(subject)).hex
+ else
+ Zlib.crc32("#{experiment.key}#{subject_id(subject)}")
+ end
- private
+ index % 100
+ end
- # When a feature does not exist, the `percentage_of_time_value` method will return 0
- def experiment_percentage
- @experiment_percentage ||= Feature.get(:"#{key}_experiment_percentage").percentage_of_time_value # rubocop:disable Gitlab/AvoidFeatureGet
+ def subject_id(subject)
+ if subject.respond_to?(:to_global_id)
+ subject.to_global_id.to_s
+ elsif subject.respond_to?(:to_s)
+ subject.to_s
+ else
+ raise ArgumentError.new('Subject must respond to `to_global_id` or `to_s`')
+ end
end
end
end