gitlab-experiment 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,12 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'scientist'
4
3
  require 'request_store'
5
- require 'active_support/callbacks'
6
- require 'active_support/cache'
7
- require 'active_support/concern'
8
- require 'active_support/core_ext/object/blank'
9
- require 'active_support/core_ext/string/inflections'
4
+ require 'active_support'
10
5
  require 'active_support/core_ext/module/delegation'
11
6
 
12
7
  require 'gitlab/experiment/errors'
@@ -19,6 +14,7 @@ require 'gitlab/experiment/cookies'
19
14
  require 'gitlab/experiment/context'
20
15
  require 'gitlab/experiment/dsl'
21
16
  require 'gitlab/experiment/middleware'
17
+ require 'gitlab/experiment/nestable'
22
18
  require 'gitlab/experiment/variant'
23
19
  require 'gitlab/experiment/version'
24
20
  require 'gitlab/experiment/engine' if defined?(Rails::Engine)
@@ -28,87 +24,141 @@ module Gitlab
28
24
  include BaseInterface
29
25
  include Cache
30
26
  include Callbacks
27
+ include Nestable
31
28
 
32
29
  class << self
33
- def default_rollout(rollout = nil, options = {})
34
- return @rollout ||= Configuration.default_rollout if rollout.blank?
30
+ # Class level behavior registration methods.
31
+
32
+ def control(*filter_list, **options, &block)
33
+ variant(:control, *filter_list, **options, &block)
34
+ end
35
+
36
+ def candidate(*filter_list, **options, &block)
37
+ variant(:candidate, *filter_list, **options, &block)
38
+ end
35
39
 
36
- @rollout = Rollout.resolve(rollout).new(options)
40
+ def variant(variant, *filter_list, **options, &block)
41
+ build_behavior_callback(filter_list, variant, **options, &block)
37
42
  end
38
43
 
44
+ # Class level callback registration methods.
45
+
39
46
  def exclude(*filter_list, **options, &block)
40
- build_callback(:exclusion_check, filter_list.unshift(block), **options) do |target, callback|
41
- throw(:abort) if target.instance_variable_get(:@excluded) || callback.call(target, nil) == true
42
- end
47
+ build_exclude_callback(filter_list.unshift(block), **options)
43
48
  end
44
49
 
45
50
  def segment(*filter_list, variant:, **options, &block)
46
- build_callback(:segmentation_check, filter_list.unshift(block), **options) do |target, callback|
47
- target.variant(variant) if target.instance_variable_get(:@variant_name).nil? && callback.call(target, nil)
48
- end
51
+ build_segment_callback(filter_list.unshift(block), variant, **options)
52
+ end
53
+
54
+ def before_run(*filter_list, **options, &block)
55
+ build_run_callback(filter_list.unshift(:before, block), **options)
56
+ end
57
+
58
+ def around_run(*filter_list, **options, &block)
59
+ build_run_callback(filter_list.unshift(:around, block), **options)
60
+ end
61
+
62
+ def after_run(*filter_list, **options, &block)
63
+ build_run_callback(filter_list.unshift(:after, block), **options)
49
64
  end
50
65
 
66
+ # Class level definition methods.
67
+
68
+ def default_rollout(rollout = nil, options = {})
69
+ return @_rollout ||= Configuration.default_rollout if rollout.blank?
70
+
71
+ @_rollout = Rollout.resolve(rollout).new(options)
72
+ end
73
+
74
+ # Class level accessor methods.
75
+
51
76
  def published_experiments
52
77
  RequestStore.store[:published_gitlab_experiments] || {}
53
78
  end
54
79
  end
55
80
 
56
81
  def name
57
- [Configuration.name_prefix, @name].compact.join('_')
82
+ [Configuration.name_prefix, @_name].compact.join('_')
58
83
  end
59
84
 
60
85
  def control(&block)
61
- candidate(:control, &block)
86
+ variant(:control, &block)
62
87
  end
63
- alias_method :use, :control
64
88
 
65
89
  def candidate(name = nil, &block)
