split 4.0.2 → 4.0.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1c97063d4d1ccf4c2cd5dfd2e83b98b3f55a934b9dfa9b5862ede3ee5b8c58c
4
- data.tar.gz: 28e30003a6d2059baf91482f5ac9a97b0b7d2e37c61a425710cbf1adf21a4a83
3
+ metadata.gz: 80d095f07432d336e773b30c3c5a873b554dd682f65ace33886d1a83697d447a
4
+ data.tar.gz: 9a504a3c9d4c67391528e1640c2c7eac3cfcafe91305cadb901e48541726f04c
5
5
  SHA512:
6
- metadata.gz: 988f115f0f96870188221552cca45f6a7207f548500dbf2a5446abaa47ac7365571644a5a231443ca7616182b68c3139f8d62fdcf18d1593fca8898092a332e2
7
- data.tar.gz: b6a668159d8b6fe9529bae5bc650f120b9de1bdf593bc89d0d949a2e07e8cfda7c96b4c12e5e4615696cf31b845215f95ce60b4abcb9ff3d7ef056669a4ae51d
6
+ metadata.gz: 68e5919618103f315fa9f4f0d18384392c02c01c680c13d50a77cfa0507bd19a1c01fb5c1db66619329c1ef38013c3ebe90e880f6af5c9bac9ba2d1a76b3963a
7
+ data.tar.gz: 1ae5fe0187e16b4bb3efc14a0e0ded4df98527fbc530dab4a89c544d133b1a7e8cd0adc6579f0f43ea3c84aa6899a2d123e97c5af105ce158e804a5ac8286728
@@ -37,6 +37,8 @@ jobs:
37
37
  - gemfile: 7.0.gemfile
38
38
  ruby: '3.1'
39
39
 
40
+ - gemfile: 7.0.gemfile
41
+ ruby: '3.2'
40
42
 
41
43
  runs-on: ubuntu-latest
42
44
 
@@ -51,7 +53,7 @@ jobs:
51
53
  --health-retries 5
52
54
 
53
55
  steps:
54
- - uses: actions/checkout@v3
56
+ - uses: actions/checkout@v4
55
57
 
56
58
  - uses: ruby/setup-ruby@v1
57
59
  with:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ # 4.0.4 (March 3rd, 2024)
