split 4.0.1 → 4.0.3

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 (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