66
- name = (name || :candidate).to_s
67
- behaviors[name] = block
90
+ if name.present?
91
+ Configuration.deprecated(<<~MESSAGE, version: '0.7.0')
92
+ passing name to `candidate` is deprecated and will be removed from {{release}} (instead use `variant(#{name.inspect})`)
93
+ MESSAGE
94
+ end
95
+
96
+ variant(name || :candidate, &block)
97
+ end
98
+
99
+ def variant(name = nil, &block)
100
+ if block.present? # we know we're defining a variant block
101
+ raise ArgumentError, 'missing variant name' if name.blank?
102
+
103
+ return behaviors[name.to_s] = block
104
+ end
105
+
106
+ if name.present?
107
+ Configuration.deprecated(<<~MESSAGE, version: '0.7.0')
108
+ setting the variant using `variant` is deprecated and will be removed from {{release}} (instead use `assigned(#{name.inspect})`)
109
+ MESSAGE
110
+ else
111
+ Configuration.deprecated(<<~MESSAGE, version: '0.7.0')
112
+ getting the assigned variant using `variant` is deprecated and will be removed from {{release}} (instead use `assigned`)
113
+ MESSAGE
114
+ end
115
+
116
+ assigned(name)
68
117
  end
69
- alias_method :try, :candidate
70
118
 
71
119
  def context(value = nil)
72
- return @context if value.blank?
120
+ return @_context if value.blank?
73
121
 
74
- @context.value(value)
75
- @context
122
+ @_context.value(value)
123
+ @_context
76
124
  end
77
125
 
78
- def variant(value = nil)
79
- @variant_name = cache_variant(value) if value.present?
80
- return Variant.new(name: (@variant_name || :unresolved).to_s) if @variant_name || @resolving_variant
126
+ def assigned(value = nil)
127
+ @_assigned_variant_name = cache_variant(value) if value.present?
128
+ if @_assigned_variant_name || @_resolving_variant
129
+ return Variant.new(name: (@_assigned_variant_name || :unresolved).to_s)
130
+ end
81
131
 
82
132
  if enabled?
83
- @resolving_variant = true
84
- @variant_name = cached_variant_resolver(@variant_name)
133
+ @_resolving_variant = true
134
+ @_assigned_variant_name = cached_variant_resolver(@_assigned_variant_name)
85
135
  end
86
136
 
87
137
  run_callbacks(segmentation_callback_chain) do
88
- @variant_name ||= :control
89
- Variant.new(name: @variant_name.to_s)
138
+ @_assigned_variant_name ||= :control
139
+ Variant.new(name: @_assigned_variant_name.to_s)
90
140
  end
91
141
  ensure
92
- @resolving_variant = false
142
+ @_resolving_variant = false
93
143
  end
94
144
 
95
145
  def rollout(rollout = nil, options = {})
96
- return @rollout ||= self.class.default_rollout(nil, options) if rollout.blank?
146
+ return @_rollout ||= self.class.default_rollout(nil, options).for(self) if rollout.blank?
97
147
 
98
- @rollout = Rollout.resolve(rollout).new(options)
148
+ @_rollout = Rollout.resolve(rollout).new(options).for(self)
99
149
  end
100
150
 
101
151
  def exclude!
102
- @excluded = true
152
+ @_excluded = true
103
153
  end
104
154
 
105
155
  def run(variant_name = nil)
106
- @result ||= super(variant(variant_name).name)
107
- rescue Scientist::BehaviorMissing => e
108
- raise Error, e
156
+ return @_result if context.frozen?
157
+
158
+ @_result = run_callbacks(run_callback_chain) { super(assigned(variant_name).name) }
109
159
  end
110
160
 
111
- def publish(result)
161
+ def publish(result = nil)
112
162
  instance_exec(result, &Configuration.publishing_behavior)
113
163
 
114
164
  (RequestStore.store[:published_gitlab_experiments] ||= {})[name] = signature.merge(excluded: excluded?)
@@ -117,7 +167,7 @@ module Gitlab
117
167
  def track(action, **event_args)
118
168
  return unless should_track?
119
169
 
120
- instance_exec(action, event_args, &Configuration.tracking_behavior)
170
+ instance_exec(action, tracking_context(event_args).try(:compact) || {}, &Configuration.tracking_behavior)
121
171
  end
122
172
 
123
173
  def process_redirect_url(url)
@@ -128,33 +178,31 @@ module Gitlab
128
178
  end
129
179
 
130
180
  def enabled?
131
- true
181
+ rollout.enabled?
132
182
  end
133
183
 
134
184
  def excluded?
135
- return @excluded if defined?(@excluded)
185
+ return @_excluded if defined?(@_excluded)
136
186
 
