diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 21:18:33 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 21:18:33 +0300 |
commit | f64a639bcfa1fc2bc89ca7db268f594306edfd7c (patch) | |
tree | a2c3c2ebcc3b45e596949db485d6ed18ffaacfa1 /app/experiments/application_experiment.rb | |
parent | bfbc3e0d6583ea1a91f627528bedc3d65ba4b10f (diff) |
Add latest changes from gitlab-org/gitlab@13-10-stable-eev13.10.0-rc40
Diffstat (limited to 'app/experiments/application_experiment.rb')
-rw-r--r-- | app/experiments/application_experiment.rb | 106 |
1 files changed, 17 insertions, 89 deletions
diff --git a/app/experiments/application_experiment.rb b/app/experiments/application_experiment.rb index 317514d088b..ed0d146af8c 100644 --- a/app/experiments/application_experiment.rb +++ b/app/experiments/application_experiment.rb @@ -3,19 +3,28 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/NamespacedClass def enabled? return false if Feature::Definition.get(feature_flag_name).nil? # there has to be a feature flag yaml file - return false unless Gitlab.dev_env_or_com? # we're in an environment that allows experiments + return false unless Gitlab.dev_env_or_com? # we have to be in an environment that allows experiments + # the feature flag has to be rolled out Feature.get(feature_flag_name).state != :off # rubocop:disable Gitlab/AvoidFeatureGet end - def publish(_result) + def publish(_result = nil) + return unless should_track? # don't track events for excluded contexts + track(:assignment) # track that we've assigned a variant for this context - Gon.global.push({ experiment: { name => signature } }, true) # push to client + + begin + Gon.push({ experiment: { name => signature } }, true) # push the experiment data to the client + rescue NoMethodError + # means we're not in the request cycle, and can't add to Gon. Log a warning maybe? + end end def track(action, **event_args) - return unless should_track? # no events for opted out actors or excluded subjects + return unless should_track? # don't track events for excluded contexts + # track the event, and mix in the experiment signature data Gitlab::Tracking.event(name, action.to_s, **event_args.merge( context: (event_args[:context] || []) << SnowplowTracker::SelfDescribingJson.new( 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0', signature @@ -23,16 +32,8 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp )) end - def rollout_strategy - # no-op override in inherited class as desired - end - - def variants - # override as desired in inherited class with all variants + control - # %i[variant1 variant2 control] - # - # this will make sure we supply variants as these go together - rollout_strategy of :round_robin must have variants - raise NotImplementedError, "Inheriting class must supply variants as an array if :round_robin strategy is used" if rollout_strategy == :round_robin + def exclude! + @excluded = true end private @@ -41,80 +42,7 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp name.tr('/', '_') end - def resolve_variant_name - case rollout_strategy - when :round_robin - round_robin_rollout - else - percentage_rollout - end - end - - def round_robin_rollout - Strategy::RoundRobin.new(feature_flag_name, variants).execute - end - - def percentage_rollout - return variant_names.first if Feature.enabled?(feature_flag_name, self, type: :experiment, default_enabled: :yaml) - - nil # Returning nil vs. :control is important for not caching and rollouts. - end - - # Cache is an implementation on top of Gitlab::Redis::SharedState that also - # adheres to the ActiveSupport::Cache::Store interface and uses the redis - # hash data type. - # - # Since Gitlab::Experiment can use any type of caching layer, utilizing the - # long lived shared state interface here gives us an efficient way to store - # context keys and the variant they've been assigned -- while also giving us - # a simple way to clean up an experiments data upon resolution. - # - # The data structure: - # key: experiment.name - # fields: context key => variant name - # - # The keys are expected to be `experiment_name:context_key`, which is the - # default cache key strategy. So running `cache.fetch("foo:bar", "value")` - # would create/update a hash with the key of "foo", with a field named - # "bar" that has "value" assigned to it. - class Cache < ActiveSupport::Cache::Store # rubocop:disable Gitlab/NamespacedClass - # Clears the entire cache for a given experiment. Be careful with this - # since it would reset all resolved variants for the entire experiment. - def clear(key:) - key = hkey(key)[0] # extract only the first part of the key - pool do |redis| - case redis.type(key) - when 'hash', 'none' then redis.del(key) - else raise ArgumentError, 'invalid call to clear a non-hash cache key' - end - end - end - - private - - def pool - raise ArgumentError, 'missing block' unless block_given? - - Gitlab::Redis::SharedState.with { |redis| yield redis } - end - - def hkey(key) - key.to_s.split(':') # this assumes the default strategy in gitlab-experiment - end - - def read_entry(key, **options) - value = pool { |redis| redis.hget(*hkey(key)) } - value.nil? ? nil : ActiveSupport::Cache::Entry.new(value) - end - - def write_entry(key, entry, **options) - return false if entry.value.blank? # don't cache any empty values - - pool { |redis| redis.hset(*hkey(key), entry.value) } - end - - def delete_entry(key, **options) - pool { |redis| redis.hdel(*hkey(key)) } - end + def experiment_group? + Feature.enabled?(feature_flag_name, self, type: :experiment, default_enabled: :yaml) end end |