split 3.4.1 → 4.0.0.pre

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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.rubocop.yml +177 -1
  4. data/.rubocop_todo.yml +40 -493
  5. data/.travis.yml +14 -42
  6. data/CHANGELOG.md +35 -0
  7. data/Gemfile +1 -0
  8. data/README.md +19 -1
  9. data/Rakefile +1 -0
  10. data/lib/split.rb +8 -2
  11. data/lib/split/algorithms/block_randomization.rb +1 -0
  12. data/lib/split/algorithms/weighted_sample.rb +2 -1
  13. data/lib/split/algorithms/whiplash.rb +3 -2
  14. data/lib/split/alternative.rb +1 -0
  15. data/lib/split/cache.rb +28 -0
  16. data/lib/split/combined_experiments_helper.rb +1 -0
  17. data/lib/split/configuration.rb +6 -12
  18. data/lib/split/dashboard.rb +17 -2
  19. data/lib/split/dashboard/helpers.rb +1 -0
  20. data/lib/split/dashboard/pagination_helpers.rb +1 -0
  21. data/lib/split/dashboard/paginator.rb +1 -0
  22. data/lib/split/dashboard/public/dashboard.js +10 -0
  23. data/lib/split/dashboard/public/style.css +5 -0
  24. data/lib/split/dashboard/views/_controls.erb +13 -0
  25. data/lib/split/encapsulated_helper.rb +3 -2
  26. data/lib/split/engine.rb +1 -0
  27. data/lib/split/exceptions.rb +1 -0
  28. data/lib/split/experiment.rb +81 -59
  29. data/lib/split/experiment_catalog.rb +1 -3
  30. data/lib/split/extensions/string.rb +1 -0
  31. data/lib/split/goals_collection.rb +1 -0
  32. data/lib/split/helper.rb +26 -7
  33. data/lib/split/metric.rb +2 -1
  34. data/lib/split/persistence.rb +4 -2
  35. data/lib/split/persistence/cookie_adapter.rb +1 -0
  36. data/lib/split/persistence/redis_adapter.rb +5 -0
  37. data/lib/split/persistence/session_adapter.rb +1 -0
  38. data/lib/split/redis_interface.rb +8 -28
  39. data/lib/split/trial.rb +20 -10
  40. data/lib/split/user.rb +14 -2
  41. data/lib/split/version.rb +2 -4
  42. data/lib/split/zscore.rb +1 -0
  43. data/spec/alternative_spec.rb +1 -1
  44. data/spec/cache_spec.rb +88 -0
  45. data/spec/configuration_spec.rb +1 -14
  46. data/spec/dashboard_spec.rb +45 -5
  47. data/spec/encapsulated_helper_spec.rb +1 -1
  48. data/spec/experiment_spec.rb +78 -7
  49. data/spec/goals_collection_spec.rb +1 -1
  50. data/spec/helper_spec.rb +68 -32
  51. data/spec/persistence/cookie_adapter_spec.rb +1 -1
  52. data/spec/persistence/redis_adapter_spec.rb +9 -0
  53. data/spec/redis_interface_spec.rb +0 -69
  54. data/spec/spec_helper.rb +5 -6
  55. data/spec/trial_spec.rb +45 -19
  56. data/spec/user_spec.rb +17 -0
  57. data/split.gemspec +7 -7
  58. metadata +23 -34
  59. data/gemfiles/4.2.gemfile +0 -9
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  module Persistence
4
5
  class SessionAdapter
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  # Simplifies the interface to Redis.
4
5
  class RedisInterface
@@ -7,40 +8,19 @@ module Split
7
8
  end
8
9
 
9
10
  def persist_list(list_name, list_values)
10
- max_index = list_length(list_name) - 1
11
- list_values.each_with_index do |value, index|
12
- if index > max_index
13
- add_to_list(list_name, value)
14
- else
15
- set_list_index(list_name, index, value)
11
+ if list_values.length > 0
12
+ redis.multi do |multi|
13
+ tmp_list = "#{list_name}_tmp"
14
+ multi.rpush(tmp_list, list_values)
15
+ multi.rename(tmp_list, list_name)
16
16
  end
17
17
  end
