Class: Gitlab::Experiment

Inherits:
Object
  • Object
show all
Includes:
ActiveModel::Model, BaseInterface, Cache, Callbacks, Nestable
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/nestable.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,
lib/gitlab/experiment/test_behaviors/trackable.rb

Defined Under Namespace

Modules: BaseInterface, Cache, Callbacks, Cookies, Dsl, Nestable, RSpecHelpers, RSpecMatchers, RSpecMocks, Rollout, TestBehaviors Classes: Configuration, Context, Engine, Middleware, NestingError, Variant, WrappedExperiment

Constant Summary collapse

Error =
Class.new(StandardError)
InvalidRolloutRules =
Class.new(Error)
UnregisteredExperiment =
Class.new(Error)
ExistingBehaviorError =
Class.new(Error)
BehaviorMissingError =
Class.new(Error)
VERSION =
'0.7.0'

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Nestable

#nest_experiment

Methods included from Cache

#cache, #cache_key, #cache_variant

Methods included from BaseInterface

#behaviors, #flipper_id, #id, #initialize, #inspect, #public_behaviors_with_deprecations, #session_id, #try, #use, #variant_names

Class Method Details

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



62
63
64
# File 'lib/gitlab/experiment.rb', line 62

def after_run(*filter_list, **options, &block)
  build_run_callback(filter_list.unshift(:after, block), **options)
end

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



58
59
60
# File 'lib/gitlab/experiment.rb', line 58

def around_run(*filter_list, **options, &block)
  build_run_callback(filter_list.unshift(:around, block), **options)
end

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



54
55
56
# File 'lib/gitlab/experiment.rb', line 54

def before_run(*filter_list, **options, &block)
  build_run_callback(filter_list.unshift(:before, block), **options)
end

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



36
37
38
# File 'lib/gitlab/experiment.rb', line 36

def candidate(*filter_list, **options, &block)
  variant(:candidate, *filter_list, **options, &block)
end

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

Class level behavior registration methods.



32
33
34
# File 'lib/gitlab/experiment.rb', line 32

def control(*filter_list, **options, &block)
  variant(:control, *filter_list, **options, &block)
end

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

Class level definition methods.



68
69
70
71
72
# File 'lib/gitlab/experiment.rb', line 68

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

Class level callback registration methods.



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

def exclude(*filter_list, **options, &block)
  build_exclude_callback(filter_list.unshift(block), **options)
end

.model_nameObject

Used for generating routes. We’ve included the method and ‘ActiveModel::Model` here because these things don’t make sense outside of Rails environments.



11
12
13
# File 'lib/gitlab/experiment/engine.rb', line 11

def self.model_name
  ActiveModel::Name.new(self, Gitlab)
end

.published_experimentsObject

Class level accessor methods.



76
77
78
# File 'lib/gitlab/experiment.rb', line 76

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

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



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

def segment(*filter_list, variant:, **options, &block)
  build_segment_callback(filter_list.unshift(block), variant, **options)
end

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



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

def variant(variant, *filter_list, **options, &block)
  build_behavior_callback(filter_list, variant, **options, &block)
end

Instance Method Details

#assigned(value = nil) ⇒ Object



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/gitlab/experiment.rb', line 126

def assigned(value = nil)
  @_assigned_variant_name = cache_variant(value) if value.present?
  if @_assigned_variant_name || @_resolving_variant
    return Variant.new(name: (@_assigned_variant_name || :unresolved).to_s)
  end

  if enabled?
    @_resolving_variant = true
    @_assigned_variant_name = cached_variant_resolver(@_assigned_variant_name)
  end

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

#candidate(name = nil, &block) ⇒ Object



89
90
91
92
93
94
95
96
97
# File 'lib/gitlab/experiment.rb', line 89

def candidate(name = nil, &block)
  if name.present?
    Configuration.deprecated(<<~MESSAGE, version: '0.7.0')
      passing name to `candidate` is deprecated and will be removed from {{release}} (instead use `variant(#{name.inspect})`)
    MESSAGE
  end

  variant(name || :candidate, &block)
end

#context(value = nil) ⇒ Object



119
120
121
122
123
124
# File 'lib/gitlab/experiment.rb', line 119

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

  @_context.value(value)
  @_context
end

#control(&block) ⇒ Object



85
86
87
# File 'lib/gitlab/experiment.rb', line 85

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

#enabled?Boolean

Returns:

  • (Boolean)


180
181
182
# File 'lib/gitlab/experiment.rb', line 180

def enabled?
  rollout.enabled?
end

#exclude!Object



151
152
153
# File 'lib/gitlab/experiment.rb', line 151

def exclude!
  @_excluded = true
end

#excluded?Boolean

Returns:

  • (Boolean)


184
185
186
187
188
# File 'lib/gitlab/experiment.rb', line 184

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

  @_excluded = !run_callbacks(exclusion_callback_chain) { :not_excluded }
end

#key_for(source, seed = name) ⇒ Object



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/gitlab/experiment.rb', line 198

def key_for(source, seed = name)
  # TODO: Remove - deprecated in release 0.7.0
  if (block = Configuration.instance_variable_get(:@__context_hash_strategy))
    return instance_exec(source, seed, &block)
  end

  return source if source.is_a?(String)

  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



81
82
83
# File 'lib/gitlab/experiment.rb', line 81

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

#process_redirect_url(url) ⇒ Object



173
174
175
176
177
178
# File 'lib/gitlab/experiment.rb', line 173

def process_redirect_url(url)
  return unless Configuration.redirect_url_validator&.call(url)

  track('visited', url: url)
  url # return the url, which allows for mutation
end

#publish(result = nil) ⇒ Object



161
162
163
164
165
# File 'lib/gitlab/experiment.rb', line 161

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

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

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



145
146
147
148
149
# File 'lib/gitlab/experiment.rb', line 145

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

  @_rollout = Rollout.resolve(rollout).new(options).for(self)
end

#run(variant_name = nil) ⇒ Object



155
156
157
158
159
# File 'lib/gitlab/experiment.rb', line 155

def run(variant_name = nil)
  return @_result if context.frozen?

  @_result = run_callbacks(run_callback_chain) { super(assigned(variant_name).name) }
end

#should_track?Boolean

Returns:

  • (Boolean)


190
191
192
# File 'lib/gitlab/experiment.rb', line 190

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

#signatureObject



194
195
196
# File 'lib/gitlab/experiment.rb', line 194

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

#track(action, **event_args) ⇒ Object



167
168
169
170
171
# File 'lib/gitlab/experiment.rb', line 167

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

  instance_exec(action, tracking_context(event_args).try(:compact) || {}, &Configuration.tracking_behavior)
end

#variant(name = nil, &block) ⇒ Object



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/gitlab/experiment.rb', line 99

def variant(name = nil, &block)
  if block.present? # we know we're defining a variant block
    raise ArgumentError, 'missing variant name' if name.blank?

    return behaviors[name.to_s] = block
  end

  if name.present?
    Configuration.deprecated(<<~MESSAGE, version: '0.7.0')
      setting the variant using `variant` is deprecated and will be removed from {{release}} (instead use `assigned(#{name.inspect})`)
    MESSAGE
  else
    Configuration.deprecated(<<~MESSAGE, version: '0.7.0')
      getting the assigned variant using `variant` is deprecated and will be removed from {{release}} (instead use `assigned`)
    MESSAGE
  end

  assigned(name)
end