split 4.0.1 → 4.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +8 -3
  3. data/.rubocop.yml +2 -5
  4. data/CHANGELOG.md +38 -0
  5. data/CONTRIBUTING.md +1 -1
  6. data/Gemfile +1 -1
  7. data/README.md +11 -3
  8. data/Rakefile +4 -5
  9. data/gemfiles/5.2.gemfile +1 -3
  10. data/gemfiles/6.0.gemfile +1 -3
  11. data/gemfiles/6.1.gemfile +1 -3
  12. data/gemfiles/7.0.gemfile +1 -3
  13. data/lib/split/algorithms/block_randomization.rb +5 -6
  14. data/lib/split/algorithms/whiplash.rb +16 -18
  15. data/lib/split/algorithms.rb +14 -0
  16. data/lib/split/alternative.rb +21 -22
  17. data/lib/split/cache.rb +0 -1
  18. data/lib/split/combined_experiments_helper.rb +4 -4
  19. data/lib/split/configuration.rb +83 -84
  20. data/lib/split/dashboard/helpers.rb +6 -7
  21. data/lib/split/dashboard/pagination_helpers.rb +53 -54
  22. data/lib/split/dashboard/public/style.css +5 -2
  23. data/lib/split/dashboard/views/_experiment.erb +2 -1
  24. data/lib/split/dashboard/views/index.erb +19 -4
  25. data/lib/split/dashboard.rb +29 -23
  26. data/lib/split/encapsulated_helper.rb +4 -6
  27. data/lib/split/experiment.rb +93 -88
  28. data/lib/split/experiment_catalog.rb +6 -5
  29. data/lib/split/extensions/string.rb +1 -1
  30. data/lib/split/goals_collection.rb +8 -10
  31. data/lib/split/helper.rb +20 -20
  32. data/lib/split/metric.rb +4 -5
  33. data/lib/split/persistence/cookie_adapter.rb +44 -47
  34. data/lib/split/persistence/dual_adapter.rb +7 -8
  35. data/lib/split/persistence/redis_adapter.rb +3 -4
  36. data/lib/split/persistence/session_adapter.rb +0 -2
  37. data/lib/split/persistence.rb +4 -4
  38. data/lib/split/redis_interface.rb +7 -1
  39. data/lib/split/trial.rb +23 -24
  40. data/lib/split/user.rb +12 -13
  41. data/lib/split/version.rb +1 -1
  42. data/lib/split/zscore.rb +1 -3
  43. data/lib/split.rb +26 -25
  44. data/spec/algorithms/block_randomization_spec.rb +6 -5
  45. data/spec/algorithms/weighted_sample_spec.rb +6 -5
  46. data/spec/algorithms/whiplash_spec.rb +4 -5
  47. data/spec/alternative_spec.rb +35 -36
  48. data/spec/cache_spec.rb +15 -19
  49. data/spec/combined_experiments_helper_spec.rb +18 -17
  50. data/spec/configuration_spec.rb +32 -38
  51. data/spec/dashboard/pagination_helpers_spec.rb +69 -67
  52. data/spec/dashboard/paginator_spec.rb +10 -9
  53. data/spec/dashboard_helpers_spec.rb +19 -18
  54. data/spec/dashboard_spec.rb +79 -35
  55. data/spec/encapsulated_helper_spec.rb +12 -14
  56. data/spec/experiment_catalog_spec.rb +14 -13
  57. data/spec/experiment_spec.rb +132 -123
  58. data/spec/goals_collection_spec.rb +17 -15
  59. data/spec/helper_spec.rb +415 -382
  60. data/spec/metric_spec.rb +14 -14
  61. data/spec/persistence/cookie_adapter_spec.rb +23 -8
  62. data/spec/persistence/dual_adapter_spec.rb +71 -71
  63. data/spec/persistence/redis_adapter_spec.rb +28 -29
  64. data/spec/persistence/session_adapter_spec.rb +2 -3
  65. data/spec/persistence_spec.rb +1 -2
  66. data/spec/redis_interface_spec.rb +26 -14
  67. data/spec/spec_helper.rb +16 -13
  68. data/spec/split_spec.rb +11 -11
  69. data/spec/support/cookies_mock.rb +1 -2
  70. data/spec/trial_spec.rb +61 -60
  71. data/spec/user_spec.rb +36 -36
  72. data/split.gemspec +21 -20
  73. metadata +25 -14
  74. data/.rubocop_todo.yml +0 -226
  75. data/Appraisals +0 -19
  76. data/gemfiles/5.0.gemfile +0 -9
  77. data/gemfiles/5.1.gemfile +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6b66f4b21c4201228d9622d5e00ff2a2e957714edfce11316316a2ca3e405ce