18
- make_list_length(list_name, list_values.length)
19
- list_values
20
- end
21
-
22
- def add_to_list(list_name, value)
23
- redis.rpush(list_name, value)
24
- end
25
-
26
- def set_list_index(list_name, index, value)
27
- redis.lset(list_name, index, value)
28
- end
29
-
30
- def list_length(list_name)
31
- redis.llen(list_name)
32
- end
33
18
 
34
- def remove_last_item_from_list(list_name)
35
- redis.rpop(list_name)
36
- end
37
-
38
- def make_list_length(list_name, new_length)
39
- redis.ltrim(list_name, 0, new_length - 1)
19
+ list_values
40
20
  end
41
21
 
42
22
  def add_to_set(set_name, value)
43
- redis.sadd(set_name, value) unless redis.sismember(set_name, value)
23
+ redis.sadd(set_name, value)
44
24
  end
45
25
 
46
26
  private
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class Trial
5
+ attr_accessor :goals
4
6
  attr_accessor :experiment
5
7
  attr_writer :metadata
6
8
 
@@ -8,11 +10,12 @@ module Split
8
10
  self.experiment = attrs.delete(:experiment)
9
11
  self.alternative = attrs.delete(:alternative)
10
12
  self.metadata = attrs.delete(:metadata)
13
+ self.goals = attrs.delete(:goals) || []
11
14
 
12
15
  @user = attrs.delete(:user)
13
16
  @options = attrs
14
17
 
15
- @alternative_choosen = false
18
+ @alternative_chosen = false
16
19
  end
17
20
 
18
21
  def metadata
@@ -33,7 +36,7 @@ module Split
33
36
  end
34
37
  end
35
38
 
36
- def complete!(goals=[], context = nil)
39
+ def complete!(context = nil)
37
40
  if alternative
38
41
  if Array(goals).empty?
39
42
  alternative.increment_completion
@@ -51,8 +54,9 @@ module Split
51
54
  def choose!(context = nil)
52
55
  @user.cleanup_old_experiments!
53
56
  # Only run the process once
54
- return alternative if @alternative_choosen
57
+ return alternative if @alternative_chosen
55
58
 
59
+ new_participant = @user[@experiment.key].nil?
56
60
  if override_is_alternative?
57
61
  self.alternative = @options[:override]
58
62
  if should_store_alternative? && !@user[@experiment.key]
@@ -70,19 +74,25 @@ module Split
70
74
  else
71
75
  self.alternative = @user[@experiment.key]
72
76
  if alternative.nil?
73
- self.alternative = @experiment.next_alternative
77
+ if @experiment.cohorting_disabled?
78
+ self.alternative = @experiment.control
79
+ else
80
+ self.alternative = @experiment.next_alternative
74
81
 
75
- # Increment the number of participants since we are actually choosing a new alternative
76
- self.alternative.increment_participation
82
+ # Increment the number of participants since we are actually choosing a new alternative
83
+ self.alternative.increment_participation
77
84
 
78
- run_callback context, Split.configuration.on_trial_choose
85
+ run_callback context, Split.configuration.on_trial_choose
86
+ end
79
87
  end
80
88
  end
81
89
  end
82
90
 
83
- @user[@experiment.key] = alternative.name if !@experiment.has_winner? && should_store_alternative?
84
- @alternative_choosen = true
85
- run_callback context, Split.configuration.on_trial unless @options[:disabled] || Split.configuration.disabled?
91
+ new_participant_and_cohorting_disabled = new_participant && @experiment.cohorting_disabled?
92
+
93
+ @user[@experiment.key] = alternative.name unless @experiment.has_winner? || !should_store_alternative? || new_participant_and_cohorting_disabled
94
+ @alternative_chosen = true
95
+ run_callback context, Split.configuration.on_trial unless @options[:disabled] || Split.configuration.disabled? || new_participant_and_cohorting_disabled
86
96
  alternative
87
97
  end
88
98
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'forwardable'
3
4
 
4
5
  module Split
@@ -7,7 +8,7 @@ module Split
7
8
  def_delegators :@user, :keys, :[], :[]=, :delete
8
9
  attr_reader :user
9
10
 
