ablab-core 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +28 -0
- data/lib/ablab.rb +158 -0
- data/lib/ablab/store.rb +2 -0
- data/lib/ablab/store/memory.rb +61 -0
- data/lib/ablab/store/redis.rb +67 -0
- data/lib/ablab/version.rb +3 -0
- data/lib/tasks/ablab_tasks.rake +4 -0
- data/spec/ablab/store/memory_spec.rb +7 -0
- data/spec/ablab/store/redis_spec.rb +25 -0
- data/spec/ablab/store/store_examples.rb +82 -0
- data/spec/ablab_spec.rb +187 -0
- data/spec/spec_helper.rb +2 -0
- metadata +118 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f5f41b9cd3f4f0cfc04bfd00fcf5209cc800d932
|
4
|
+
data.tar.gz: 9e3afbbd5baf0a22bcb4a6719f111c8a38e6af6f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cbe6dc86065bd3fa2e57e99292e008958c8d1829625a0774162bfa2d25a5d243fbc9bbaa31f1ed7b9a38fb5bb6bb647d101395556d3f7d19eb6705f798843b7f
|
7
|
+
data.tar.gz: 80b85998ece7e265af961bb25089d0e4d57ac0b2ecde848c0f002122e1e8aea35c5cf759cbdd7244a14a258f37ebc648fd3338e3dbe664b7ff362c08101f55e8
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2016 Luca Ongaro
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Ablab'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
load 'rails/tasks/statistics.rake'
|
18
|
+
|
19
|
+
require "rspec/core/rake_task"
|
20
|
+
|
21
|
+
Bundler::GemHelper.install_tasks(name: 'ablab')
|
22
|
+
|
23
|
+
namespace :core do
|
24
|
+
Bundler::GemHelper.install_tasks(name: 'ablab-core')
|
25
|
+
end
|
26
|
+
|
27
|
+
RSpec::Core::RakeTask.new(:spec)
|
28
|
+
task default: :spec
|
data/lib/ablab.rb
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
require "ablab/version"
|
2
|
+
require "ablab/helper"
|
3
|
+
require "ablab/store"
|
4
|
+
require "ablab/engine"
|
5
|
+
require "forwardable"
|
6
|
+
|
7
|
+
module Ablab
|
8
|
+
module ModuleMethods
|
9
|
+
attr_reader :experiments
|
10
|
+
|
11
|
+
def setup(&block)
|
12
|
+
instance_exec(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def experiment(name, &block)
|
16
|
+
@experiments ||= {}
|
17
|
+
@experiments[name] = Experiment.new(name, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def store(type, *args)
|
21
|
+
if type.is_a? Class
|
22
|
+
@tracker = Class.new(*args)
|
23
|
+
else
|
24
|
+
class_name = type.to_s.split('_').map(&:capitalize).join
|
25
|
+
@tracker = Ablab::Store.const_get(class_name).new(*args)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def tracker
|
30
|
+
@tracker ||= Ablab::Store::Memory.new
|
31
|
+
end
|
32
|
+
|
33
|
+
def dashboard_credentials(credentials = nil)
|
34
|
+
if credentials
|
35
|
+
unless credentials[:name] && credentials[:password]
|
36
|
+
raise InvalidCredentials, 'credentials should provide name and password'
|
37
|
+
end
|
38
|
+
@dashboard_credentials = credentials
|
39
|
+
end
|
40
|
+
@dashboard_credentials
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class InvalidCredentials < ArgumentError; end
|
45
|
+
|
46
|
+
extend ModuleMethods
|
47
|
+
|
48
|
+
class Experiment
|
49
|
+
attr_reader :name, :groups, :control
|
50
|
+
|
51
|
+
def initialize(name, &block)
|
52
|
+
@name = name.to_sym
|
53
|
+
@control = Group.new(:control, 'control group')
|
54
|
+
@groups = [@control]
|
55
|
+
instance_exec(&block)
|
56
|
+
end
|
57
|
+
|
58
|
+
def description(desc = nil)
|
59
|
+
@description = desc if desc
|
60
|
+
@description
|
61
|
+
end
|
62
|
+
|
63
|
+
def goal(goal = nil)
|
64
|
+
@goal = goal if goal
|
65
|
+
@goal
|
66
|
+
end
|
67
|
+
|
68
|
+
def percentage_of_visitors(percentage = nil)
|
69
|
+
@percentage_of_visitors = percentage if percentage
|
70
|
+
@percentage_of_visitors || 100
|
71
|
+
end
|
72
|
+
|
73
|
+
def group(name, options = {})
|
74
|
+
group = Group.new(name, options[:description])
|
75
|
+
@groups << group
|
76
|
+
end
|
77
|
+
|
78
|
+
def results
|
79
|
+
@result ||= Result.new(self)
|
80
|
+
@result.data
|
81
|
+
end
|
82
|
+
|
83
|
+
def run(session_id)
|
84
|
+
Run.new(self, session_id)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class Run
|
89
|
+
attr_reader :experiment, :session_id
|
90
|
+
|
91
|
+
def initialize(experiment, session_id)
|
92
|
+
@experiment, @session_id = experiment, session_id
|
93
|
+
end
|
94
|
+
|
95
|
+
def in_group?(name)
|
96
|
+
group == name
|
97
|
+
end
|
98
|
+
|
99
|
+
def track_view!
|
100
|
+
Ablab.tracker.track_view!(experiment.name, group, session_id)
|
101
|
+
end
|
102
|
+
|
103
|
+
def track_success!
|
104
|
+
Ablab.tracker.track_success!(experiment.name, group, session_id)
|
105
|
+
end
|
106
|
+
|
107
|
+
def group
|
108
|
+
return @group unless @group.nil?
|
109
|
+
size = 1000.0 * (experiment.percentage_of_visitors) / 100.0
|
110
|
+
idx = (draw * experiment.groups.size / size).floor
|
111
|
+
@group = experiment.groups[idx].try(:name)
|
112
|
+
end
|
113
|
+
|
114
|
+
def draw
|
115
|
+
Random.new(session_id.hash ^ experiment.name.hash).rand(1000)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class Group
|
120
|
+
attr_reader :name, :description
|
121
|
+
def initialize(name, description = nil)
|
122
|
+
@name, @description = name.to_sym, description
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class Result
|
127
|
+
extend Forwardable
|
128
|
+
def_delegators :@experiment, :name, :control, :groups
|
129
|
+
|
130
|
+
def initialize(experiment)
|
131
|
+
@experiment = experiment
|
132
|
+
end
|
133
|
+
|
134
|
+
def data
|
135
|
+
counts_c = counts(control)
|
136
|
+
groups.map do |group|
|
137
|
+
if group == control
|
138
|
+
next [group.name, counts_c.merge(control: true, description: group.description)]
|
139
|
+
end
|
140
|
+
counts = counts(group)
|
141
|
+
z = z_score(counts[:sessions], counts[:conversions],
|
142
|
+
counts_c[:sessions], counts_c[:conversions])
|
143
|
+
[group.name, counts.merge(z_score: z, control: false, description: group.description)]
|
144
|
+
end.to_h
|
145
|
+
end
|
146
|
+
|
147
|
+
private def counts(group)
|
148
|
+
Ablab.tracker.counts(name, group.name)
|
149
|
+
end
|
150
|
+
|
151
|
+
private def z_score(s, c, sc, cc)
|
152
|
+
return nil if s == 0 || sc == 0
|
153
|
+
p = c.to_f / s
|
154
|
+
pc = cc.to_f / sc
|
155
|
+
(p - pc) / Math.sqrt((p*(1 - p) / s) + (pc*(1 - pc) / sc))
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
data/lib/ablab/store.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Ablab
|
4
|
+
module Store
|
5
|
+
class Memory
|
6
|
+
def initialize
|
7
|
+
@views = Hash.new do |hash, key|
|
8
|
+
hash[key] = Hash.new { |h, k| h[k] = 0 }
|
9
|
+
end
|
10
|
+
@sessions = Hash.new do |hash, key|
|
11
|
+
hash[key] = Hash.new { |h, k| h[k] = Set.new }
|
12
|
+
end
|
13
|
+
@successes = Hash.new do |hash, key|
|
14
|
+
hash[key] = Hash.new { |h, k| h[k] = 0 }
|
15
|
+
end
|
16
|
+
@conversions = Hash.new do |hash, key|
|
17
|
+
hash[key] = Hash.new { |h, k| h[k] = Set.new }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def track_view!(experiment, bucket, session_id)
|
22
|
+
track(experiment, bucket, session_id, @views, @sessions)
|
23
|
+
end
|
24
|
+
|
25
|
+
def track_success!(experiment, bucket, session_id)
|
26
|
+
track(experiment, bucket, session_id, @successes, @conversions)
|
27
|
+
end
|
28
|
+
|
29
|
+
def views(experiment, bucket)
|
30
|
+
@views[experiment][bucket]
|
31
|
+
end
|
32
|
+
|
33
|
+
def sessions(experiment, bucket)
|
34
|
+
@sessions[experiment][bucket].size
|
35
|
+
end
|
36
|
+
|
37
|
+
def successes(experiment, bucket)
|
38
|
+
@successes[experiment][bucket]
|
39
|
+
end
|
40
|
+
|
41
|
+
def conversions(experiment, bucket)
|
42
|
+
@conversions[experiment][bucket].size
|
43
|
+
end
|
44
|
+
|
45
|
+
def counts(experiment, bucket)
|
46
|
+
{
|
47
|
+
views: views(experiment, bucket),
|
48
|
+
sessions: sessions(experiment, bucket),
|
49
|
+
successes: successes(experiment, bucket),
|
50
|
+
conversions: conversions(experiment, bucket)
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
private def track(experiment, bucket, session_id, counter, set)
|
55
|
+
return false if bucket.nil?
|
56
|
+
counter[experiment][bucket] += 1
|
57
|
+
set[experiment][bucket].add(session_id)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
module Ablab
|
4
|
+
module Store
|
5
|
+
class Redis
|
6
|
+
attr_reader :redis
|
7
|
+
|
8
|
+
def initialize(opts = {})
|
9
|
+
@key_prefix = opts[:key_prefix] || 'ablab'
|
10
|
+
@redis = ::Redis.new(opts)
|
11
|
+
end
|
12
|
+
|
13
|
+
def track_view!(experiment, bucket, session_id)
|
14
|
+
track(experiment, bucket, session_id, :views, :sessions)
|
15
|
+
end
|
16
|
+
|
17
|
+
def track_success!(experiment, bucket, session_id)
|
18
|
+
track(experiment, bucket, session_id, :successes, :conversions)
|
19
|
+
end
|
20
|
+
|
21
|
+
def views(experiment, bucket)
|
22
|
+
(redis.get(key(:views, experiment, bucket)) || 0).to_i
|
23
|
+
end
|
24
|
+
|
25
|
+
def sessions(experiment, bucket)
|
26
|
+
(redis.pfcount(key(:sessions, experiment, bucket)) || 0).to_i
|
27
|
+
end
|
28
|
+
|
29
|
+
def successes(experiment, bucket)
|
30
|
+
(redis.get(key(:successes, experiment, bucket)) || 0).to_i
|
31
|
+
end
|
32
|
+
|
33
|
+
def conversions(experiment, bucket)
|
34
|
+
(redis.pfcount(key(:conversions, experiment, bucket)) || 0).to_i
|
35
|
+
end
|
36
|
+
|
37
|
+
def counts(experiment, bucket)
|
38
|
+
v, s, x, c = nil, nil, nil, nil
|
39
|
+
redis.multi do
|
40
|
+
v = redis.get(key(:views, experiment, bucket))
|
41
|
+
s = redis.pfcount(key(:sessions, experiment, bucket))
|
42
|
+
x = redis.get(key(:successes, experiment, bucket))
|
43
|
+
c = redis.pfcount(key(:conversions, experiment, bucket))
|
44
|
+
end
|
45
|
+
{
|
46
|
+
views: (v.value || 0).to_i,
|
47
|
+
sessions: (s.value || 0).to_i,
|
48
|
+
successes: (x.value || 0).to_i,
|
49
|
+
conversions: (c.value || 0).to_i
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
private def key(type, experiment, bucket)
|
54
|
+
"#{@key_prefix}:#{type}:#{experiment}:#{bucket}"
|
55
|
+
end
|
56
|
+
|
57
|
+
private def track(experiment, bucket, session_id, counter, set)
|
58
|
+
return false if bucket.nil?
|
59
|
+
redis.pipelined do
|
60
|
+
redis.incr(key(counter, experiment, bucket))
|
61
|
+
redis.pfadd(key(set, experiment, bucket), session_id)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'ablab/store/store_examples'
|
3
|
+
|
4
|
+
describe Ablab::Store::Redis do
|
5
|
+
def cleanup
|
6
|
+
redis = ::Redis.new(db: 2)
|
7
|
+
keys = redis.keys('ablabtest:*')
|
8
|
+
redis.pipelined do
|
9
|
+
keys.each { |key| redis.del(key) }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
around do |example|
|
14
|
+
begin
|
15
|
+
cleanup
|
16
|
+
example.run
|
17
|
+
ensure
|
18
|
+
cleanup
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
let(:store) { Ablab::Store::Redis.new(db: 2, key_prefix: 'ablabtest') }
|
23
|
+
include_examples 'store'
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.shared_examples 'store' do
|
4
|
+
describe '#track_view!' do
|
5
|
+
it 'returns false if group is nil' do
|
6
|
+
expect(store.track_view!(:foo, nil, 'abc')).to be(false)
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'tracks views' do
|
10
|
+
5.times { store.track_view!(:foo, :a, 'abc') }
|
11
|
+
7.times { store.track_view!(:foo, :b, 'abc') }
|
12
|
+
3.times { store.track_view!(:bar, :b, 'abc') }
|
13
|
+
expect(store.views(:foo, :a)).to eq(5)
|
14
|
+
expect(store.views(:foo, :b)).to eq(7)
|
15
|
+
expect(store.views(:bar, :b)).to eq(3)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'tracks unique sessions' do
|
19
|
+
2.times { store.track_view!(:foo, :a, 'foo') }
|
20
|
+
3.times { store.track_view!(:foo, :a, 'bar') }
|
21
|
+
3.times { store.track_view!(:foo, :b, 'foo') }
|
22
|
+
3.times { store.track_view!(:bar, :b, 'bar') }
|
23
|
+
expect(store.sessions(:foo, :a)).to eq(2)
|
24
|
+
expect(store.sessions(:foo, :b)).to eq(1)
|
25
|
+
expect(store.sessions(:bar, :b)).to eq(1)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#track_success!' do
|
30
|
+
it 'returns false if group is nil' do
|
31
|
+
expect(store.track_success!(:foo, nil, 'abc')).to be(false)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'tracks successes' do
|
35
|
+
5.times { store.track_success!(:foo, :a, 'abc') }
|
36
|
+
7.times { store.track_success!(:foo, :b, 'abc') }
|
37
|
+
3.times { store.track_success!(:bar, :b, 'abc') }
|
38
|
+
expect(store.successes(:foo, :a)).to eq(5)
|
39
|
+
expect(store.successes(:foo, :b)).to eq(7)
|
40
|
+
expect(store.successes(:bar, :b)).to eq(3)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'tracks unique conversions' do
|
44
|
+
5.times { |i| store.track_success!(:foo, :a, i) }
|
45
|
+
2.times { store.track_success!(:foo, :b, 'foo') }
|
46
|
+
3.times { store.track_success!(:foo, :b, 'bar') }
|
47
|
+
3.times { store.track_success!(:bar, :b, 'foo') }
|
48
|
+
expect(store.conversions(:foo, :a)).to eq(5)
|
49
|
+
expect(store.conversions(:foo, :b)).to eq(2)
|
50
|
+
expect(store.conversions(:bar, :b)).to eq(1)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#views' do
|
55
|
+
it 'returns 0 if nothing was tracked' do
|
56
|
+
expect(store.views(:xxx, :a)).to eq(0)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe '#conversions' do
|
61
|
+
it 'returns 0 if nothing was tracked' do
|
62
|
+
expect(store.conversions(:xxx, :a)).to eq(0)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '#sessions' do
|
67
|
+
it 'returns 0 if nothing was tracked' do
|
68
|
+
expect(store.sessions(:xxx, :a)).to eq(0)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '#counts' do
|
73
|
+
it 'returns all counts in one call' do
|
74
|
+
2.times { store.track_view!(:foo, :a, 'foo') }
|
75
|
+
3.times { store.track_view!(:foo, :a, 'bar') }
|
76
|
+
1.times { store.track_success!(:foo, :a, 'foo') }
|
77
|
+
5.times { store.track_success!(:foo, :a, 'bar') }
|
78
|
+
2.times { store.track_success!(:foo, :a, 'baz') }
|
79
|
+
expect(store.counts(:foo, :a)).to eq(views: 5, sessions: 2, successes: 8, conversions: 3)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/spec/ablab_spec.rb
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Ablab do
|
4
|
+
let(:ab) do
|
5
|
+
Module.new { extend Ablab::ModuleMethods }
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'has a version number' do
|
9
|
+
expect(Ablab::VERSION).not_to be nil
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '.experiment' do
|
13
|
+
it 'creates an experiment' do
|
14
|
+
ab.experiment :foo_bar do; end
|
15
|
+
expect(ab.experiments[:foo_bar]).to be_a(Ablab::Experiment)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '.tracker' do
|
20
|
+
around do |example|
|
21
|
+
begin
|
22
|
+
Ablab::Store::Dummy = Class.new
|
23
|
+
example.run
|
24
|
+
ensure
|
25
|
+
Ablab::Store.send(:remove_const, :Dummy)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'returns a Ablab::Store::Memory instance if store was never set' do
|
30
|
+
expect(ab.tracker).to be_a(Ablab::Store::Memory)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'returns the store if it was set' do
|
34
|
+
ab.store :dummy
|
35
|
+
expect(ab.tracker).to be_a(Ablab::Store::Dummy)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '.dashboard_credentials' do
|
40
|
+
it 'raises if called without name or password' do
|
41
|
+
expect {
|
42
|
+
ab.dashboard_credentials(name: 'foo')
|
43
|
+
}.to raise_error(Ablab::InvalidCredentials)
|
44
|
+
|
45
|
+
expect {
|
46
|
+
ab.dashboard_credentials(password: 'foo')
|
47
|
+
}.to raise_error(Ablab::InvalidCredentials)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'sets and gets the credentials' do
|
51
|
+
ab.dashboard_credentials(name: 'foo', password: 'bar')
|
52
|
+
expect(ab.dashboard_credentials).to eq(name: 'foo', password: 'bar')
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe Ablab::Experiment do
|
57
|
+
let(:experiment) do
|
58
|
+
Ablab::Experiment.new('foo') do; end
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'automatically creates a control group' do
|
62
|
+
expect(experiment.control).to be_a(Ablab::Group)
|
63
|
+
expect(experiment.groups).to_not be_empty
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'symbolizes its name' do
|
67
|
+
expect(experiment.name).to eq(:foo)
|
68
|
+
end
|
69
|
+
|
70
|
+
describe '#description' do
|
71
|
+
it 'sets the description' do
|
72
|
+
experiment.description 'foo bar'
|
73
|
+
expect(experiment.description).to eq('foo bar')
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe '#goal' do
|
78
|
+
it 'sets the experiment goal' do
|
79
|
+
experiment.goal 'foo bar'
|
80
|
+
expect(experiment.goal).to eq('foo bar')
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe '#percentage_of_visitors' do
|
85
|
+
it 'returns 100 if never set' do
|
86
|
+
expect(experiment.percentage_of_visitors).to eq(100)
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'sets the percentage of visitors included in the experiment' do
|
90
|
+
experiment.percentage_of_visitors 25
|
91
|
+
expect(experiment.percentage_of_visitors).to eq(25)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe '#group' do
|
96
|
+
it 'creates a group' do
|
97
|
+
experiment.group :a, description: 'foo bar baz'
|
98
|
+
expect(experiment.groups.last).to be_a(Ablab::Group)
|
99
|
+
expect(experiment.groups.last.description).to eq('foo bar baz')
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'symbolizes the group name' do
|
103
|
+
experiment.group 'yeah'
|
104
|
+
expect(experiment.groups.last.name).to eq(:yeah)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe '.results' do
|
109
|
+
it 'returns the results of the experiment' do
|
110
|
+
experiment.group :x, description: 'a test group'
|
111
|
+
allow(Ablab.tracker).to receive(:views) do |_, group|
|
112
|
+
{ control: 201, x: 238 }[group]
|
113
|
+
end
|
114
|
+
allow(Ablab.tracker).to receive(:sessions) do |_, group|
|
115
|
+
{ control: 182, x: 188 }[group]
|
116
|
+
end
|
117
|
+
allow(Ablab.tracker).to receive(:successes) do |_, group|
|
118
|
+
{ control: 38, x: 70 }[group]
|
119
|
+
end
|
120
|
+
allow(Ablab.tracker).to receive(:conversions) do |_, group|
|
121
|
+
{ control: 35, x: 61 }[group]
|
122
|
+
end
|
123
|
+
results = experiment.results
|
124
|
+
expect(results[:control]).to eq({
|
125
|
+
views: 201,
|
126
|
+
sessions: 182,
|
127
|
+
successes: 38,
|
128
|
+
conversions: 35,
|
129
|
+
control: true,
|
130
|
+
description: 'control group'
|
131
|
+
})
|
132
|
+
expect(results[:x]).to eq({
|
133
|
+
views: 238,
|
134
|
+
sessions: 188,
|
135
|
+
successes: 70,
|
136
|
+
conversions: 61,
|
137
|
+
control: false,
|
138
|
+
z_score: 2.9410157224928595,
|
139
|
+
description: 'a test group'
|
140
|
+
})
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
describe Ablab::Run do
|
146
|
+
let(:experiment) do
|
147
|
+
Ablab::Experiment.new(:foo) do
|
148
|
+
group :a
|
149
|
+
group :b
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'gets assigned to the right group' do
|
154
|
+
run = Ablab::Run.new(experiment, 0)
|
155
|
+
allow(run).to receive(:draw).and_return 0
|
156
|
+
expect(run).to be_in_group(:control)
|
157
|
+
run = Ablab::Run.new(experiment, 0)
|
158
|
+
allow(run).to receive(:draw).and_return 334
|
159
|
+
expect(run).to be_in_group(:a)
|
160
|
+
run = Ablab::Run.new(experiment, 0)
|
161
|
+
allow(run).to receive(:draw).and_return 999
|
162
|
+
expect(run).to be_in_group(:b)
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'assigns the same session ID to the same group' do
|
166
|
+
run1 = Ablab::Run.new(experiment, 'foobar')
|
167
|
+
run2 = Ablab::Run.new(experiment, 'foobar')
|
168
|
+
expect(run1.group).to eq(run2.group)
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'selects only the given percentage of users' do
|
172
|
+
experiment.percentage_of_visitors 30
|
173
|
+
run = Ablab::Run.new(experiment, 0)
|
174
|
+
allow(run).to receive(:draw).and_return 0
|
175
|
+
expect(run).to be_in_group(:control)
|
176
|
+
run = Ablab::Run.new(experiment, 0)
|
177
|
+
allow(run).to receive(:draw).and_return 100
|
178
|
+
expect(run).to be_in_group(:a)
|
179
|
+
run = Ablab::Run.new(experiment, 0)
|
180
|
+
allow(run).to receive(:draw).and_return 200
|
181
|
+
expect(run).to be_in_group(:b)
|
182
|
+
run = Ablab::Run.new(experiment, 0)
|
183
|
+
allow(run).to receive(:draw).and_return 300
|
184
|
+
expect(run.group).to be_nil
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ablab-core
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Luca Ongaro
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-01-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: redis
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Ablab - A/B testing library
|
70
|
+
email:
|
71
|
+
- lukeongaro@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- MIT-LICENSE
|
77
|
+
- Rakefile
|
78
|
+
- lib/ablab.rb
|
79
|
+
- lib/ablab/store.rb
|
80
|
+
- lib/ablab/store/memory.rb
|
81
|
+
- lib/ablab/store/redis.rb
|
82
|
+
- lib/ablab/version.rb
|
83
|
+
- lib/tasks/ablab_tasks.rake
|
84
|
+
- spec/ablab/store/memory_spec.rb
|
85
|
+
- spec/ablab/store/redis_spec.rb
|
86
|
+
- spec/ablab/store/store_examples.rb
|
87
|
+
- spec/ablab_spec.rb
|
88
|
+
- spec/spec_helper.rb
|
89
|
+
homepage: https://github.com/lucaong/ablab
|
90
|
+
licenses:
|
91
|
+
- MIT
|
92
|
+
metadata: {}
|
93
|
+
post_install_message:
|
94
|
+
rdoc_options: []
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
requirements: []
|
108
|
+
rubyforge_project:
|
109
|
+
rubygems_version: 2.5.1
|
110
|
+
signing_key:
|
111
|
+
specification_version: 4
|
112
|
+
summary: Ablab core - A/B testing library
|
113
|
+
test_files:
|
114
|
+
- spec/ablab/store/memory_spec.rb
|
115
|
+
- spec/ablab/store/redis_spec.rb
|
116
|
+
- spec/ablab/store/store_examples.rb
|
117
|
+
- spec/ablab_spec.rb
|
118
|
+
- spec/spec_helper.rb
|