4
- data.tar.gz: 6a441cf39a14ef9e36b805e7728f34029db7f7d1e1e90f3389168942b7b77f60
3
+ metadata.gz: 746dd3b526b5464f12e01e2f00f8ff62b71ac6483527632f6f0bca7bc5242e8c
4
+ data.tar.gz: bb40ca355a1aa9eec9cfb4067e6e2b3c381072830cffadffe414f8d7a4af043f
5
5
  SHA512:
6
- metadata.gz: bc46df4b6f301b8bdaf99ba4648f7363ad2c1234b3be7586e078b9772fec9a2bae3c9e15335c0c207e88c45113a1348a0dcd608a9dcfee654846753ba59c6696
7
- data.tar.gz: 53dffc60d0f0617f1a1e943170f7702eff6ec5d00381b9d3b19380ef50aaef4c76956974a94a645bfb288eddd75486f24c2c2d3c56e7465f85f68791f1aeda31
6
+ metadata.gz: 70466ddfe57955a43dda0506c4a23806f6bdab154b344e495879027ae2baebec93d59f34018e78a3e83ef945c776c7a2ddcf2b40cd71998fb0bc9207ed60df61
7
+ data.tar.gz: db1d46d7e7e1826aacf80ec1c8634f0502bd07d4b58d488e57f4917a8843954d568b4cd20c9e2db09ac2ff80811b14ec68a3158b870d091eeabf4e426af8172d
@@ -34,9 +34,11 @@ jobs:
34
34
  - gemfile: 7.0.gemfile
35
35
  ruby: '3.0'
36
36
 
37
- # - gemfile: 7.0.gemfile
38
- # ruby: '3.1'
37
+ - gemfile: 7.0.gemfile
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@v2
56
+ - uses: actions/checkout@v4
55
57
 
56
58
  - uses: ruby/setup-ruby@v1
57
59
  with:
@@ -69,3 +71,6 @@ jobs:
69
71
  run: bundle exec rspec
70
72
  env:
71
73
  REDIS_URL: redis:6379
74
+
75
+ - name: Rubocop
76
+ run: bundle exec rubocop
data/.rubocop.yml CHANGED
@@ -1,12 +1,9 @@
1
- inherit_from: .rubocop_todo.yml
2
-
3
1
  AllCops:
4
2
  TargetRubyVersion: 2.5
5
3
  DisabledByDefault: true
4
+ SuggestExtensions: false
6
5
  Exclude:
7
- - 'Appraisals'
8
6
  - 'gemfiles/**/*'
9
- - 'spec/**/*.rb'
10
7
 
11
8
  Style/AndOr:
12
9
  Enabled: true
@@ -114,7 +111,7 @@ Layout/SpaceInsideParens:
114
111
  Enabled: true
115
112
 
116
113
  Style/StringLiterals:
117
- Enabled: false
114
+ Enabled: true
118
115
  EnforcedStyle: double_quotes
119
116
 
120
117
  Layout/IndentationStyle:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,41 @@