10
- def initialize(context, adapter=nil)
11
+ def initialize(context, adapter = nil)
11
12
  @user = adapter || Split::Persistence.adapter.new(context)
12
13
  @cleaned_up = false
13
14
  end
@@ -27,7 +28,8 @@ module Split
27
28
  def max_experiments_reached?(experiment_key)
28
29
  if Split.configuration.allow_multiple_experiments == 'control'
29
30
  experiments = active_experiments
30
- count_control = experiments.count {|k,v| k == experiment_key || v == 'control'}
31
+ experiment_key_without_version = key_without_version(experiment_key)
32
+ count_control = experiments.count {|k, v| k == experiment_key_without_version || v == 'control'}
31
33
  experiments.size > count_control
32
34
  else
33
35
  !Split.configuration.allow_multiple_experiments &&
@@ -52,6 +54,16 @@ module Split
52
54
  experiment_pairs
53
55
  end
54
56
 
57
+ def self.find(user_id, adapter)
58
+ adapter = adapter.is_a?(Symbol) ? Split::Persistence::ADAPTERS[adapter] : adapter
59
+
60
+ if adapter.respond_to?(:find)
61
+ User.new(nil, adapter.find(user_id))
62
+ else
63
+ nil
64
+ end
65
+ end
66
+
55
67
  private
56
68
 
57
69
  def keys_without_experiment(keys, experiment_key)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
- MAJOR = 3
4
- MINOR = 4
5
- PATCH = 1
6
- VERSION = [MAJOR, MINOR, PATCH].join('.')
4
+ VERSION = "4.0.0.pre"
7
5
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class Zscore
4
5
 
@@ -126,7 +126,7 @@ describe Split::Alternative do
126
126
 
127
127
  it "should save to redis" do
128
128
  alternative.save
129
- expect(Split.redis.exists('basket_text:Basket')).to be true
129
+ expect(Split.redis.exists?('basket_text:Basket')).to be true
130
130
  end
131
131
 
132
132
  it "should increment participation count" do
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ describe Split::Cache do
5
+
6
+ let(:namespace) { :test_namespace }
7
+ let(:key) { :test_key }
8
+ let(:now) { 1606189017 }
9
+
10
+ before { allow(Time).to receive(:now).and_return(now) }
11
+
12
+ describe 'clear' do
13
+
14
+ before { Split.configuration.cache = true }
15
+
16
+ it 'clears the cache' do
17
+ expect(Time).to receive(:now).and_return(now).exactly(2).times
18
+ Split::Cache.fetch(namespace, key) { Time.now }
19
+ Split::Cache.clear
20
+ Split::Cache.fetch(namespace, key) { Time.now }
21
+ end
22
+ end
23
+
24
+ describe 'clear_key' do
25
+ before { Split.configuration.cache = true }
26
+
27
+ it 'clears the cache' do
28
+ expect(Time).to receive(:now).and_return(now).exactly(3).times
29
+ Split::Cache.fetch(namespace, :key1) { Time.now }
30
+ Split::Cache.fetch(namespace, :key2) { Time.now }
31
+ Split::Cache.clear_key(:key1)
32
+
33
+ Split::Cache.fetch(namespace, :key1) { Time.now }
34
+ Split::Cache.fetch(namespace, :key2) { Time.now }
35
+ end
36
+ end
37
+
38
+ describe 'fetch' do
39
+
40
+ subject { Split::Cache.fetch(namespace, key) { Time.now } }
41
+
42
+ context 'when cache disabled' do
43
+
44
+ before { Split.configuration.cache = false }
45
+
46
+ it 'returns the yield' do
47
+ expect(subject).to eql(now)
48
+ end
49
+
50
+ it 'yields every time' do
51
+ expect(Time).to receive(:now).and_return(now).exactly(2).times
52
+ Split::Cache.fetch(namespace, key) { Time.now }
53
+ Split::Cache.fetch(namespace, key) { Time.now }
54
+ end
55
+ end
56
+
57
+ context 'when cache enabled' do
58
+
59
+ before { Split.configuration.cache = true }
60
+
61
+ it 'returns the yield' do
62
+ expect(subject).to eql(now)
63
+ end
64
+
65
+ it 'yields once' do
66
+ expect(Time).to receive(:now).and_return(now).once
67
+ Split::Cache.fetch(namespace, key) { Time.now }
68
+ Split::Cache.fetch(namespace, key) { Time.now }
69
+ end
70
+
71
+ it 'honors namespace' do
72
+ expect(Split::Cache.fetch(:a, key) { :a }).to eql(:a)
73
+ expect(Split::Cache.fetch(:b, key) { :b }).to eql(:b)
74
+
75
+ expect(Split::Cache.fetch(:a, key) { :a }).to eql(:a)
76
+ expect(Split::Cache.fetch(:b, key) { :b }).to eql(:b)
77
+ end
78
+
79
+ it 'honors key' do
80
+ expect(Split::Cache.fetch(namespace, :a) { :a }).to eql(:a)
81
+ expect(Split::Cache.fetch(namespace, :b) { :b }).to eql(:b)
82
+
83
+ expect(Split::Cache.fetch(namespace, :a) { :a }).to eql(:a)
84
+ expect(Split::Cache.fetch(namespace, :b) { :b }).to eql(:b)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -212,20 +212,6 @@ describe Split::Configuration do
212
212
  expect(@config.normalized_experiments).to eq({:my_experiment=>{:alternatives=>[{"control_opt"=>0.67}, [{"second_opt"=>0.1}, {"third_opt"=>0.23}]]}})
