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