1
+ # 4.0.3 (November 15rd, 2023)
2
+
3
+ Bugfixes:
4
+ - Do not throw error if alternativas have data that can lead to negative numbers for probability calculation (@andrehjr, #703)
5
+ - Do not persist invalid extra_info on ab_record_extra_info. (@trostli @andrehjr, #717)
6
+ - CROSSSLOT keys issue fix when using redis cluster (@naveen-chidhambaram, #710)
7
+ - Convert value to string before saving it in RedisAdapter (@Jealrock, #714)
8
+ - Fix deprecation warning with Redis 4.8.0 (@martingregoire, #701)
9
+
10
+ Misc:
11
+ - Add matrix as a default dependency (@andrehjr, #705)
12
+ - Add Ruby 3.2 to Github Actions (@andrehjr, #702)
13
+ - Update documentation regarding finding users outside a web session (@andrehjr, #716)
14
+ - Update actions/checkout to v4 (@andrehjr, #718)
15
+
16
+ # 4.0.2 (December 2nd, 2022)
17
+
18
+ Bugfixes:
19
+ - Stop crashing on non-hash json (@knarewski, #697)
20
+ - Handle when Rails is partially loaded as a Gem (@TSMMark, #687)
21
+
22
+ Features:
23
+ - Add support for redis-client, which does not automatically cast types to strings (@knarewski, #696)
24
+ - Add ability to initialize experiments (@robin-phung, #673)
25
+
26
+ Misc:
27
+ - Fix default branch name and gem metadata indentation (@ursm, #693)
28
+ - Update actions/checkout to v3 (@andrehjr, #683)
29
+ - Enforce double quotes (@andrehjr, #682)
30
+ - Fix Rubocop Style/* Offenses (@andrehjr, #681)
31
+ - Enable rubocop on Github Actions (@andrehjr, #680)
32
+ - Fix all Layout issues on the project (@andrehjr, #679)
33
+ - Fix Style/HashSyntax offenses (@andrehjr, #678)
34
+ - Remove usage of deprecated implicit block expectation from specs (@andrehjr, #677)
35
+ - Remove appraisals configuration (@andrehjr, #676)
36
+ - Add Ruby 3.1 (@andrehjr, #675)
37
+ - Encapsulate Split::Algorithms at our own module to avoid explicit calling rubystats everywhere (@andrehjr, #674)
38
+
1
39
  ## 4.0.1 (December 30th, 2021)
2
40
 
3
41
  Bugfixes:
data/CONTRIBUTING.md CHANGED
@@ -25,7 +25,7 @@ Want to contribute to Split? That's great! Here are a couple of guidelines that
25
25
 
26
26
  ## Setup instructions
27
27
 
28
- You can find in-depth instructions to install in our [README](https://github.com/splitrb/split/blob/master/README.md).
28
+ You can find in-depth instructions to install in our [README](https://github.com/splitrb/split/blob/main/README.md).
29
29
 
30
30
  *Note*: Split requires Ruby 1.9.2 or higher.
31
31
 
data/Gemfile CHANGED
@@ -4,5 +4,5 @@ source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
6
 
7
- gem "appraisal"
7
+ gem "rubocop", require: false
8
8
  gem "codeclimate-test-reporter"
data/README.md CHANGED
@@ -19,11 +19,13 @@ Split is designed to be hacker friendly, allowing for maximum customisation and
19
19
 
20
20
  ### Requirements
21
21
 
22
- Split currently requires Ruby 1.9.3 or higher. If your project requires compatibility with Ruby 1.8.x and Rails 2.3, please use v0.8.0.
22
+ Split v4.0+ is currently tested with Ruby >= 2.5 and Rails >= 5.2.
23
+
24
+ If your project requires compatibility with Ruby 2.4.x or older Rails versions. You can try v3.0 or v0.8.0(for Ruby 1.9.3)
23
25
 
24
26
  Split uses Redis as a datastore.
25
27
 
26
- Split only supports Redis 2.0 or greater.
28
+ Split only supports Redis 4.0 or greater.
27
29
 
28
30
  If you're on OS X, Homebrew is the simplest way to install Redis:
29
31
 
@@ -805,10 +807,16 @@ conduct experiments that are not tied to a web session.
805
807
  ```ruby
806
808
  # create a new experiment
807
809
  experiment = Split::ExperimentCatalog.find_or_create('color', 'red', 'blue')
810
+
811
+ # find the user
812
+ user = Split::User.find(user_id, :redis)
813
+
808
814
  # create a new trial
809
- trial = Split::Trial.new(:experiment => experiment)
815
+ trial = Split::Trial.new(user: user, experiment: experiment)
816
+
810
817
  # run trial
811
818
  trial.choose!
819
+
812
820
  # get the result, returns either red or blue
813
821
  trial.alternative.name
814
822
 
data/Rakefile CHANGED
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env rake
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'bundler/gem_tasks'
5
- require 'rspec/core/rake_task'
6
- require 'appraisal'
4
+ require "bundler/gem_tasks"
5
+ require "rspec/core/rake_task"
7
6
 
8
- RSpec::Core::RakeTask.new('spec')
7
+ RSpec::Core::RakeTask.new("spec")
9
8
 
10
- task :default => :spec
9
+ task default: :spec
data/gemfiles/5.2.gemfile CHANGED
@@ -1,8 +1,6 @@
1
- # This file was generated by Appraisal
2
-
3
1
  source "https://rubygems.org"
4
2
 
5
- gem "appraisal"
3
+ gem "rubocop", require: false
6
4
  gem "codeclimate-test-reporter"
7
5
  gem "rails", "~> 5.2"
8
6
 
data/gemfiles/6.0.gemfile CHANGED
@@ -1,8 +1,6 @@
1
- # This file was generated by Appraisal
2
-
3
1
  source "https://rubygems.org"
4
2
 
5
- gem "appraisal"
3
+ gem "rubocop", require: false
6
4
  gem "codeclimate-test-reporter"
7
5
  gem "rails", "~> 6.0"
8
6
 
data/gemfiles/6.1.gemfile CHANGED
@@ -1,8 +1,6 @@
1
- # This file was generated by Appraisal
2
-
3
1
  source "https://rubygems.org"
4
2
 
5
- gem "appraisal"
3
+ gem "rubocop", require: false
6
4
  gem "codeclimate-test-reporter"
7
5
  gem "rails", "~> 6.1"
8
6
 
data/gemfiles/7.0.gemfile CHANGED
@@ -1,8 +1,6 @@
1
- # This file was generated by Appraisal
2
-
3
1
  source "https://rubygems.org"
4
2
 
5
- gem "appraisal"
3
+ gem "rubocop", require: false
6
4
  gem "codeclimate-test-reporter"
7
5
  gem "rails", "~> 7.0"
8
6
 
@@ -12,12 +12,11 @@ module Split
12
12
  end
13
13
 
14
14
  private
15
-
16
- def minimum_participant_alternatives(alternatives)
17
- alternatives_by_count = alternatives.group_by(&:participant_count)
18
- min_group = alternatives_by_count.min_by { |k, v| k }
19
- min_group.last
20
- end
15
+ def minimum_participant_alternatives(alternatives)
16
+ alternatives_by_count = alternatives.group_by(&:participant_count)
17
+ min_group = alternatives_by_count.min_by { |k, v| k }
18
+ min_group.last
19
+ end
21
20
  end
22
21
  end
23
22
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  # A multi-armed bandit implementation inspired by
4
4
  # @aaronsw and victorykit/whiplash
5
- require 'rubystats'
6
5
 
7
6
  module Split
8
7
  module Algorithms
@@ -13,26 +12,25 @@ module Split
13
12
  end
14
13
 
15
14
  private
15
+ def arm_guess(participants, completions)
16
+ a = [participants, 0].max
17
+ b = [participants-completions, 0].max
18
+ Split::Algorithms.beta_distribution_rng(a + fairness_constant, b + fairness_constant)
19
+ end
16
20
 
17
- def arm_guess(participants, completions)
18
- a = [participants, 0].max
19
- b = [participants-completions, 0].max
20
- Rubystats::BetaDistribution.new(a+fairness_constant, b+fairness_constant).rng
21
- end
22
-
23
- def best_guess(alternatives)
24
- guesses = {}
25
- alternatives.each do |alternative|
26
- guesses[alternative.name] = arm_guess(alternative.participant_count, alternative.all_completed_count)
21
+ def best_guess(alternatives)
22
+ guesses = {}
23
+ alternatives.each do |alternative|
24
+ guesses[alternative.name] = arm_guess(alternative.participant_count, alternative.all_completed_count)
25
+ end
26
+ gmax = guesses.values.max
27
+ best = guesses.keys.select { |name| guesses[name] == gmax }
28
+ best.sample
27
29
  end
28
- gmax = guesses.values.max
29
- best = guesses.keys.select { |name| guesses[name] == gmax }
30
- best.sample
31
- end
32
30
 
33
- def fairness_constant
34
- 7
35
- end
31
+ def fairness_constant
32
+ 7
33
+ end
36
34
  end
37
35
  end
38
36
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "matrix"
4
+ require "rubystats"
5
+
6
+ module Split
7
+ module Algorithms
8
+ class << self
9
+ def beta_distribution_rng(a, b)
10
+ Rubystats::BetaDistribution.new(a, b).rng
11
+ end
12
+ end
13
+ end
14
+ end
@@ -38,11 +38,11 @@ module Split
38
38
  end
39
39
 
40
40
  def participant_count
41
- Split.redis.hget(key, 'participant_count').to_i
41
+ Split.redis.hget(key, "participant_count").to_i
42
42
  end
43
43
 
44
44
  def participant_count=(count)
45
- Split.redis.hset(key, 'participant_count', count.to_i)
45
+ Split.redis.hset(key, "participant_count", count.to_i)
46
46
  end
47
47
 
48
48
  def completed_count(goal = nil)
@@ -67,13 +67,13 @@ module Split
67
67
  def set_field(goal)
68
68
  field = "completed_count"
69
69
  field += ":" + goal unless goal.nil?
70
- return field
70
+ field
71
71
  end
72
72
 
73
73
  def set_prob_field(goal)
74
74
  field = "p_winner"
75
75
  field += ":" + goal unless goal.nil?
76
- return field
76
+ field
77
77
  end
78
78
 
79
79
  def set_completed_count(count, goal = nil)
@@ -82,7 +82,7 @@ module Split
82
82
  end
83
83
 
84
84
  def increment_participation
85
- Split.redis.hincrby key, 'participant_count', 1
85
+ Split.redis.hincrby key, "participant_count", 1
86
86
  end
87
87
 
88
88
  def increment_completion(goal = nil)
@@ -112,7 +112,7 @@ module Split
112
112
  control = experiment.control
113
113
  alternative = self
114
114
 
115
- return 'N/A' if control.name == alternative.name
115
+ return "N/A" if control.name == alternative.name
116
116
 
117
117
  p_a = alternative.conversion_rate(goal)
118
118
  p_c = control.conversion_rate(goal)
@@ -121,13 +121,13 @@ module Split
121
121
  n_c = control.participant_count
122
122
 
123
123
  # can't calculate zscore for P(x) > 1
124
- return 'N/A' if p_a > 1 || p_c > 1
124
+ return "N/A" if p_a > 1 || p_c > 1
125
125
 
126
126
  Split::Zscore.calculate(p_a, n_a, p_c, n_c)
127
127
  end
128
128
 
129
129
  def extra_info
130
- data = Split.redis.hget(key, 'recorded_info')
130
+ data = Split.redis.hget(key, "recorded_info")
131
131
  if data && data.length > 1
132
132
  begin
133
133
  JSON.parse(data)
@@ -149,24 +149,24 @@ module Split
149
149
  @recorded_info[k] = value
150
150
  end
151
151
 
152
- Split.redis.hset key, 'recorded_info', (@recorded_info || {}).to_json
152
+ Split.redis.hset key, "recorded_info", (@recorded_info || {}).to_json
153
153
  end
154
154
 
155
155
  def save
156
- Split.redis.hsetnx key, 'participant_count', 0
157
- Split.redis.hsetnx key, 'completed_count', 0
158
- Split.redis.hsetnx key, 'p_winner', p_winner
159
- Split.redis.hsetnx key, 'recorded_info', (@recorded_info || {}).to_json
156
+ Split.redis.hsetnx key, "participant_count", 0
157
+ Split.redis.hsetnx key, "completed_count", 0
158
+ Split.redis.hsetnx key, "p_winner", p_winner
159
+ Split.redis.hsetnx key, "recorded_info", (@recorded_info || {}).to_json
160
160
  end
161
161
 
162
162
  def validate!
163
163
  unless String === @name || hash_with_correct_values?(@name)
164
- raise ArgumentError, 'Alternative must be a string'
164
+ raise ArgumentError, "Alternative must be a string"
165
165
  end
166
166
  end
167
167
 
168
168
  def reset
169
- Split.redis.hmset key, 'participant_count', 0, 'completed_count', 0, 'recorded_info', nil
169
+ Split.redis.hmset key, "participant_count", 0, "completed_count", 0, "recorded_info", ""
170
170
  unless goals.empty?
171
171
  goals.each do |g|
172
172
  field = "completed_count:#{g}"
@@ -180,13 +180,12 @@ module Split
180
180
  end
181
181
 
182
182
  private
183
+ def hash_with_correct_values?(name)
184
+ Hash === name && String === name.keys.first && Float(name.values.first) rescue false
185
+ end
183
186
 
184
- def hash_with_correct_values?(name)
185
- Hash === name && String === name.keys.first && Float(name.values.first) rescue false
186
- end
187
-
188
- def key
189
- "#{experiment_name}:#{name}"
190
- end
187
+ def key
188
+ "#{experiment_name}:#{name}"
189
+ end
191
190
  end
192
191
  end
data/lib/split/cache.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Split
4
4
  class Cache
5
-
6
5
  def self.clear
7
6
  @cache = nil
8
7
  end
@@ -29,10 +29,10 @@ module Split
29
29
  end
30
30
 
31
31
  def find_combined_experiment(metric_descriptor)
32
- raise(Split::InvalidExperimentsFormatError, 'Invalid descriptor class (String or Symbol required)') unless metric_descriptor.class == String || metric_descriptor.class == Symbol
33
- raise(Split::InvalidExperimentsFormatError, 'Enable configuration') unless Split.configuration.enabled
34
- raise(Split::InvalidExperimentsFormatError, 'Enable `allow_multiple_experiments`') unless Split.configuration.allow_multiple_experiments
35
- Split::configuration.experiments[metric_descriptor.to_sym]
32
+ raise(Split::InvalidExperimentsFormatError, "Invalid descriptor class (String or Symbol required)") unless metric_descriptor.class == String || metric_descriptor.class == Symbol
33
+ raise(Split::InvalidExperimentsFormatError, "Enable configuration") unless Split.configuration.enabled
34
+ raise(Split::InvalidExperimentsFormatError, "Enable `allow_multiple_experiments`") unless Split.configuration.allow_multiple_experiments
35
+ Split.configuration.experiments[metric_descriptor.to_sym]
36
36
  end
37
37
  end
38
38
  end