vinted-ab 0.0.3 → 0.1.0
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/README.md +3 -3
- data/lib/ab/assigned_test.rb +54 -0
- data/lib/ab/{null_experiment.rb → null_test.rb} +1 -1
- data/lib/ab/{experiment.rb → test.rb} +1 -1
- data/lib/ab/tests.rb +41 -0
- data/lib/ab/version.rb +1 -1
- data/lib/ab.rb +4 -4
- data/spec/{assigned_experiment_spec.rb → assigned_test_spec.rb} +7 -7
- data/spec/{null_experiment_spec.rb → null_test_spec.rb} +2 -2
- data/spec/test_spec.rb +39 -0
- data/spec/{experiments_spec.rb → tests_spec.rb} +3 -3
- metadata +10 -10
- data/lib/ab/assigned_experiment.rb +0 -54
- data/lib/ab/experiments.rb +0 -41
- data/spec/experiment_spec.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 45ab4518594015ccbca8ebd4d441f835e103e22c
|
4
|
+
data.tar.gz: 80fa73e5144373f439208f7773b82c76c4949de4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 467ebec6807d536330430ebd111cff5fcb7d43935b43fa01866d370d61a5db9afa934b662836d5c025cd44395737270a1d029209ed4b5a8a01c58b953530f58c
|
7
|
+
data.tar.gz: a6a561c9a611031fd09d75da86ad36d6b3714fed4980e46cdae5a863aa1c7c45a25c48135134a5e87a1f183dd78e847c07cb0f45def7bf2495ad136c6ef918fa
|
data/README.md
CHANGED
@@ -82,11 +82,11 @@ For this lib to work, it requires a configuration json, which looks like this:
|
|
82
82
|
|
83
83
|
```ruby
|
84
84
|
configuration = retrieve_from_svc_abs
|
85
|
-
ab = Ab::
|
85
|
+
ab = Ab::Tests.new(configuration, user_id)
|
86
86
|
|
87
87
|
# defining callbacks
|
88
|
-
Ab::
|
89
|
-
Ab::
|
88
|
+
Ab::Tests.before_picking_variant { |test| puts 'magic' }
|
89
|
+
Ab::Tests.after_picking_variant { |test, variant| puts "#{variant_name}" }
|
90
90
|
|
91
91
|
# ab.experiment never returns nil
|
92
92
|
# but if you don't belong to any of the buckets, variant will be nil
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Ab
|
2
|
+
class AssignedTest
|
3
|
+
include Hooks
|
4
|
+
define_hooks :before_picking_variant, :after_picking_variant
|
5
|
+
|
6
|
+
def initialize(test, id)
|
7
|
+
@test, @id = test, id
|
8
|
+
@test.variants.map(&:name).each do |name|
|
9
|
+
define_singleton_method("#{name}?") { name == variant }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def variant
|
14
|
+
@variant ||= begin
|
15
|
+
return unless part_of_test?
|
16
|
+
return unless running?
|
17
|
+
|
18
|
+
run_hook :before_picking_variant, @test.name
|
19
|
+
picked_variant = @test.variants.find { |v| v.accumulated_chance_weight > weight_id }
|
20
|
+
|
21
|
+
result = picked_variant.name if picked_variant
|
22
|
+
run_hook :after_picking_variant, @test.name, result
|
23
|
+
result
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def part_of_test?
|
30
|
+
@test.buckets == 'all' || @test.buckets.include?(bucket_id)
|
31
|
+
end
|
32
|
+
|
33
|
+
def bucket_id
|
34
|
+
@bucket_id ||= digest(@test.salt + @id.to_s) % @test.bucket_count
|
35
|
+
end
|
36
|
+
|
37
|
+
def running?
|
38
|
+
now = DateTime.now
|
39
|
+
now.between?(@test.start_at, @test.end_at)
|
40
|
+
end
|
41
|
+
|
42
|
+
def weight_id
|
43
|
+
@variant_digest ||= digest("#{@test.seed}#{@id}") % positive_weight_sum
|
44
|
+
end
|
45
|
+
|
46
|
+
def positive_weight_sum
|
47
|
+
@test.weight_sum > 0 ? @test.weight_sum : 1
|
48
|
+
end
|
49
|
+
|
50
|
+
def digest(string)
|
51
|
+
Digest::SHA256.hexdigest(string).to_i(16)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/ab/tests.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
module Ab
|
2
|
+
class Tests
|
3
|
+
include Hooks
|
4
|
+
define_hooks :before_picking_variant, :after_picking_variant
|
5
|
+
|
6
|
+
Ab::AssignedTest.before_picking_variant do |test|
|
7
|
+
Ab::Tests.run_hook :before_picking_variant, test
|
8
|
+
end
|
9
|
+
Ab::AssignedTest.after_picking_variant do |test, variant|
|
10
|
+
Ab::Tests.run_hook :after_picking_variant, test, variant
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(config, id)
|
14
|
+
@assigned_tests ||= {}
|
15
|
+
|
16
|
+
(config['ab_tests'] || []).each do |test|
|
17
|
+
name = test['name']
|
18
|
+
@assigned_tests[name] = nil
|
19
|
+
define_singleton_method(name) do
|
20
|
+
test = Test.new(test, config['salt'], config['bucket_count'])
|
21
|
+
@assigned_tests[name] ||= AssignedTest.new(test, id)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def all
|
27
|
+
result = @assigned_tests.keys.map do |name|
|
28
|
+
[name, send(name).variant]
|
29
|
+
end
|
30
|
+
Hash[result]
|
31
|
+
end
|
32
|
+
|
33
|
+
def method_missing(meth, *args, &block)
|
34
|
+
@null_test ||= NullTest.new
|
35
|
+
end
|
36
|
+
|
37
|
+
def respond_to?(meth)
|
38
|
+
true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/ab/version.rb
CHANGED
data/lib/ab.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'hooks'
|
2
2
|
require 'ab/version'
|
3
|
-
require 'ab/
|
3
|
+
require 'ab/null_test'
|
4
4
|
require 'ab/variant'
|
5
|
-
require 'ab/
|
6
|
-
require 'ab/
|
7
|
-
require 'ab/
|
5
|
+
require 'ab/test'
|
6
|
+
require 'ab/assigned_test'
|
7
|
+
require 'ab/tests'
|
@@ -1,9 +1,9 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
module Ab
|
4
|
-
describe
|
5
|
-
let(:
|
6
|
-
let(:
|
4
|
+
describe AssignedTest do
|
5
|
+
let(:assigned_test) { AssignedTest.new(test, id) }
|
6
|
+
let(:test) { Test.new(hash, 'e131bfcfcab06c65d633d0266c7e62a4918', 1000) }
|
7
7
|
let(:hash) {
|
8
8
|
{
|
9
9
|
'name' => name,
|
@@ -20,10 +20,10 @@ module Ab
|
|
20
20
|
let(:end_at) { DateTime.now.next_year.to_s }
|
21
21
|
let(:seed) { 'cccc8888' }
|
22
22
|
let(:buckets) { 'all' }
|
23
|
-
let(:thousand_variants) { 1.upto(1000).map { |i|
|
23
|
+
let(:thousand_variants) { 1.upto(1000).map { |i| AssignedTest.new(test, i).variant } }
|
24
24
|
|
25
25
|
describe '#variant' do
|
26
|
-
subject {
|
26
|
+
subject { assigned_test.variant }
|
27
27
|
|
28
28
|
context 'single variant' do
|
29
29
|
let(:variants) { [{ 'name' => 'enabled', 'chance_weight' => chance_weight }] }
|
@@ -54,14 +54,14 @@ module Ab
|
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
57
|
-
context '
|
57
|
+
context 'test that has not started yet' do
|
58
58
|
let(:start_at) { DateTime.now.next_year.to_s }
|
59
59
|
let(:buckets) { [1, 2, 3] }
|
60
60
|
let(:variants) { [{ 'name' => 'enabled', 'chance_weight' => 1 }] }
|
61
61
|
it { should be_nil }
|
62
62
|
end
|
63
63
|
|
64
|
-
context '
|
64
|
+
context 'test that has already ended' do
|
65
65
|
let(:end_at) { DateTime.now.prev_year.to_s }
|
66
66
|
let(:buckets) { [1, 2, 3] }
|
67
67
|
let(:variants) { [{ 'name' => 'enabled', 'chance_weight' => 1 }] }
|
data/spec/test_spec.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Ab
|
4
|
+
describe Test do
|
5
|
+
let(:test) { Test.new(test_hash, '4321', 100) }
|
6
|
+
|
7
|
+
context '#buckets' do
|
8
|
+
subject { test.buckets }
|
9
|
+
let(:test_hash) { { 'buckets' => [1, 2, 3] } }
|
10
|
+
it { should == [1, 2, 3] }
|
11
|
+
end
|
12
|
+
|
13
|
+
context '#name' do
|
14
|
+
subject { test.name }
|
15
|
+
let(:test_hash) { { 'name' => 'test' } }
|
16
|
+
it { should == 'test' }
|
17
|
+
end
|
18
|
+
|
19
|
+
context '#start_at' do
|
20
|
+
subject { test.start_at }
|
21
|
+
let(:test_hash) { { 'start_at' => '2014-05-27T11:56:25+03:00' } }
|
22
|
+
it { should == DateTime.new(2014, 5, 27, 11, 56, 25, '+3') }
|
23
|
+
end
|
24
|
+
|
25
|
+
context '#end_at' do
|
26
|
+
subject { test.end_at }
|
27
|
+
|
28
|
+
context 'nil' do
|
29
|
+
let(:test_hash) { {} }
|
30
|
+
it { should > DateTime.new(2020) }
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'april fools' do
|
34
|
+
let(:test_hash) { { 'end_at' => '2014-04-01T12:00:00+00:00' } }
|
35
|
+
it { should == DateTime.new(2014, 4, 1, 12) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
module Ab
|
4
|
-
describe
|
4
|
+
describe Tests do
|
5
5
|
describe '.new' do
|
6
|
-
subject {
|
6
|
+
subject { Tests.new(config, id) }
|
7
7
|
let(:id) { 1 }
|
8
8
|
|
9
9
|
context 'empty config' do
|
@@ -30,7 +30,7 @@ module Ab
|
|
30
30
|
}]
|
31
31
|
}
|
32
32
|
}
|
33
|
-
its(:feed) { should be_kind_of
|
33
|
+
its(:feed) { should be_kind_of AssignedTest }
|
34
34
|
its(:all) { should == { 'feed' => 'enabled' } }
|
35
35
|
end
|
36
36
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: vinted-ab
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mindaugas Mozūras
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-06-
|
12
|
+
date: 2014-06-10 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: hooks
|
@@ -100,17 +100,17 @@ files:
|
|
100
100
|
- Rakefile
|
101
101
|
- ab.gemspec
|
102
102
|
- lib/ab.rb
|
103
|
-
- lib/ab/
|
104
|
-
- lib/ab/
|
105
|
-
- lib/ab/
|
106
|
-
- lib/ab/
|
103
|
+
- lib/ab/assigned_test.rb
|
104
|
+
- lib/ab/null_test.rb
|
105
|
+
- lib/ab/test.rb
|
106
|
+
- lib/ab/tests.rb
|
107
107
|
- lib/ab/variant.rb
|
108
108
|
- lib/ab/version.rb
|
109
|
-
- spec/
|
110
|
-
- spec/
|
111
|
-
- spec/experiments_spec.rb
|
112
|
-
- spec/null_experiment_spec.rb
|
109
|
+
- spec/assigned_test_spec.rb
|
110
|
+
- spec/null_test_spec.rb
|
113
111
|
- spec/spec_helper.rb
|
112
|
+
- spec/test_spec.rb
|
113
|
+
- spec/tests_spec.rb
|
114
114
|
homepage: https://github.com/vinted/ab
|
115
115
|
licenses:
|
116
116
|
- MIT
|
@@ -1,54 +0,0 @@
|
|
1
|
-
module Ab
|
2
|
-
class AssignedExperiment
|
3
|
-
include Hooks
|
4
|
-
define_hooks :before_picking_variant, :after_picking_variant
|
5
|
-
|
6
|
-
def initialize(experiment, id)
|
7
|
-
@experiment, @id = experiment, id
|
8
|
-
@experiment.variants.map(&:name).each do |name|
|
9
|
-
define_singleton_method("#{name}?") { name == variant }
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
def variant
|
14
|
-
@variant ||= begin
|
15
|
-
return unless part_of_experiment?
|
16
|
-
return unless running?
|
17
|
-
|
18
|
-
run_hook :before_picking_variant, @experiment.name
|
19
|
-
picked_variant = @experiment.variants.find { |v| v.accumulated_chance_weight > weight_id }
|
20
|
-
|
21
|
-
result = picked_variant.name if picked_variant
|
22
|
-
run_hook :after_picking_variant, @experiment.name, result
|
23
|
-
result
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
28
|
-
|
29
|
-
def part_of_experiment?
|
30
|
-
@experiment.buckets == 'all' || @experiment.buckets.include?(bucket_id)
|
31
|
-
end
|
32
|
-
|
33
|
-
def bucket_id
|
34
|
-
@bucket_id ||= digest(@experiment.salt + @id.to_s) % @experiment.bucket_count
|
35
|
-
end
|
36
|
-
|
37
|
-
def running?
|
38
|
-
now = DateTime.now
|
39
|
-
now.between?(@experiment.start_at, @experiment.end_at)
|
40
|
-
end
|
41
|
-
|
42
|
-
def weight_id
|
43
|
-
@variant_digest ||= digest("#{@experiment.seed}#{@id}") % positive_weight_sum
|
44
|
-
end
|
45
|
-
|
46
|
-
def positive_weight_sum
|
47
|
-
@experiment.weight_sum > 0 ? @experiment.weight_sum : 1
|
48
|
-
end
|
49
|
-
|
50
|
-
def digest(string)
|
51
|
-
Digest::SHA256.hexdigest(string).to_i(16)
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
data/lib/ab/experiments.rb
DELETED
@@ -1,41 +0,0 @@
|
|
1
|
-
module Ab
|
2
|
-
class Experiments
|
3
|
-
include Hooks
|
4
|
-
define_hooks :before_picking_variant, :after_picking_variant
|
5
|
-
|
6
|
-
Ab::AssignedExperiment.before_picking_variant do |experiment|
|
7
|
-
Ab::Experiments.run_hook :before_picking_variant, experiment
|
8
|
-
end
|
9
|
-
Ab::AssignedExperiment.after_picking_variant do |experiment, variant|
|
10
|
-
Ab::Experiments.run_hook :after_picking_variant, experiment, variant
|
11
|
-
end
|
12
|
-
|
13
|
-
def initialize(config, id)
|
14
|
-
@assigned_experiments ||= {}
|
15
|
-
|
16
|
-
(config['ab_tests'] || []).each do |experiment|
|
17
|
-
name = experiment['name']
|
18
|
-
@assigned_experiments[name] = nil
|
19
|
-
define_singleton_method(name) do
|
20
|
-
experiment = Experiment.new(experiment, config['salt'], config['bucket_count'])
|
21
|
-
@assigned_experiments[name] ||= AssignedExperiment.new(experiment, id)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
def all
|
27
|
-
result = @assigned_experiments.keys.map do |name|
|
28
|
-
[name, send(name).variant]
|
29
|
-
end
|
30
|
-
Hash[result]
|
31
|
-
end
|
32
|
-
|
33
|
-
def method_missing(meth, *args, &block)
|
34
|
-
@null_experiment ||= NullExperiment.new
|
35
|
-
end
|
36
|
-
|
37
|
-
def respond_to?(meth)
|
38
|
-
true
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
data/spec/experiment_spec.rb
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
module Ab
|
4
|
-
describe Experiment do
|
5
|
-
let(:experiment) { Experiment.new(experiment_hash, '4321', 100) }
|
6
|
-
|
7
|
-
context '#buckets' do
|
8
|
-
subject { experiment.buckets }
|
9
|
-
let(:experiment_hash) { { 'buckets' => [1, 2, 3] } }
|
10
|
-
it { should == [1, 2, 3] }
|
11
|
-
end
|
12
|
-
|
13
|
-
context '#name' do
|
14
|
-
subject { experiment.name }
|
15
|
-
let(:experiment_hash) { { 'name' => 'test' } }
|
16
|
-
it { should == 'test' }
|
17
|
-
end
|
18
|
-
|
19
|
-
context '#start_at' do
|
20
|
-
subject { experiment.start_at }
|
21
|
-
let(:experiment_hash) { { 'start_at' => '2014-05-27T11:56:25+03:00' } }
|
22
|
-
it { should == DateTime.new(2014, 5, 27, 11, 56, 25, '+3') }
|
23
|
-
end
|
24
|
-
|
25
|
-
context '#end_at' do
|
26
|
-
subject { experiment.end_at }
|
27
|
-
|
28
|
-
context 'nil' do
|
29
|
-
let(:experiment_hash) { {} }
|
30
|
-
it { should > DateTime.new(2020) }
|
31
|
-
end
|
32
|
-
|
33
|
-
context 'april fools' do
|
34
|
-
let(:experiment_hash) { { 'end_at' => '2014-04-01T12:00:00+00:00' } }
|
35
|
-
it { should == DateTime.new(2014, 4, 1, 12) }
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|