213
213
  end
214
214
 
215
- context 'redis_url configuration [DEPRECATED]' do
216
- it 'should warn on set and assign to #redis' do
217
- expect(@config).to receive(:warn).with(/\[DEPRECATED\]/) { nil }
218
- @config.redis_url = 'example_url'
219
- expect(@config.redis).to eq('example_url')
220
- end
221
-
222
- it 'should warn on get and return #redis' do
223
- expect(@config).to receive(:warn).with(/\[DEPRECATED\]/) { nil }
224
- @config.redis = 'example_url'
225
- expect(@config.redis_url).to eq('example_url')
226
- end
227
- end
228
-
229
215
  context "redis configuration" do
230
216
  it "should default to local redis server" do
231
217
  expect(@config.redis).to eq("redis://localhost:6379")
@@ -240,6 +226,7 @@ describe Split::Configuration do
240
226
  it "should use the ENV variable" do
241
227
  ENV['REDIS_URL'] = "env_redis_url"
242
228
  expect(Split::Configuration.new.redis).to eq("env_redis_url")
229
+ ENV.delete('REDIS_URL')
243
230
  end
244
231
  end
245
232
  end
@@ -6,8 +6,16 @@ require 'split/dashboard'
6
6
  describe Split::Dashboard do
7
7
  include Rack::Test::Methods
8
8
 
9
+ class TestDashboard < Split::Dashboard
10
+ include Split::Helper
11
+
12
+ get '/my_experiment' do
13
+ ab_test(params[:experiment], 'blue', 'red')
14
+ end
15
+ end
16
+
9
17
  def app
10
- @app ||= Split::Dashboard
18
+ @app ||= TestDashboard
11
19
  end
12
20
 
13
21
  def link(color)
@@ -90,8 +98,17 @@ describe Split::Dashboard do
90
98
  it "should set current user's alternative" do
91
99
  blue_link.participant_count = 7
92
100
  post "/force_alternative?experiment=#{experiment.name}", alternative: "blue"
93
- expect(user[experiment.key]).to eq("blue")
94
- expect(blue_link.participant_count).to eq(8)
101
+
102
+ get "/my_experiment?experiment=#{experiment.name}"
103
+ expect(last_response.body).to include("blue")
104
+ end
105
+
106
+ it "should not modify an existing user" do
107
+ blue_link.participant_count = 7
108
+ post "/force_alternative?experiment=#{experiment.name}", alternative: "blue"
109
+
110
+ expect(user[experiment.key]).to eq("red")
111
+ expect(blue_link.participant_count).to eq(7)
95
112
  end
96
113
  end
97
114
 
@@ -108,8 +125,9 @@ describe Split::Dashboard do
108
125
  it "should set current user's alternative" do
109
126
  blue_link.participant_count = 7
110
127
  post "/force_alternative?experiment=#{experiment.name}", alternative: "blue"
