split 4.0.2 → 4.0.4

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