137
- @excluded = !run_callbacks(:exclusion_check) { :not_excluded }
138
- end
139
-
140
- def experiment_group?
141
- instance_exec(@variant_name, &Configuration.inclusion_resolver)
187
+ @_excluded = !run_callbacks(exclusion_callback_chain) { :not_excluded }
142
188
  end
143
189
 
144
190
  def should_track?
145
- enabled? && @context.trackable? && !excluded?
191
+ enabled? && context.trackable? && !excluded?
146
192
  end
147
193
 
148
194
  def signature
149
- { variant: variant.name, experiment: name }.merge(context.signature)
195
+ { variant: assigned.name, experiment: name }.merge(context.signature)
150
196
  end
151
197
 
152
198
  def key_for(source, seed = name)
153
- # TODO: Added deprecation in release 0.6.0
199
+ # TODO: Remove - deprecated in release 0.7.0
154
200
  if (block = Configuration.instance_variable_get(:@__context_hash_strategy))
155
201
  return instance_exec(source, seed, &block)
156
202
  end
157
203
 
204
+ return source if source.is_a?(String)
205
+
158
206
  source = source.keys + source.values if source.is_a?(Hash)
159
207
 
160
208
  ingredients = Array(source).map { |v| identify(v) }
@@ -169,14 +217,27 @@ module Gitlab
169
217
  (object.respond_to?(:to_global_id) ? object.to_global_id : object).to_s
170
218
  end
171
219
 
172
- def segmentation_callback_chain
173
- return :segmentation_check if @variant_name.nil? && enabled? && !excluded?
174
-
175
- :unsegmented
220
+ def resolve_variant_name
221
+ if respond_to?(:experiment_group?, true)
222
+ # TODO: Remove - deprecated in release 0.7.0
223
+ Configuration.deprecated(:experiment_group?, <<~MESSAGE, version: '0.7.0')
224
+ instead put this logic into custom rollout strategies
225
+ MESSAGE
226
+
227
+ rollout.resolve if experiment_group?
228
+ elsif (block = Configuration.instance_variable_get(:@__inclusion_resolver))
229
+ # TODO: Remove - deprecated in release 0.7.0
230
+ rollout.resolve if instance_exec(@_assigned_variant_name, &block)
231
+ elsif (block = Configuration.instance_variable_get(:@__variant_resolver))
232
+ # TODO: Remove - deprecated in release 0.6.5
233
+ instance_exec(@_assigned_variant_name, &block)
234
+ else
235
+ rollout.resolve # this is the end result of all deprecations
236
+ end
176
237
  end
177
238
 
178
- def resolve_variant_name
179
- rollout.rollout_for(self) if experiment_group?
239
+ def tracking_context(event_args)
240
+ {}.merge(event_args)
180
241
  end
181
242
  end
182
243
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-experiment
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab
@@ -38,26 +38,6 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.0'
41
- - !ruby/object:Gem::Dependency
42
- name: scientist
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '1.6'
48
- - - ">="
49
- - !ruby/object:Gem::Version
50
- version: 1.6.0
51
- type: :runtime
52
- prerelease: false
53
- version_requirements: !ruby/object:Gem::Requirement
54
- requirements:
55
- - - "~>"
56
- - !ruby/object:Gem::Version
57
- version: '1.6'
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: 1.6.0
61
41
  description:
62
42
  email:
63
43
  - gitlab_rubygems@gitlab.com
@@ -65,53 +45,56 @@ executables: []
65
45
  extensions: []
66
46
  extra_rdoc_files: []
67
47
  files:
68
- - lib/generators/gitlab
69
- - lib/generators/gitlab/experiment
70
- - lib/generators/gitlab/experiment/install
71
- - lib/generators/gitlab/experiment/install/install_generator.rb
72
- - lib/generators/gitlab/experiment/install/templates
73
- - lib/generators/gitlab/experiment/install/templates/application_experiment.rb.tt
74
- - lib/generators/gitlab/experiment/install/templates/initializer.rb.tt
75
- - lib/generators/gitlab/experiment/install/templates/POST_INSTALL
76
- - lib/generators/gitlab/experiment/USAGE
77
- - lib/generators/gitlab/experiment/experiment_generator.rb
78
- - lib/generators/gitlab/experiment/templates
79
- - lib/generators/gitlab/experiment/templates/experiment.rb.tt
80
48
  - lib/generators/test_unit
81
49
  - lib/generators/test_unit/experiment
82
- - lib/generators/test_unit/experiment/experiment_generator.rb
83
50
  - lib/generators/test_unit/experiment/templates
