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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3566b6e9c6dc50f3ca1e435644b7e3bb1eab9fd1
4
- data.tar.gz: 062d9118532a050d1155981c7ca4633911365421
3
+ metadata.gz: 45ab4518594015ccbca8ebd4d441f835e103e22c
4
+ data.tar.gz: 80fa73e5144373f439208f7773b82c76c4949de4
5
5
  SHA512:
6
- metadata.gz: 3a23e68efe7ebd7e8442f088feb526f1718338b4b41e94375de361deca94b865ad526634a4fd267999b6aebed3d4729d419a7a0a1bbc6159480e38e2b4516e42
7
- data.tar.gz: 5f40b68ced5a8adf1f9eed2a34066e68ca94d7716419694bcba22004b3df738509a079d9eb37aeb3987bfecafd279c40114c7dbb7e03c8d9813acf89e90ec903
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::Experiments.new(configuration, user_id)
85
+ ab = Ab::Tests.new(configuration, user_id)
86
86
 
87
87
  # defining callbacks
88
- Ab::Experiments.before_picking_variant { |experiment| puts 'magic' }
89
- Ab::Experiments.after_picking_variant { |experiment, variant| puts "#{variant_name}" }
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
@@ -1,4 +1,4 @@
1
- class NullExperiment
1
+ class NullTest
2
2
  def variant
3
3
  nil
4
4
  end
@@ -1,5 +1,5 @@
1
1
  module Ab
2
- class Experiment < Struct.new(:hash, :salt, :bucket_count)
2
+ class Test < Struct.new(:hash, :salt, :bucket_count)
3
3
  def buckets
4
4
  hash['buckets']
5
5
  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
@@ -1,3 +1,3 @@
1
1
  module Ab
2
- VERSION = '0.0.3'
2
+ VERSION = '0.1.0'
3
3
  end
data/lib/ab.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'hooks'
2
2
  require 'ab/version'
3
- require 'ab/null_experiment'
3
+ require 'ab/null_test'
4
4
  require 'ab/variant'
5
- require 'ab/experiment'
6
- require 'ab/assigned_experiment'
7
- require 'ab/experiments'
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 AssignedExperiment do
5
- let(:assigned_experiment) { AssignedExperiment.new(experiment, id) }
6
- let(:experiment) { Experiment.new(hash, 'e131bfcfcab06c65d633d0266c7e62a4918', 1000) }
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| AssignedExperiment.new(experiment, i).variant } }
23
+ let(:thousand_variants) { 1.upto(1000).map { |i| AssignedTest.new(test, i).variant } }
24
24
 
25
25
  describe '#variant' do
26
- subject { assigned_experiment.variant }
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 'experiment that has not started yet' do
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 'experiment that has already ended' do
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 }] }
@@ -1,8 +1,8 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  module Ab
4
- describe NullExperiment do
5
- subject { NullExperiment.new }
4
+ describe NullTest do
5
+ subject { NullTest.new }
6
6
 
7
7
  specify 'does not raise for method ending in question mark' do
8
8
  expect{ subject.bla? }.to_not raise_error
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 Experiments do
4
+ describe Tests do
5
5
  describe '.new' do
6
- subject { Experiments.new(config, id) }
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 AssignedExperiment }
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.3
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-02 00:00:00.000000000 Z
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/assigned_experiment.rb
104
- - lib/ab/experiment.rb
105
- - lib/ab/experiments.rb
106
- - lib/ab/null_experiment.rb
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/assigned_experiment_spec.rb
110
- - spec/experiment_spec.rb
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
@@ -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
@@ -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