Class: Gitlab::Experiment

Inherits:
Object
  • Object
show all
Includes:
BaseInterface, Cache, Callbacks
Defined in:
lib/gitlab/experiment.rb,
lib/gitlab/experiment/dsl.rb,
lib/gitlab/experiment/cache.rb,
lib/gitlab/experiment/rspec.rb,
lib/gitlab/experiment/engine.rb,
lib/gitlab/experiment/errors.rb,
lib/gitlab/experiment/context.rb,
lib/gitlab/experiment/cookies.rb,
lib/gitlab/experiment/rollout.rb,
lib/gitlab/experiment/variant.rb,
lib/gitlab/experiment/version.rb,
lib/gitlab/experiment/callbacks.rb,
lib/gitlab/experiment/middleware.rb,
lib/gitlab/experiment/configuration.rb,
lib/gitlab/experiment/base_interface.rb,
lib/gitlab/experiment/rollout/random.rb,
lib/gitlab/experiment/rollout/percent.rb,
lib/gitlab/experiment/rollout/round_robin.rb,
lib/gitlab/experiment/cache/redis_hash_store.rb

Defined Under Namespace

Modules: BaseInterface, Cache, Callbacks, Cookies, Dsl, RSpecHelpers, RSpecMatchers, Rollout Classes: Configuration, Context, Engine, Middleware, Variant

Constant Summary collapse

Error =
Class.new(StandardError)
InvalidRolloutRules =
Class.new(Error)
VERSION =
'0.6.1'

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Cache

#cache, #cache_key, #cache_variant

Methods included from BaseInterface

#behaviors, #flipper_id, #id, #initialize, #inspect, #variant_names

Class Method Details

.default_rollout(rollout = nil, options = {}) ⇒ Object



33
34
35
36
37
# File 'lib/gitlab/experiment.rb', line 33

def default_rollout(rollout = nil, options = {})
  return @rollout ||= Configuration.default_rollout if rollout.blank?

  @rollout = Rollout.resolve(rollout).new(options)
end

.exclude(*filter_list, **options, &block) ⇒ Object



39
40
41
42
43
# File 'lib/gitlab/experiment.rb', line 39

def exclude(*filter_list, **options, &block)
  build_callback(:exclusion_check, filter_list.unshift(block), **options) do |target, callback|
    throw(:abort) if target.instance_variable_get(:@excluded) || callback.call(target, nil) == true
  end
end

.published_experimentsObject



51
52
53
# File 'lib/gitlab/experiment.rb', line 51

def published_experiments
  RequestStore.store[:published_gitlab_experiments] || {}
end

.segment(*filter_list, variant:, **options, &block) ⇒ Object



45
46
47
48
49
# File 'lib/gitlab/experiment.rb', line 45

def segment(*filter_list, variant:, **options, &block)
  build_callback(:segmentation_check, filter_list.unshift(block), **options) do |target, callback|
    target.variant(variant) if target.instance_variable_get(:@variant_name).nil? && callback.call(target, nil)
  end
end

Instance Method Details

#candidate(name = nil, &block) ⇒ Object Also known as: try



65
66
67
68
# File 'lib/gitlab/experiment.rb', line 65

def candidate(name = nil, &block)
  name = (name || :candidate).to_s
  behaviors[name] = block
end

#context(value = nil) ⇒ Object



71
72
73
74
75
76
# File 'lib/gitlab/experiment.rb', line 71

def context(value = nil)
  return @context if value.blank?

  @context.value(value)
  @context
end

#control(&block) ⇒ Object Also known as: use



60
61
62
# File 'lib/gitlab/experiment.rb', line 60

def control(&block)
  candidate(:control, &block)
end

#enabled?Boolean

Returns:

  • (Boolean)


123
124
125
# File 'lib/gitlab/experiment.rb', line 123

def enabled?
  true
end

#exclude!Object



101
102
103
# File 'lib/gitlab/experiment.rb', line 101

def exclude!
  @excluded = true
end

#excluded?Boolean

Returns:

  • (Boolean)


127
128
129
130
131
# File 'lib/gitlab/experiment.rb', line 127

def excluded?
  return @excluded if defined?(@excluded)

  @excluded = !run_callbacks(:exclusion_check) { :not_excluded }
end

#experiment_group?Boolean

Returns:

  • (Boolean)


133
134
135
# File 'lib/gitlab/experiment.rb', line 133

def experiment_group?
  instance_exec(@variant_name, &Configuration.inclusion_resolver)
end

#key_for(source, seed = name) ⇒ Object



145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/gitlab/experiment.rb', line 145

def key_for(source, seed = name)
  # TODO: Added deprecation in release 0.6.0
  if (block = Configuration.instance_variable_get(:@__context_hash_strategy))
    return instance_exec(source, seed, &block)
  end

  source = source.keys + source.values if source.is_a?(Hash)

  ingredients = Array(source).map { |v| identify(v) }
  ingredients.unshift(seed).unshift(Configuration.context_key_secret)

  Digest::SHA2.new(Configuration.context_key_bit_length).hexdigest(ingredients.join('|'))
end

#nameObject



56
57
58
# File 'lib/gitlab/experiment.rb', line 56

def name
  [Configuration.name_prefix, @name].compact.join('_')
end

#publish(result) ⇒ Object



111
112
113
114
115
# File 'lib/gitlab/experiment.rb', line 111

def publish(result)
  instance_exec(result, &Configuration.publishing_behavior)

  (RequestStore.store[:published_gitlab_experiments] ||= {})[name] = signature.merge(excluded: excluded?)
end

#rollout(rollout = nil, options = {}) ⇒ Object



95
96
97
98
99
# File 'lib/gitlab/experiment.rb', line 95

def rollout(rollout = nil, options = {})
  return @rollout ||= self.class.default_rollout(nil, options) if rollout.blank?

  @rollout = Rollout.resolve(rollout).new(options)
end

#run(variant_name = nil) ⇒ Object



105
106
107
108
109
# File 'lib/gitlab/experiment.rb', line 105

def run(variant_name = nil)
  @result ||= super(variant(variant_name).name)
rescue Scientist::BehaviorMissing => e
  raise Error, e
end

#should_track?Boolean

Returns:

  • (Boolean)


137
138
139
# File 'lib/gitlab/experiment.rb', line 137

def should_track?
  enabled? && @context.trackable? && !excluded?
end

#signatureObject



141
142
143
# File 'lib/gitlab/experiment.rb', line 141

def signature
  { variant: variant.name, experiment: name }.merge(context.signature)
end

#track(action, **event_args) ⇒ Object



117
118
119
120
121
# File 'lib/gitlab/experiment.rb', line 117

def track(action, **event_args)
  return unless should_track?

  instance_exec(action, event_args, &Configuration.tracking_behavior)
end

#variant(value = nil) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/gitlab/experiment.rb', line 78

def variant(value = nil)
  @variant_name = cache_variant(value) if value.present?
  return Variant.new(name: (@variant_name || :unresolved).to_s) if @variant_name || @resolving_variant

  if enabled?
    @resolving_variant = true
    @variant_name = cached_variant_resolver(@variant_name)
  end

  run_callbacks(segmentation_callback_chain) do
    @variant_name ||= :control
    Variant.new(name: @variant_name.to_s)
  end
ensure
  @resolving_variant = false
end