Class: Gitlab::Experiment

Inherits:
Object
  • Object
show all
Includes:
Cache, Callbacks, Scientist::Experiment
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/context.rb,
lib/gitlab/experiment/cookies.rb,
lib/gitlab/experiment/variant.rb,
lib/gitlab/experiment/version.rb,
lib/gitlab/experiment/callbacks.rb,
lib/gitlab/experiment/configuration.rb,
lib/gitlab/experiment/cache/redis_hash_store.rb

Defined Under Namespace

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

Constant Summary collapse

VERSION =
'0.4.12'

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Cache

#cache, #cache_key, #cache_variant

Constructor Details

#initialize(name = nil, variant_name = nil, **context) {|_self| ... } ⇒ Experiment

Returns a new instance of Experiment.

Yields:

  • (_self)

Yield Parameters:

Raises:

  • (ArgumentError)


58
59
60
61
62
63
64
65
66
67
68
# File 'lib/gitlab/experiment.rb', line 58

def initialize(name = nil, variant_name = nil, **context)
  raise ArgumentError, 'name is required' if name.blank? && self.class.base?

  @name = self.class.experiment_name(name, suffix: false)
  @context = Context.new(self, **context)
  @variant_name = cache_variant(variant_name) { nil } if variant_name.present?

  compare { false }

  yield self if block_given?
end

Class Method Details

.base?Boolean

Returns:

  • (Boolean)


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

def base?
  self == Gitlab::Experiment || name == Configuration.base_class
end

.configure {|Configuration| ... } ⇒ Object

Yields:



26
27
28
# File 'lib/gitlab/experiment.rb', line 26

def configure
  yield Configuration
end

.experiment_name(name = nil, suffix: true, suffix_word: 'experiment') ⇒ Object



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

def experiment_name(name = nil, suffix: true, suffix_word: 'experiment')
  name = (name.presence || self.name).to_s.underscore.sub(%r{(?<char>[_/]|)#{suffix_word}$}, '')
  name = "#{name}#{Regexp.last_match(:char) || '_'}#{suffix_word}"
  suffix ? name : name.sub(/_#{suffix_word}$/, '')
end

.run(name = nil, variant_name = nil, **context, &block) ⇒ Object

Raises:

  • (ArgumentError)


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

def run(name = nil, variant_name = nil, **context, &block)
  raise ArgumentError, 'name is required' if name.nil? && base?

  instance = constantize(name).new(name, variant_name, **context, &block)
  return instance unless block

  instance.context.frozen? ? instance.run : instance.tap(&:run)
end

Instance Method Details

#behaviorsObject



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

def behaviors
  @behaviors ||= public_methods.each_with_object(super) do |name, behaviors|
    next unless name.end_with?('_behavior')

    behavior_name = name.to_s.sub(/_behavior$/, '')
    behaviors[behavior_name] ||= -> { send(name) } # rubocop:disable GitlabSecurity/PublicSend
  end
end

#context(value = nil) ⇒ Object



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

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

  @context.value(value)
  @context
end

#enabled?Boolean

Returns:

  • (Boolean)


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

def enabled?
  true
end

#excluded?Boolean

Returns:

  • (Boolean)


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

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

  @excluded = !@context.trackable? || # adhere to DNT headers
    !run_callbacks(:exclusion_check) { :not_excluded } # didn't pass exclusion check
end

#flipper_idObject



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

def flipper_id
  "Experiment;#{id}"
end

#idObject Also known as: session_id



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

def id
  "#{name}:#{key_for(context.value)}"
end

#inspectObject



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

def inspect
  "#<#{self.class.name || 'AnonymousClass'}:#{format('0x%016X', __id__)} @name=#{name} @context=#{context.value}>"
end

#key_for(hash) ⇒ Object



166
167
168
# File 'lib/gitlab/experiment.rb', line 166

def key_for(hash)
  instance_exec(hash, &Configuration.context_hash_strategy)
end

#nameObject



116
117
118
# File 'lib/gitlab/experiment.rb', line 116

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

#publish(result) ⇒ Object



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

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

#run(variant_name = nil) ⇒ Object



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

def run(variant_name = nil)
  @result ||= super(variant(variant_name).name)
end

#should_track?Boolean

Returns:

  • (Boolean)


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

def should_track?
  enabled? && !excluded?
end

#signatureObject



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

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

#track(action, **event_args) ⇒ Object



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

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

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

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



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

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

#variant(value = nil) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/gitlab/experiment.rb', line 81

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?
    @variant_name ||= :control if excluded?

    @resolving_variant = true
    if (result = cache_variant(@variant_name) { resolve_variant_name }).present?
      @variant_name = result.to_sym
    end
  end

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

#variant_namesObject



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

def variant_names
  @variant_names ||= behaviors.keys.map(&:to_sym) - [:control]
end