gitlab-experiment 0.6.4 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []