gitlab-experiment 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []