split 4.0.0.pre2 → 4.0.2
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 +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
|