gitlab-experiment 0.6.4 → 0.7.1

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