111
- expect(user[experiment.key]).to eq("blue")
112
- expect(blue_link.participant_count).to eq(8)
128
+
129
+ get "/my_experiment?experiment=#{experiment.name}"
130
+ expect(last_response.body).to include("blue")
113
131
  end
114
132
  end
115
133
  end
@@ -161,6 +179,28 @@ describe Split::Dashboard do
161
179
  end
162
180
  end
163
181
 
182
+ describe "update cohorting" do
183
+ it "calls enable of cohorting when action is enable" do
184
+ post "/update_cohorting?experiment=#{experiment.name}", { "cohorting_action": "enable" }
185
+
186
+ expect(experiment.cohorting_disabled?).to eq false
187
+ end
188
+
189
+ it "calls disable of cohorting when action is disable" do
190
+ post "/update_cohorting?experiment=#{experiment.name}", { "cohorting_action": "disable" }
191
+
192
+ expect(experiment.cohorting_disabled?).to eq true
193
+ end
194
+
195
+ it "calls neither enable or disable cohorting when passed invalid action" do
196
+ previous_value = experiment.cohorting_disabled?
197
+
198
+ post "/update_cohorting?experiment=#{experiment.name}", { "cohorting_action": "other" }
199
+
200
+ expect(experiment.cohorting_disabled?).to eq previous_value
201
+ end
202
+ end
203
+
164
204
  it "should reset an experiment" do
165
205
  red_link.participant_count = 5
166
206
  blue_link.participant_count = 7
@@ -21,7 +21,7 @@ describe Split::EncapsulatedHelper do
21
21
  end
22
22
 
23
23
  it "calls the block with selected alternative" do
24
- expect{|block| ab_test('link_color', 'red', 'red', &block) }.to yield_with_args('red', nil)
24
+ expect{|block| ab_test('link_color', 'red', 'red', &block) }.to yield_with_args('red', {})
25
25
  end
26
26
 
27
27
  context "inside a view" do
@@ -37,7 +37,7 @@ describe Split::Experiment do
37
37
 
38
38
  it "should save to redis" do
39
39
  experiment.save
40
- expect(Split.redis.exists('basket_text')).to be true
40
+ expect(Split.redis.exists?('basket_text')).to be true
41
41
  end
42
42
 
43
43
  it "should save the start time to redis" do
@@ -85,7 +85,7 @@ describe Split::Experiment do
85
85
  it "should not create duplicates when saving multiple times" do
86
86
  experiment.save
87
87
  experiment.save
88
- expect(Split.redis.exists('basket_text')).to be true
88
+ expect(Split.redis.exists?('basket_text')).to be true
89
89
  expect(Split.redis.lrange('basket_text', 0, -1)).to eq(['{"Basket":1}', '{"Cart":1}'])
90
90
  end
91
91
 
@@ -118,6 +118,23 @@ describe Split::Experiment do
118
118
  experiment = Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :resettable => false)
119
119
  expect(experiment.resettable).to be_falsey
120
120
  end
121
+
122
+ context 'from configuration' do
123
+ let(:experiment_name) { :my_experiment }
124
+ let(:experiments) do
125
+ {
126
+ experiment_name => {
127
+ :alternatives => ['Control Opt', 'Alt one']
128
+ }
129
+ }
130
+ end
131
+
132
+ before { Split.configuration.experiments = experiments }
133
+
134
+ it 'assigns default values to the experiment' do
135
+ expect(Split::Experiment.new(experiment_name).resettable).to eq(true)
136
+ end
137
+ end
121
138
  end
122
139
 
123
140
  describe 'persistent configuration' do
@@ -134,10 +151,23 @@ describe Split::Experiment do
134
151
 
135
152
  describe '#metadata' do
136
153
  let(:experiment) { Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :algorithm => Split::Algorithms::Whiplash, :metadata => meta) }
154
+ let(:meta) { { a: 'b' }}
155
+
156
+ before do
157
+ experiment.save
158
+ end
159
+
160
+ it "should delete the key when metadata is removed" do
161
+ experiment.metadata = nil
162
+ experiment.save
163
+
164
+ expect(Split.redis.exists?(experiment.metadata_key)).to be_falsey
165
+ end
166
+
137
167
  context 'simple hash' do
