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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +8 -3
- data/.rubocop.yml +2 -5
- data/CHANGELOG.md +38 -0
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +1 -1
- data/README.md +11 -3
- data/Rakefile +4 -5
- data/gemfiles/5.2.gemfile +1 -3
- data/gemfiles/6.0.gemfile +1 -3
- data/gemfiles/6.1.gemfile +1 -3
- data/gemfiles/7.0.gemfile +1 -3
- data/lib/split/algorithms/block_randomization.rb +5 -6
- data/lib/split/algorithms/whiplash.rb +16 -18
- data/lib/split/algorithms.rb +14 -0
- data/lib/split/alternative.rb +21 -22
- data/lib/split/cache.rb +0 -1
- data/lib/split/combined_experiments_helper.rb +4 -4
- data/lib/split/configuration.rb +83 -84
- data/lib/split/dashboard/helpers.rb +6 -7
- data/lib/split/dashboard/pagination_helpers.rb +53 -54
- data/lib/split/dashboard/public/style.css +5 -2
- data/lib/split/dashboard/views/_experiment.erb +2 -1
- data/lib/split/dashboard/views/index.erb +19 -4
- data/lib/split/dashboard.rb +29 -23
- data/lib/split/encapsulated_helper.rb +4 -6
- data/lib/split/experiment.rb +93 -88
- data/lib/split/experiment_catalog.rb +6 -5
- data/lib/split/extensions/string.rb +1 -1
- data/lib/split/goals_collection.rb +8 -10
- data/lib/split/helper.rb +20 -20
- data/lib/split/metric.rb +4 -5
- data/lib/split/persistence/cookie_adapter.rb +44 -47
- data/lib/split/persistence/dual_adapter.rb +7 -8
- data/lib/split/persistence/redis_adapter.rb +3 -4
- data/lib/split/persistence/session_adapter.rb +0 -2
- data/lib/split/persistence.rb +4 -4
- data/lib/split/redis_interface.rb +7 -1
- data/lib/split/trial.rb +23 -24
- data/lib/split/user.rb +12 -13
- data/lib/split/version.rb +1 -1
- data/lib/split/zscore.rb +1 -3
- data/lib/split.rb +26 -25
- data/spec/algorithms/block_randomization_spec.rb +6 -5
- data/spec/algorithms/weighted_sample_spec.rb +6 -5
- data/spec/algorithms/whiplash_spec.rb +4 -5
- data/spec/alternative_spec.rb +35 -36
- data/spec/cache_spec.rb +15 -19
- data/spec/combined_experiments_helper_spec.rb +18 -17
- data/spec/configuration_spec.rb +32 -38
- data/spec/dashboard/pagination_helpers_spec.rb +69 -67
- data/spec/dashboard/paginator_spec.rb +10 -9
- data/spec/dashboard_helpers_spec.rb +19 -18
- data/spec/dashboard_spec.rb +79 -35
- data/spec/encapsulated_helper_spec.rb +12 -14
- data/spec/experiment_catalog_spec.rb +14 -13
- data/spec/experiment_spec.rb +132 -123
- data/spec/goals_collection_spec.rb +17 -15
- data/spec/helper_spec.rb +415 -382
- data/spec/metric_spec.rb +14 -14
- data/spec/persistence/cookie_adapter_spec.rb +23 -8
- data/spec/persistence/dual_adapter_spec.rb +71 -71
- data/spec/persistence/redis_adapter_spec.rb +28 -29
- data/spec/persistence/session_adapter_spec.rb +2 -3
- data/spec/persistence_spec.rb +1 -2
- data/spec/redis_interface_spec.rb +26 -14
- data/spec/spec_helper.rb +16 -13
- data/spec/split_spec.rb +11 -11
- data/spec/support/cookies_mock.rb +1 -2
- data/spec/trial_spec.rb +61 -60
- data/spec/user_spec.rb +36 -36
- data/split.gemspec +21 -20
- metadata +25 -14
- data/.rubocop_todo.yml +0 -226
- data/Appraisals +0 -19
- data/gemfiles/5.0.gemfile +0 -9
- data/gemfiles/5.1.gemfile +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 746dd3b526b5464f12e01e2f00f8ff62b71ac6483527632f6f0bca7bc5242e8c
|
4
|
+
data.tar.gz: bb40ca355a1aa9eec9cfb4067e6e2b3c381072830cffadffe414f8d7a4af043f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 70466ddfe57955a43dda0506c4a23806f6bdab154b344e495879027ae2baebec93d59f34018e78a3e83ef945c776c7a2ddcf2b40cd71998fb0bc9207ed60df61
|
7
|
+
data.tar.gz: db1d46d7e7e1826aacf80ec1c8634f0502bd07d4b58d488e57f4917a8843954d568b4cd20c9e2db09ac2ff80811b14ec68a3158b870d091eeabf4e426af8172d
|
data/.github/workflows/ci.yml
CHANGED
@@ -34,9 +34,11 @@ jobs:
|
|
34
34
|
- gemfile: 7.0.gemfile
|
35
35
|
ruby: '3.0'
|
36
36
|
|
37
|
-
|
38
|
-
|
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@
|
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:
|
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/
|
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
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
|
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
|
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(:
|
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
|
5
|
-
require
|
6
|
-
require 'appraisal'
|
4
|
+
require "bundler/gem_tasks"
|
5
|
+
require "rspec/core/rake_task"
|
7
6
|
|
8
|
-
RSpec::Core::RakeTask.new(
|
7
|
+
RSpec::Core::RakeTask.new("spec")
|
9
8
|
|
10
|
-
task :
|
9
|
+
task default: :spec
|
data/gemfiles/5.2.gemfile
CHANGED
data/gemfiles/6.0.gemfile
CHANGED
data/gemfiles/6.1.gemfile
CHANGED
data/gemfiles/7.0.gemfile
CHANGED
@@ -12,12 +12,11 @@ module Split
|
|
12
12
|
end
|
13
13
|
|
14
14
|
private
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
31
|
+
def fairness_constant
|
32
|
+
7
|
33
|
+
end
|
36
34
|
end
|
37
35
|
end
|
38
36
|
end
|
data/lib/split/alternative.rb
CHANGED
@@ -38,11 +38,11 @@ module Split
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def participant_count
|
41
|
-
Split.redis.hget(key,
|
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,
|
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
|
-
|
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
|
-
|
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,
|
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
|
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
|
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,
|
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,
|
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,
|
157
|
-
Split.redis.hsetnx key,
|
158
|
-
Split.redis.hsetnx key,
|
159
|
-
Split.redis.hsetnx key,
|
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,
|
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,
|
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
|
-
|
185
|
-
|
186
|
-
|
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
@@ -29,10 +29,10 @@ module Split
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def find_combined_experiment(metric_descriptor)
|
32
|
-
raise(Split::InvalidExperimentsFormatError,
|
33
|
-
raise(Split::InvalidExperimentsFormatError,
|
34
|
-
raise(Split::InvalidExperimentsFormatError,
|
35
|
-
Split
|
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
|