split 4.0.0.pre2 → 4.0.2
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 +14 -1
- data/.rubocop.yml +2 -5
- data/CHANGELOG.md +26 -2
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +2 -1
- data/README.md +4 -2
- data/Rakefile +4 -5
- data/gemfiles/5.2.gemfile +1 -3
- data/gemfiles/6.0.gemfile +1 -3
- data/gemfiles/{5.0.gemfile → 6.1.gemfile} +2 -4
- data/gemfiles/{5.1.gemfile → 7.0.gemfile} +3 -4
- data/lib/split/algorithms/block_randomization.rb +5 -6
- data/lib/split/algorithms/whiplash.rb +16 -18
- data/lib/split/algorithms.rb +22 -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/index.erb +19 -4
- data/lib/split/dashboard.rb +29 -23
- data/lib/split/encapsulated_helper.rb +4 -6
- data/lib/split/experiment.rb +84 -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 +19 -19
- 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 +2 -3
- data/lib/split/persistence/session_adapter.rb +0 -2
- data/lib/split/persistence.rb +4 -4
- data/lib/split/redis_interface.rb +1 -2
- 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 +67 -35
- data/spec/encapsulated_helper_spec.rb +12 -14
- data/spec/experiment_catalog_spec.rb +14 -13
- data/spec/experiment_spec.rb +121 -123
- data/spec/goals_collection_spec.rb +17 -15
- data/spec/helper_spec.rb +379 -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 +25 -26
- data/spec/persistence/session_adapter_spec.rb +2 -3
- data/spec/persistence_spec.rb +1 -2
- data/spec/redis_interface_spec.rb +16 -14
- data/spec/spec_helper.rb +15 -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 +20 -20
- metadata +9 -10
- data/.rubocop_todo.yml +0 -226
- data/Appraisals +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f1c97063d4d1ccf4c2cd5dfd2e83b98b3f55a934b9dfa9b5862ede3ee5b8c58c
|
4
|
+
data.tar.gz: 28e30003a6d2059baf91482f5ac9a97b0b7d2e37c61a425710cbf1adf21a4a83
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 988f115f0f96870188221552cca45f6a7207f548500dbf2a5446abaa47ac7365571644a5a231443ca7616182b68c3139f8d62fdcf18d1593fca8898092a332e2
|
7
|
+
data.tar.gz: b6a668159d8b6fe9529bae5bc650f120b9de1bdf593bc89d0d949a2e07e8cfda7c96b4c12e5e4615696cf31b845215f95ce60b4abcb9ff3d7ef056669a4ae51d
|
data/.github/workflows/ci.yml
CHANGED
@@ -28,6 +28,16 @@ jobs:
|
|
28
28
|
- gemfile: 6.0.gemfile
|
29
29
|
ruby: '3.0'
|
30
30
|
|
31
|
+
- gemfile: 6.1.gemfile
|
32
|
+
ruby: '3.0'
|
33
|
+
|
34
|
+
- gemfile: 7.0.gemfile
|
35
|
+
ruby: '3.0'
|
36
|
+
|
37
|
+
- gemfile: 7.0.gemfile
|
38
|
+
ruby: '3.1'
|
39
|
+
|
40
|
+
|
31
41
|
runs-on: ubuntu-latest
|
32
42
|
|
33
43
|
services:
|
@@ -41,7 +51,7 @@ jobs:
|
|
41
51
|
--health-retries 5
|
42
52
|
|
43
53
|
steps:
|
44
|
-
- uses: actions/checkout@
|
54
|
+
- uses: actions/checkout@v3
|
45
55
|
|
46
56
|
- uses: ruby/setup-ruby@v1
|
47
57
|
with:
|
@@ -59,3 +69,6 @@ jobs:
|
|
59
69
|
run: bundle exec rspec
|
60
70
|
env:
|
61
71
|
REDIS_URL: redis:6379
|
72
|
+
|
73
|
+
- name: Rubocop
|
74
|
+
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,4 +1,27 @@
|
|
1
|
-
|
1
|
+
# 4.0.2 (December 2nd, 2022)
|
2
|
+
|
3
|
+
Bugfixes:
|
4
|
+
- Stop crashing on non-hash json (@knarewski, #697)
|
5
|
+
- Handle when Rails is partially loaded as a Gem (@TSMMark, #687)
|
6
|
+
|
7
|
+
Features:
|
8
|
+
- Add support for redis-client, which does not automatically cast types to strings (@knarewski, #696)
|
9
|
+
- Add ability to initialize experiments (@robin-phung, #673)
|
10
|
+
|
11
|
+
Misc:
|
12
|
+
- Fix default branch name and gem metadata indentation (@ursm, #693)
|
13
|
+
- Update actions/checkout to v3 (@andrehjr, #683)
|
14
|
+
- Enforce double quotes (@andrehjr, #682)
|
15
|
+
- Fix Rubocop Style/* Offenses (@andrehjr, #681)
|
16
|
+
- Enable rubocop on Github Actions (@andrehjr, #680)
|
17
|
+
- Fix all Layout issues on the project (@andrehjr, #679)
|
18
|
+
- Fix Style/HashSyntax offenses (@andrehjr, #678)
|
19
|
+
- Remove usage of deprecated implicit block expectation from specs (@andrehjr, #677)
|
20
|
+
- Remove appraisals configuration (@andrehjr, #676)
|
21
|
+
- Add Ruby 3.1 (@andrehjr, #675)
|
22
|
+
- Encapsulate Split::Algorithms at our own module to avoid explicit calling rubystats everywhere (@andrehjr, #674)
|
23
|
+
|
24
|
+
## 4.0.1 (December 30th, 2021)
|
2
25
|
|
3
26
|
Bugfixes:
|
4
27
|
- ab_test must return metadata on error or if split is disabled/excluded user (@andrehjr, #622)
|
@@ -7,7 +30,7 @@ Bugfixes:
|
|
7
30
|
- Respect experiment defaults when loading experiments in initializer. (@mattwd7, #599)
|
8
31
|
- Removes metadata key when it updated to nil (@andrehjr, #633)
|
9
32
|
- Force experiment does not count for metrics (@andrehjr, #637)
|
10
|
-
- Fix cleanup_old_versions! misbehaviour (@
|
33
|
+
- Fix cleanup_old_versions! misbehaviour (@serggl, #661)
|
11
34
|
|
12
35
|
Features:
|
13
36
|
- Make goals accessible via on_trial_complete callbacks (@robin-phung, #625)
|
@@ -32,6 +55,7 @@ Misc:
|
|
32
55
|
- Remove 'set' parsing for alternatives. Sets were used as storage and deprecated on 0.x (@andrehjr, #639)
|
33
56
|
- Adding documentation related to what is stored on cookies. (@andrehjr, #634)
|
34
57
|
- Keep railtie defined under the Split gem namespace (@avit, #666)
|
58
|
+
- Update RSpec helper to support block syntax (@clowder, #665)
|
35
59
|
|
36
60
|
## 3.4.1 (November 12th, 2019)
|
37
61
|
|
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
|
|
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
@@ -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
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
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
|
+
|
12
|
+
require "rubystats"
|
13
|
+
|
14
|
+
module Split
|
15
|
+
module Algorithms
|
16
|
+
class << self
|
17
|
+
def beta_distribution_rng(a, b)
|
18
|
+
Rubystats::BetaDistribution.new(a, b).rng
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
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
|