ablab-core 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 +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
|