2
+
3
+ Bugfixes:
4
+ - Better integration for EncapsulatedHelper when needing params/request info (@henrique-ft, #721 and #723)
5
+
6
+ Misc:
7
+ - Make specs compatible with newer Rack versions (@andrehjr, #722)
8
+
9
+ # 4.0.3 (November 15th, 2023)
10
+
11
+ Bugfixes:
12
+ - Do not throw error if alternativas have data that can lead to negative numbers for probability calculation (@andrehjr, #703)
13
+ - Do not persist invalid extra_info on ab_record_extra_info. (@trostli @andrehjr, #717)
14
+ - CROSSSLOT keys issue fix when using redis cluster (@naveen-chidhambaram, #710)
15
+ - Convert value to string before saving it in RedisAdapter (@Jealrock, #714)
16
+ - Fix deprecation warning with Redis 4.8.0 (@martingregoire, #701)
17
+
18
+ Misc:
19
+ - Add matrix as a default dependency (@andrehjr, #705)
20
+ - Add Ruby 3.2 to Github Actions (@andrehjr, #702)
21
+ - Update documentation regarding finding users outside a web session (@andrehjr, #716)
22
+ - Update actions/checkout to v4 (@andrehjr, #718)
23
+
1
24
  # 4.0.2 (December 2nd, 2022)
2
25
 
3
26
  Bugfixes:
data/Gemfile CHANGED
@@ -5,5 +5,4 @@ source "https://rubygems.org"
5
5
  gemspec
6
6
 
7
7
  gem "rubocop", require: false
8
- gem "matrix"
9
8
  gem "codeclimate-test-reporter"
data/README.md CHANGED
@@ -807,10 +807,16 @@ conduct experiments that are not tied to a web session.
807
807
  ```ruby
808
808
  # create a new experiment
809
809
  experiment = Split::ExperimentCatalog.find_or_create('color', 'red', 'blue')
810
+
811
+ # find the user
812
+ user = Split::User.find(user_id, :redis)
813
+
810
814
  # create a new trial
811
- trial = Split::Trial.new(:experiment => experiment)
815
+ trial = Split::Trial.new(user: user, experiment: experiment)
816
+
812
817
  # run trial
813
818
  trial.choose!
819
+
814
820
  # get the result, returns either red or blue
815
821
  trial.alternative.name
816
822
 
data/gemfiles/7.0.gemfile CHANGED
@@ -3,6 +3,5 @@ source "https://rubygems.org"
3
3
  gem "rubocop", require: false
4
4
  gem "codeclimate-test-reporter"
5
5
  gem "rails", "~> 7.0"
6
- gem "matrix"
7
6
 
8
7
  gemspec path: "../"
@@ -1,14 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- begin
4
- require "matrix"
5
- rescue LoadError => error
6
- if error.message.match?(/matrix/)
7
- $stderr.puts "You don't have matrix installed in your application. Please add it to your Gemfile and run bundle install"
8
- raise
9
- end
10
- end
11
-
3
+ require "matrix"
12
4
  require "rubystats"
13
5
 
14
6
  module Split
@@ -16,7 +16,8 @@
16
16
  summary_texts = {}
17
17
  extra_columns.each do |column|
18
18
  extra_infos = experiment.alternatives.map(&:extra_info).select{|extra_info| extra_info && extra_info[column] }
19
- if extra_infos[0][column].kind_of?(Numeric)
19
+
20
+ if extra_infos.length > 0 && extra_infos.all? { |extra_info| extra_info[column].kind_of?(Numeric) }
20
21
  summary_texts[column] = extra_infos.inject(0){|sum, extra_info| sum += extra_info[column]}
21
22
  else
22
23
  summary_texts[column] = "N/A"
@@ -23,6 +23,14 @@ module Split
23
23
  @context = context
24
24
  end
25
25
 
26
+ def params
27
+ request.params if request && request.respond_to?(:params)
28
+ end
29
+
30
+ def request
31
+ @context.request if @context.respond_to?(:request)
32
+ end
33
+
26
34
  def ab_user
27
35
  @ab_user ||= Split::User.new(@context)
28
36
  end
@@ -270,7 +270,16 @@ module Split
270
270
  set_alternatives_and_options(options)
271
271
  end
272
272
 
273
+ def can_calculate_winning_alternatives?
274
+ self.alternatives.all? do |alternative|
275
+ alternative.participant_count >= 0 &&
276
+ (alternative.participant_count >= alternative.completed_count)
277
+ end
278
+ end
279
+
273
280
  def calc_winning_alternatives
281
+ return unless can_calculate_winning_alternatives?
282
+
274
283
  # Cache the winning alternatives so we recalculate them once per the specified interval.
275
284
  intervals_since_epoch =
276
285
  Time.now.utc.to_i / Split.configuration.winning_alternative_recalculation_interval
data/lib/split/helper.rb CHANGED
@@ -86,7 +86,7 @@ module Split
86
86
  end
87
87
 
88
88
  def ab_record_extra_info(metric_descriptor, key, value = 1)
89
- return if exclude_visitor? || Split.configuration.disabled?
89
+ return if exclude_visitor? || Split.configuration.disabled? || value.nil?
90
90
  metric_descriptor, _ = normalize_metric(metric_descriptor)
91
91
  experiments = Metric.possible_experiments(metric_descriptor)
92
92
 
@@ -121,11 +121,11 @@ module Split
121
121
  end
122
122
 
123
123
  def override_alternative_by_params(experiment_name)
124
- defined?(params) && params[OVERRIDE_PARAM_NAME] && params[OVERRIDE_PARAM_NAME][experiment_name]
124
+ params_present? && params[OVERRIDE_PARAM_NAME] && params[OVERRIDE_PARAM_NAME][experiment_name]
125
125
  end
126
126
 
127
127
  def override_alternative_by_cookies(experiment_name)
128
- return unless defined?(request)
128
+ return unless request_present?
129
129
 
130
130
  if request.cookies && request.cookies.key?("split_override")
131
131
  experiments = JSON.parse(request.cookies["split_override"]) rescue {}
@@ -134,7 +134,7 @@ module Split
134
134
  end
135
135
 
136
136
  def split_generically_disabled?
137
- defined?(params) && params["SPLIT_DISABLE"]
137
+ params_present? && params["SPLIT_DISABLE"]
138
138
  end
139
139
 
140
140
  def ab_user
@@ -142,26 +142,34 @@ module Split
142
142
  end
143
143
 
144
144
  def exclude_visitor?
145
- defined?(request) && (instance_exec(request, &Split.configuration.ignore_filter) || is_ignored_ip_address? || is_robot? || is_preview?)
145
+ request_present? && (instance_exec(request, &Split.configuration.ignore_filter) || is_ignored_ip_address? || is_robot? || is_preview?)
146
146
  end
147
147
 
148
148
  def is_robot?
149
- defined?(request) && request.user_agent =~ Split.configuration.robot_regex
149
+ request_present? && request.user_agent =~ Split.configuration.robot_regex
150
150
  end
151
151
 
152
152
  def is_preview?
153
- defined?(request) && defined?(request.headers) && request.headers["x-purpose"] == "preview"
153
+ request_present? && defined?(request.headers) && request.headers["x-purpose"] == "preview"
154
154
  end
155
155
 
156
156
  def is_ignored_ip_address?
157
157
  return false if Split.configuration.ignore_ip_addresses.empty?
158
158
 
159
159
  Split.configuration.ignore_ip_addresses.each do |ip|
160
- return true if defined?(request) && (request.ip == ip || (ip.class == Regexp && request.ip =~ ip))
160
+ return true if request_present? && (request.ip == ip || (ip.class == Regexp && request.ip =~ ip))
161
161
  end
162
162
  false
163
163
  end
164
164
 
165
+ def params_present?
166
+ defined?(params) && params
167
+ end
168
+
169
+ def request_present?
170
+ defined?(request) && request
171
+ end
172
+
165
173
  def active_experiments
166
174
  ab_user.active_experiments
167
175
  end
@@ -27,7 +27,7 @@ module Split
27
27
  end
28
28
 
29
29
  def []=(field, value)
30
- Split.redis.hset(redis_key, field, value)
30
+ Split.redis.hset(redis_key, field, value.to_s)
31
31
  expire_seconds = self.class.config[:expire_seconds]
32
32
  Split.redis.expire(redis_key, expire_seconds) if expire_seconds
33
33
  end
@@ -11,6 +11,7 @@ module Split
11
11
  if list_values.length > 0
12
12
  redis.multi do |multi|
13
13
  tmp_list = "#{list_name}_tmp"
14
+ tmp_list += redis_namespace_used? ? "{#{Split.redis.namespace}:#{list_name}}" : "{#{list_name}}"
14
15
  multi.rpush(tmp_list, list_values)
15
16
  multi.rename(tmp_list, list_name)
16
17
  end
@@ -20,10 +21,16 @@ module Split
20
21
  end
21
22
 
22
23
  def add_to_set(set_name, value)
24
+ return redis.sadd?(set_name, value) if redis.respond_to?(:sadd?)
25
+
23
26
  redis.sadd(set_name, value)
24
27
  end
25
28
 
26
29
  private
27
30
  attr_accessor :redis
31
+
32
+ def redis_namespace_used?
33
+ Redis.const_defined?("Namespace") && Split.redis.is_a?(Redis::Namespace)
34
+ end
28
35
  end
29
36
  end
data/lib/split/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Split
4
- VERSION = "4.0.2"
4
+ VERSION = "4.0.4"
5
5
  end
@@ -279,4 +279,16 @@ describe Split::Dashboard do
279
279
 
280
280
  expect(last_response.body).to include("<small>Unknown</small>")
281
281
  end
282
+
283
+ it "should be explode with experiments with invalid data" do
284
+ red_link.participant_count = 1
285
+ red_link.set_completed_count(10)
286
+
287
+ blue_link.participant_count = 3
288
+ blue_link.set_completed_count(2)
289
+
290
+ get "/"
291
+
292
+ expect(last_response).to be_ok
293
+ end
282
294
  end
@@ -3,11 +3,7 @@
3
3
  require "spec_helper"
4
4
 
5
5
  describe Split::EncapsulatedHelper do
6
- include Split::EncapsulatedHelper
7
-
8
- def params
9
- raise NoMethodError, "This method is not really defined"
10
- end
6
+ let(:context_shim) { Split::EncapsulatedHelper::ContextShim.new(double(request: request)) }
11
7
 
12
8
  describe "ab_test" do
13
9
  before do
@@ -15,20 +11,15 @@ describe Split::EncapsulatedHelper do
15
11
  .and_return(mock_user)
16
12
  end
17
13
 
18
- it "should not raise an error when params raises an error" do
19
- expect { params }.to raise_error(NoMethodError)
20
- expect { ab_test("link_color", "blue", "red") }.not_to raise_error
21
- end
22
-
23
14
  it "calls the block with selected alternative" do
24
- expect { |block| ab_test("link_color", "red", "red", &block) }.to yield_with_args("red", {})
15
+ expect { |block| context_shim.ab_test("link_color", "red", "red", &block) }.to yield_with_args("red", {})
25
16
  end
26
17
 
27
18
  context "inside a view" do
28
19
  it "works inside ERB" do
29
20
  require "erb"
30
21
  template = ERB.new(<<-ERB.split(/\s+/s).map(&:strip).join(" "), nil, "%")
31
- foo <% ab_test(:foo, '1', '2') do |alt, meta| %>
22
+ foo <% context_shim.ab_test(:foo, '1', '2') do |alt, meta| %>
32
23
  static <%= alt %>
33
24
  <% end %>
34
25
  ERB
@@ -43,8 +34,43 @@ describe Split::EncapsulatedHelper do
43
34
  include Split::EncapsulatedHelper
44
35
  public :session
45
36
  }.new
37
+
46
38
  expect(ctx).to receive(:session) { {} }
47
39
  expect { ctx.ab_test("link_color", "blue", "red") }.not_to raise_error
48
40
  end
41
+
42
+ context "when request is defined in context of ContextShim" do
43
+ context "when overriding by params" do
44
+ it do
45
+ ctx = Class.new {
46
+ public :session
47
+ def request
48
+ build_request(params: {
49
+ "ab_test" => { "link_color" => "blue" }
50
+ })
51
+ end
52
+ }.new
53
+
54
+ context_shim = Split::EncapsulatedHelper::ContextShim.new(ctx)
55
+ expect(context_shim.ab_test("link_color", "blue", "red")).to be("blue")
56
+ end
57
+ end
58
+
59
+ context "when overriding by cookies" do
60
+ it do
61
+ ctx = Class.new {
62
+ public :session
63
+ def request
64
+ build_request(cookies: {
65
+ "split_override" => '{ "link_color": "red" }'
66
+ })
67
+ end
68
+ }.new
69
+
70
+ context_shim = Split::EncapsulatedHelper::ContextShim.new(ctx)
71
+ expect(context_shim.ab_test("link_color", "blue", "red")).to be("red")
72
+ end
73
+ end
74
+ end
49
75
  end
50
76
  end
@@ -576,6 +576,17 @@ describe Split::Experiment do
576
576
  expect(p_goal1).not_to be_within(0.04).of(p_goal2)
577
577
  end
578
578
 
579
+ it "should not calculate when data is not valid for beta distribution" do
580
+ experiment = Split::ExperimentCatalog.find_or_create("scientists", "einstein", "bohr")
581
+
582
+ experiment.alternatives.each do |alternative|
583
+ alternative.participant_count = 9
584
+ alternative.set_completed_count(10)
585
+ end
586
+
587
+ expect { experiment.calc_winning_alternatives }.to_not raise_error
588
+ end
589
+
579
590
  it "should return nil and not re-calculate probabilities if they have already been calculated today" do
580
591
  experiment = Split::ExperimentCatalog.find_or_create({ "link_color3" => ["purchase", "refund"] }, "blue", "red", "green")
581
592
  expect(experiment.calc_winning_alternatives).not_to be nil
data/spec/helper_spec.rb CHANGED
@@ -558,6 +558,42 @@ describe Split::Helper do
558
558
  end
559
559
  end
560
560
 
561
+
562
+ describe "ab_record_extra_info" do
563
+ context "for an experiment that the user participates in" do
564
+ before(:each) do
565
+ @experiment_name = "link_color"
566
+ @alternatives = ["blue", "red"]
567
+ @experiment = Split::ExperimentCatalog.find_or_create(@experiment_name, *@alternatives)
568
+ @alternative_name = ab_test(@experiment_name, *@alternatives)
569
+ end
570
+
571
+ it "records extra data for a given experiment" do
572
+ alternative = Split::Alternative.new(@alternative_name, "link_color")
573
+
574
+ ab_record_extra_info(@experiment_name, "some_data", 10)
575
+
576
+ expect(alternative.extra_info).to eql({ "some_data" => 10 })
577
+ end
578
+
579
+ it "records extra data for a given experiment" do
580
+ alternative = Split::Alternative.new(@alternative_name, "link_color")
581
+
582
+ ab_record_extra_info(@experiment_name, "some_data")
583
+
584
+ expect(alternative.extra_info).to eql({ "some_data" => 1 })
585
+ end
586
+
587
+ it "records extra data for a given experiment" do
588
+ alternative = Split::Alternative.new(@alternative_name, "link_color")
589
+
590
+ ab_record_extra_info(@experiment_name, "some_data", nil)
591
+
592
+ expect(alternative.extra_info).to eql({})
593
+ end
594
+ end
595
+ end
596
+
561
597
  describe "conversions" do
562
598
  it "should return a conversion rate for an alternative" do
563
599
  alternative_name = ab_test("link_color", "blue", "red")
@@ -67,14 +67,14 @@ describe Split::Persistence::CookieAdapter do
67
67
  it "puts multiple experiments in a single cookie" do
68
68
  subject["foo"] = "FOO"
69
69
  subject["bar"] = "BAR"
70
- expect(context.response.headers["Set-Cookie"]).to match(/\Asplit=%7B%22foo%22%3A%22FOO%22%2C%22bar%22%3A%22BAR%22%7D; path=\/; expires=[a-zA-Z]{3}, \d{2} [a-zA-Z]{3} \d{4} \d{2}:\d{2}:\d{2} [A-Z]{3}\Z/)
70
+ expect(Array(context.response.headers["Set-Cookie"])).to include(/\Asplit=%7B%22foo%22%3A%22FOO%22%2C%22bar%22%3A%22BAR%22%7D; path=\/; expires=[a-zA-Z]{3}, \d{2} [a-zA-Z]{3} \d{4} \d{2}:\d{2}:\d{2} [A-Z]{3}\Z/)
71
71
  end
72
72
 
73
73
  it "ensure other added cookies are not overriden" do
74
74
  context.response.set_cookie "dummy", "wow"
75
75
  subject["foo"] = "FOO"
76
- expect(context.response.headers["Set-Cookie"]).to include("dummy=wow")
77
- expect(context.response.headers["Set-Cookie"]).to include("split=")
76
+ expect(Array(context.response.headers["Set-Cookie"])).to include(/dummy=wow/)
77
+ expect(Array(context.response.headers["Set-Cookie"])).to include(/split=/)
78
78
  end
79
79
  end
80
80
 
@@ -73,9 +73,9 @@ describe Split::Persistence::RedisAdapter do
73
73
  before { Split::Persistence::RedisAdapter.with_config(lookup_by: "lookup") }
74
74
 
75
75
  describe "#[] and #[]=" do
76
- it "should set and return the value for given key" do
77
- subject["my_key"] = "my_value"
78
- expect(subject["my_key"]).to eq("my_value")
76
+ it "should convert to string, set and return the value for given key" do
77
+ subject["my_key"] = true
78
+ expect(subject["my_key"]).to eq("true")
79
79
  end
80
80
  end
81
81
 
@@ -40,5 +40,15 @@ describe Split::RedisInterface do
40
40
  add_to_set
41
41
  expect(Split.redis.sismember(set_name, "something")).to be true
42
42
  end
43
+
44
+ context "when a Redis version is used that supports the 'sadd?' method" do
45
+ before { expect(Split.redis).to receive(:respond_to?).with(:sadd?).and_return(true) }
46
+
47
+ it "will use this method instead of 'sadd'" do
48
+ expect(Split.redis).to receive(:sadd?).with(set_name, "something")
49
+ expect(Split.redis).not_to receive(:sadd).with(set_name, "something")
50
+ add_to_set
51
+ end
52
+ end
43
53
  end
44
54
  end
data/spec/spec_helper.rb CHANGED
@@ -11,6 +11,7 @@ SimpleCov.start
11
11
  require "split"
12
12
  require "ostruct"
13
13
  require "yaml"
14
+ require "pry"
14
15
 
15
16
  Dir["./spec/support/*.rb"].each { |f| require f }
16
17
 
@@ -43,11 +44,20 @@ def params
43
44
  @params ||= {}
44
45
  end
45
46
 
46
- def request(ua = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; de-de) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27")
47
- @request ||= begin
48
- r = OpenStruct.new
49
- r.user_agent = ua
50
- r.ip = "192.168.1.1"
51
- r
52
- end
47
+ def request
48
+ @request ||= build_request
49
+ end
50
+
51
+ def build_request(
52
+ ua: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; de-de) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27",
53
+ ip: "192.168.1.1",
54
+ params: {},
55
+ cookies: {}
56
+ )
57
+ r = OpenStruct.new
58
+ r.user_agent = ua
59
+ r.ip = ip
60
+ r.params = params
61
+ r.cookies = cookies
62
+ r
53
63
  end
data/split.gemspec CHANGED
@@ -33,6 +33,7 @@ Gem::Specification.new do |s|
33
33
  s.add_dependency "redis", ">= 4.2"
34
34
  s.add_dependency "sinatra", ">= 1.2.6"
35
35
  s.add_dependency "rubystats", ">= 0.3.0"
36
+ s.add_dependency "matrix"
36
37
 
37
38
  s.add_development_dependency "bundler", ">= 1.17"
38
39
  s.add_development_dependency "simplecov", "~> 0.15"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: split
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.2
4
+ version: 4.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-02 00:00:00.000000000 Z
11
+ date: 2024-03-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: 0.3.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: matrix
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: bundler
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -275,7 +289,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
275
289
  - !ruby/object:Gem::Version
276
290
  version: 2.0.0
277
291
  requirements: []
278
- rubygems_version: 3.3.7
292
+ rubygems_version: 3.5.3
279
293
  signing_key:
280
294
  specification_version: 4
281
295
  summary: Rack based split testing framework