84
51
  - lib/generators/test_unit/experiment/templates/experiment_test.rb.tt
52
+ - lib/generators/test_unit/experiment/experiment_generator.rb
85
53
  - lib/generators/rspec
86
54
  - lib/generators/rspec/experiment
87
- - lib/generators/rspec/experiment/experiment_generator.rb
88
55
  - lib/generators/rspec/experiment/templates
89
56
  - lib/generators/rspec/experiment/templates/experiment_spec.rb.tt
90
- - lib/gitlab/experiment.rb
57
+ - lib/generators/rspec/experiment/experiment_generator.rb
58
+ - lib/generators/gitlab
59
+ - lib/generators/gitlab/experiment
60
+ - lib/generators/gitlab/experiment/USAGE
61
+ - lib/generators/gitlab/experiment/templates
62
+ - lib/generators/gitlab/experiment/templates/experiment.rb.tt
63
+ - lib/generators/gitlab/experiment/install
64
+ - lib/generators/gitlab/experiment/install/templates
65
+ - lib/generators/gitlab/experiment/install/templates/application_experiment.rb.tt
66
+ - lib/generators/gitlab/experiment/install/templates/initializer.rb.tt
67
+ - lib/generators/gitlab/experiment/install/templates/POST_INSTALL
68
+ - lib/generators/gitlab/experiment/install/install_generator.rb
69
+ - lib/generators/gitlab/experiment/experiment_generator.rb
91
70
  - lib/gitlab/experiment
92
- - lib/gitlab/experiment/variant.rb
93
- - lib/gitlab/experiment/middleware.rb
71
+ - lib/gitlab/experiment/dsl.rb
72
+ - lib/gitlab/experiment/rspec.rb
73
+ - lib/gitlab/experiment/context.rb
74
+ - lib/gitlab/experiment/nestable.rb
75
+ - lib/gitlab/experiment/configuration.rb
76
+ - lib/gitlab/experiment/rollout.rb
94
77
  - lib/gitlab/experiment/cache
95
78
  - lib/gitlab/experiment/cache/redis_hash_store.rb
96
- - lib/gitlab/experiment/errors.rb
97
- - lib/gitlab/experiment/callbacks.rb
98
- - lib/gitlab/experiment/rollout.rb
99
- - lib/gitlab/experiment/base_interface.rb
100
- - lib/gitlab/experiment/context.rb
101
79
  - lib/gitlab/experiment/engine.rb
102
- - lib/gitlab/experiment/rspec.rb
80
+ - lib/gitlab/experiment/base_interface.rb
81
+ - lib/gitlab/experiment/middleware.rb
82
+ - lib/gitlab/experiment/version.rb
83
+ - lib/gitlab/experiment/cookies.rb
84
+ - lib/gitlab/experiment/errors.rb
85
+ - lib/gitlab/experiment/cache.rb
86
+ - lib/gitlab/experiment/variant.rb
103
87
  - lib/gitlab/experiment/rollout
104
88
  - lib/gitlab/experiment/rollout/random.rb
105
- - lib/gitlab/experiment/rollout/round_robin.rb
106
89
  - lib/gitlab/experiment/rollout/percent.rb
107
- - lib/gitlab/experiment/cache.rb
108
- - lib/gitlab/experiment/version.rb
109
- - lib/gitlab/experiment/cookies.rb
110
- - lib/gitlab/experiment/configuration.rb
111
- - lib/gitlab/experiment/dsl.rb
90
+ - lib/gitlab/experiment/rollout/round_robin.rb
91
+ - lib/gitlab/experiment/callbacks.rb
92
+ - lib/gitlab/experiment/test_behaviors
93
+ - lib/gitlab/experiment/test_behaviors/trackable.rb
94
+ - lib/gitlab/experiment.rb
112
95
  - LICENSE.txt
113
96
  - README.md
114
- homepage: https://gitlab.com/gitlab-org/gitlab-experiment
97
+ homepage: https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment
115
98
  licenses:
116
99
  - MIT
117
100
  metadata: {}
@@ -130,8 +113,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
113
  - !ruby/object:Gem::Version
131
114
  version: '0'
132
115
  requirements: []
133
- rubygems_version: 3.1.4
116
+ rubygems_version: 3.1.6
134
117
  signing_key:
135
118
  specification_version: 4
136
- summary: GitLab experiment library built on top of scientist.
119
+ summary: GitLab experimentation library.
137
120
  test_files: []