vinted-ab 0.2.0 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 73b2901ba92e371c5f79d864db755df119cadd6a
4
- data.tar.gz: 14bfef36209d2fd22a70ec59d7741812a18c67a7
3
+ metadata.gz: 67031305b2c5c180288a616ff7d931d4918604dc
4
+ data.tar.gz: de313c446acea2eda841eea20930c82938628c0c
5
5
  SHA512:
6
- metadata.gz: 8ae98d8cd54d30db32c7a54aee1377a9b782f4ca9d38c7e5d00cfea6f63f7e616faba5309601698eea4045ba6497612f12d4a47ff3a9f90cdfefbe45d1a8df62
7
- data.tar.gz: 7c709e4b04db27be43ddc6b03fa37aca36d6e31dab8625abe153b2b68305c52ec8a8bbb138384295603ecccafec54eef421b8c65ee10c5937576761cc7fe2e61
6
+ metadata.gz: 8cd252fbb99403b6405401584879f9a410450adbaf2b8efa1c942d113e6046089fea519c90c7d4a9ae449e3e45a236acc1fc2ec0d9762071dcde2e2b88e07712
7
+ data.tar.gz: e0f21c86e0a21c40359da87572cfcfc9a23b955ea4d61c0730b547ea80d16a565e85c9a4c75da3551fbabda8f7c76703ae07c8cb7fa35f3bf3edeb8dc90fe9bd
data/README.md CHANGED
@@ -4,9 +4,9 @@
4
4
  [![Gem Version](https://badge.fury.io/rb/vinted-ab.png)](http://badge.fury.io/rb/vinted-ab)
5
5
  [![Dependency Status](https://gemnasium.com/vinted/ab.png)](https://gemnasium.com/vinted/ab)
6
6
 
7
- vinted-ab is used to determine whether an identifier belongs to a particular ab test and which variant of that ab test. Identifiers will usually represent users, but other scenario are possible. There are two parts to that: [Configuration](#configuration) and [Algorithm](#algorithm).
7
+ If you didn't guess it from the name, this library is meant for ab testing. But it doesn't cover everything associated with it, it lacks configuration and management parts. [vinted/ab](https://github.com/vinted/ab) is only used to determine which variant should be applied for a user. Two inputs are expected - [configuration](#configuration) and identifier. Identifier, at least in Vinted's case, represents users, but other scenarios are certainly possible.
8
8
 
9
- High-level description: Identifiers are divided into some number of buckets, using hashing. Before a test is started, buckets are chosen for that test. That gives the ability to pick the needed level of isolation. Each test also has a seed, which is used to randomise how users are divided among test variants.
9
+ Each identifier is assigned to a bucket, using a hashing function. Buckets can then be assigned to tests. That allows isolation control, when we don't want clashing and creation of biases. Each test also has a seed, which is used to randomise how identifiers are divided among test variants. You can find algorithm description [here](#algorithm) if you want more detail.
10
10
 
11
11
  ![users](https://cloud.githubusercontent.com/assets/54526/2971326/0535267a-db69-11e3-9878-e2b6a5d5505d.png)
12
12
 
@@ -19,8 +19,7 @@ ab = Ab::Tests.new(configuration, identifier)
19
19
  Ab::Tests.before_picking_variant { |test| puts "picking variant for #{test}" }
20
20
  Ab::Tests.after_picking_variant { |test, variant| puts "#{variant_name}" }
21
21
 
22
- # ab.test never returns nil
23
- # but if you don't belong to any of the buckets, variant will be nil
22
+ # ab.test never returns nil, but #variant can
24
23
  case ab.test.variant
25
24
  when 'red_button'
26
25
  red_button
@@ -32,6 +31,9 @@ end
32
31
 
33
32
  # calls #variant underneath, results of that call are cached
34
33
  puts 'red button' if ab.test.red_button?
34
+
35
+ # both start_at and end_at dates are accessible
36
+ puts 'newbie button' if user.created_at > ab.test.start_at && ab.test.for_newbies?
35
37
  ```
36
38
 
37
39
  ## Configuration
@@ -91,7 +93,7 @@ Short explanation for a couple of config parameters:
91
93
 
92
94
  `ab_tests.variants`: tests can have multiple variants, each with a name and a weight.
93
95
 
94
- More examples can be found in [spec/examples](https://github.com/vinted/ab/tree/master/spec/examples). Those examples are part of the test suite, which is run using [this code](https://github.com/vinted/ab/blob/master/spec/integration_spec.rb). We strongly recommend using those examples if you're reimplementing this library in another language.
96
+ More examples can be found in [spec/examples](https://github.com/vinted/ab/tree/master/spec/examples). Those examples are part of the test suite, which is run using [this code](https://github.com/vinted/ab/blob/master/spec/integration_spec.rb). `input.json` is configuration json and `output.json` gives expectations - which identifiers should fall to which variant. We strongly recommend using those examples if you're reimplementing this library in another language.
95
97
 
96
98
  ## Algorithm
97
99
 
@@ -100,12 +102,14 @@ Most of the logic, is in `AssignedTest` class, which can be used as an [example
100
102
  Here's some procedural pseudo code to serve as a reference:
101
103
 
102
104
  ```pseudo
103
- bucket_id = SHA256(salt + identifier.to_string).to_integer % bucket_count
105
+ salted_identifier = salt + identifier.to_string
106
+ bucket_id = SHA256.hexdigest(salted_identifier).to_int % bucket_count
104
107
 
105
108
  return if not (test.all_buckets? or test.buckets.include?(bucket_id))
106
109
  return if not DateTime.now.between?(test.start_at, test.end_at)
107
110
 
108
111
  chance_weight_sum = chance_weight_sum > 0 ? test.chance_weight_sum : 1
109
- weight_id = SHA256(test.seed + identifier.to_string).to_integer % chance_weight_sum
112
+ seeded_identifier = test.seed + identifier.to_string
113
+ weight_id = SHA256.hexdigest(seeded_identifier).to_int % chance_weight_sum
110
114
  test.variants.find { |variant| variant.accumulated_chance_weight > weight_id }
111
115
  ```
data/ab.gemspec CHANGED
@@ -24,4 +24,6 @@ Gem::Specification.new do |spec|
24
24
  spec.add_development_dependency 'rake', '~> 10.1', '>= 10.1.0'
25
25
  spec.add_development_dependency 'rspec', '~> 2.14', '>= 2.14.0'
26
26
  spec.add_development_dependency 'json-schema', '~> 2.2.2', '>= 2.2.2'
27
+ spec.add_development_dependency 'ruby-prof', '~> 0.15.0', '>= 0.15.0'
28
+ spec.add_development_dependency 'pry', '~> 0.10.0', '>= 0.10.0'
27
29
  end
@@ -24,6 +24,14 @@ module Ab
24
24
  end
25
25
  end
26
26
 
27
+ def start_at
28
+ @test.start_at
29
+ end
30
+
31
+ def end_at
32
+ @test.end_at
33
+ end
34
+
27
35
  private
28
36
 
29
37
  def part_of_test?
data/lib/ab/null_test.rb CHANGED
@@ -1,12 +1,23 @@
1
- class NullTest
2
- def variant
3
- end
1
+ module Ab
2
+ class NullTest
3
+ def variant
4
+ end
4
5
 
5
- def method_missing(meth, *args, &block)
6
- meth.to_s.end_with?('?') ? false : super
7
- end
6
+ def start_at
7
+ Test::DEFAULT_START_AT
8
+ end
9
+
10
+ def end_at
11
+ Test::DEFAULT_END_AT
12
+ end
13
+
14
+
15
+ def method_missing(meth, *args, &block)
16
+ meth.to_s.end_with?('?') ? false : super
17
+ end
8
18
 
9
- def respond_to?(meth)
10
- meth.to_s.end_with?('?') ? true : super
19
+ def respond_to?(meth)
20
+ meth.to_s.end_with?('?') ? true : super
21
+ end
11
22
  end
12
23
  end
data/lib/ab/test.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  module Ab
2
2
  class Test < Struct.new(:hash, :salt, :bucket_count)
3
+ DEFAULT_START_AT = DateTime.new(0)
4
+ DEFAULT_END_AT = DateTime.new(3000)
5
+
3
6
  def buckets
4
7
  hash['buckets']
5
8
  end
@@ -26,11 +29,11 @@ module Ab
26
29
  end
27
30
 
28
31
  def start_at
29
- @start_at ||= parse_time('start_at', 0)
32
+ @start_at ||= parse_time('start_at', DEFAULT_START_AT)
30
33
  end
31
34
 
32
35
  def end_at
33
- @end_at ||= parse_time('end_at', 3000)
36
+ @end_at ||= parse_time('end_at', DEFAULT_END_AT)
34
37
  end
35
38
 
36
39
  def weight_sum
@@ -41,7 +44,7 @@ module Ab
41
44
 
42
45
  def parse_time(name, default)
43
46
  value = hash[name]
44
- value.nil? ? DateTime.new(default) : DateTime.parse(value)
47
+ value.nil? ? default : DateTime.parse(value)
45
48
  end
46
49
  end
47
50
  end
data/lib/ab/tests.rb CHANGED
@@ -11,6 +11,7 @@ module Ab
11
11
  end
12
12
 
13
13
  def initialize(json, id)
14
+ json ||= {}
14
15
  config = json.is_a?(Hash) ? json : JSON.parse(json)
15
16
 
16
17
  salt = config['salt']
data/lib/ab/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Ab
2
- VERSION = '0.2.0'
2
+ VERSION = '0.2.1'
3
3
  end
@@ -21,6 +21,7 @@ describe 'ab' do
21
21
  cases.each do |id, variant|
22
22
  tests = Ab::Tests.new(input, id)
23
23
  tests.send(output['test']).variant.to_s.should == variant
24
+ tests.send(output['test']).send("#{variant}?").should be_true unless variant.empty?
24
25
  end
25
26
  end
26
27
  end
@@ -5,6 +5,8 @@ module Ab
5
5
  subject { NullTest.new }
6
6
 
7
7
  its(:variant) { should be_nil }
8
+ its(:start_at) { should_not be_nil }
9
+ its(:end_at) { should_not be_nil }
8
10
 
9
11
  specify 'does not raise for method ending in question mark' do
10
12
  expect { subject.bla? }.to_not raise_error
data/spec/tests_spec.rb CHANGED
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  module Ab
4
4
  describe Tests do
5
5
  let(:tests) { Tests.new(config, id) }
6
- let(:config) { {} }
6
+ let(:config) { nil }
7
7
  let(:id) { 1 }
8
8
 
9
9
  shared_context 'simple config with feed' do
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.2.0
4
+ version: 0.2.1
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-16 00:00:00.000000000 Z
12
+ date: 2014-06-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: hooks
@@ -105,6 +105,46 @@ dependencies:
105
105
  - - '>='
106
106
  - !ruby/object:Gem::Version
107
107
  version: 2.2.2
108
+ - !ruby/object:Gem::Dependency
109
+ name: ruby-prof
110
+ requirement: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ~>
113
+ - !ruby/object:Gem::Version
114
+ version: 0.15.0
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: 0.15.0
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ~>
123
+ - !ruby/object:Gem::Version
124
+ version: 0.15.0
125
+ - - '>='
126
+ - !ruby/object:Gem::Version
127
+ version: 0.15.0
128
+ - !ruby/object:Gem::Dependency
129
+ name: pry
130
+ requirement: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ~>
133
+ - !ruby/object:Gem::Version
134
+ version: 0.10.0
135
+ - - '>='
136
+ - !ruby/object:Gem::Version
137
+ version: 0.10.0
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ~>
143
+ - !ruby/object:Gem::Version
144
+ version: 0.10.0
145
+ - - '>='
146
+ - !ruby/object:Gem::Version
147
+ version: 0.10.0
108
148
  description:
109
149
  email:
110
150
  - mindaugas.mozuras@gmail.com