vinted-ab 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|