138
168
  let(:meta) { { 'basket' => 'a', 'cart' => 'b' } }
169
+
139
170
  it "should persist metadata in redis" do
140
- experiment.save
141
171
  e = Split::ExperimentCatalog.find('basket_text')
142
172
  expect(e).to eq(experiment)
143
173
  expect(e.metadata).to eq(meta)
@@ -147,7 +177,6 @@ describe Split::Experiment do
147
177
  context 'nested hash' do
148
178
  let(:meta) { { 'basket' => { 'one' => 'two' }, 'cart' => 'b' } }
149
179
  it "should persist metadata in redis" do
150
- experiment.save
151
180
  e = Split::ExperimentCatalog.find('basket_text')
152
181
  expect(e).to eq(experiment)
153
182
  expect(e.metadata).to eq(meta)
@@ -180,7 +209,7 @@ describe Split::Experiment do
180
209
  experiment.save
181
210
 
182
211
  experiment.delete
183
- expect(Split.redis.exists('link_color')).to be false
212
+ expect(Split.redis.exists?('link_color')).to be false
184
213
  expect(Split::ExperimentCatalog.find('link_color')).to be_nil
185
214
  end
186
215
 
@@ -206,8 +235,14 @@ describe Split::Experiment do
206
235
  experiment.delete
207
236
  expect(experiment.start_time).to be_nil
208
237
  end
209
- end
210
238
 
239
+ it "should default cohorting back to false" do
240
+ experiment.disable_cohorting
241
+ expect(experiment.cohorting_disabled?).to eq(true)
242
+ experiment.delete
243
+ expect(experiment.cohorting_disabled?).to eq(false)
244
+ end
245
+ end
211
246
 
212
247
  describe 'winner' do
213
248
  it "should have no winner initially" do
@@ -216,12 +251,17 @@ describe Split::Experiment do
216
251
  end
217
252
 
218
253
  describe 'winner=' do
219
- it "should allow you to specify a winner" do
254
+ it 'should allow you to specify a winner' do
220
255
  experiment.save
221
256
  experiment.winner = 'red'
222
257
  expect(experiment.winner.name).to eq('red')
223
258
  end
224
259
 
260
+ it 'should call the on_experiment_winner_choose hook' do
261
+ expect(Split.configuration.on_experiment_winner_choose).to receive(:call)
262
+ experiment.winner = 'green'
263
+ end
264
+
225
265
  context 'when has_winner state is memoized' do
226
266
  before { expect(experiment).to_not have_winner }
227
267
 
@@ -370,6 +410,22 @@ describe Split::Experiment do
370
410
  end
371
411
  end
372
412
 
413
+ describe "#cohorting_disabled?" do
414
+ it "returns false when nothing has been configured" do
415
+ expect(experiment.cohorting_disabled?).to eq false
416
+ end
417
+
418
+ it "returns true when enable_cohorting is performed" do
419
+ experiment.enable_cohorting
420
+ expect(experiment.cohorting_disabled?).to eq false
421
+ end
422
+
423
+ it "returns false when nothing has been configured" do
424
+ experiment.disable_cohorting
425
+ expect(experiment.cohorting_disabled?).to eq true
426
+ end
427
+ end
428
+
373
429
  describe 'changing an existing experiment' do
374
430
  def same_but_different_alternative
375
431
  Split::ExperimentCatalog.find_or_create('link_color', 'blue', 'yellow', 'orange')
@@ -392,6 +448,21 @@ describe Split::Experiment do
392
448
  expect(same_experiment_again.version).to eq(1)
393
449
  end
394
450
 
451
+ context "when metadata is changed" do
452
+ it "should increase version" do
453
+ experiment.save
454
+ experiment.metadata = { 'foo' => 'bar' }
455
+
456
+ expect { experiment.save }.to change { experiment.version }.by(1)
457
+ end
458
+
459
+ it "does not increase version" do
460
+ experiment.metadata = nil
461
+ experiment.save
462
+ expect { experiment.save }.to change { experiment.version }.by(0)
463
+ end
464
+ end
465
+
395
466
  context 'when experiment configuration is changed' do
396
467
  let(:reset_manually) { false }
397
468