ablab 0.1.1 → 0.2.0

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.
@@ -0,0 +1,11 @@
1
+ require 'rails'
2
+
3
+ module Ablab
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Ablab
6
+
7
+ initializer "ablab.assets.precompile" do |app|
8
+ app.config.assets.precompile += %w(tracker.js)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,27 @@
1
+ require 'securerandom'
2
+
3
+ module Ablab
4
+ module Helper
5
+ def self.included(klass)
6
+ if klass.respond_to? :helper_method
7
+ self.instance_methods(false).each do |method|
8
+ klass.send(:helper_method, method)
9
+ klass.send(:private, method)
10
+ end
11
+ end
12
+ end
13
+
14
+ def experiment(name)
15
+ @experiments ||= {}
16
+ unless Ablab.experiments.has_key?(name)
17
+ raise "No experiment with name #{name}"
18
+ end
19
+ @experiments[name] ||=
20
+ Ablab.experiments[name].run(ablab_session_id)
21
+ end
22
+
23
+ def ablab_session_id
24
+ cookies[:ablab_sid] || cookies[:ablab_sid] = SecureRandom.hex
25
+ end
26
+ end
27
+ end
@@ -1,29 +1,56 @@
1
- module ABLab
1
+ require 'set'
2
+
3
+ module Ablab
2
4
  module Store
3
5
  class Memory
4
6
  def initialize
5
7
  @views = Hash.new do |hash, key|
6
8
  hash[key] = Hash.new { |hash, key| hash[key] = 0 }
7
9
  end
8
- @conversions = Hash.new do |hash, key|
10
+ @sessions = Hash.new do |hash, key|
11
+ hash[key] = Hash.new { |hash, key| hash[key] = Set.new }
12
+ end
13
+ @successes = Hash.new do |hash, key|
9
14
  hash[key] = Hash.new { |hash, key| hash[key] = 0 }
10
15
  end
16
+ @conversions = Hash.new do |hash, key|
17
+ hash[key] = Hash.new { |hash, key| hash[key] = Set.new }
18
+ end
11
19
  end
12
20
 
13
- def track_view!(experiment, bucket)
21
+ def track_view!(experiment, bucket, session_id)
14
22
  @views[experiment][bucket] += 1
23
+ @sessions[experiment][bucket].add(session_id)
15
24
  end
16
25
 
17
- def track_conversion!(experiment, bucket)
18
- @conversions[experiment][bucket] += 1
26
+ def track_success!(experiment, bucket, session_id)
27
+ @successes[experiment][bucket] += 1
28
+ @conversions[experiment][bucket].add(session_id)
19
29
  end
20
30
 
21
31
  def views(experiment, bucket)
22
32
  @views[experiment][bucket]
23
33
  end
24
34
 
35
+ def sessions(experiment, bucket)
36
+ @sessions[experiment][bucket].size
37
+ end
38
+
39
+ def successes(experiment, bucket)
40
+ @successes[experiment][bucket]
41
+ end
42
+
25
43
  def conversions(experiment, bucket)
26
- @conversions[experiment][bucket]
44
+ @conversions[experiment][bucket].size
45
+ end
46
+
47
+ def counts(experiment, bucket)
48
+ {
49
+ views: views(experiment, bucket),
50
+ sessions: sessions(experiment, bucket),
51
+ successes: successes(experiment, bucket),
52
+ conversions: conversions(experiment, bucket)
53
+ }
27
54
  end
28
55
  end
29
56
  end
@@ -1,6 +1,6 @@
1
1
  require 'redis'
2
2
 
3
- module ABLab
3
+ module Ablab
4
4
  module Store
5
5
  class Redis
6
6
  attr_reader :redis
@@ -10,28 +10,54 @@ module ABLab
10
10
  @redis = ::Redis.new(opts)
11
11
  end
12
12
 
13
- def track_view!(experiment, bucket)
14
- redis.hincrby(key(:views), field(experiment, bucket), 1)
13
+ def track_view!(experiment, bucket, session_id)
14
+ redis.pipelined do
15
+ redis.incr(key(:views, experiment, bucket))
16
+ redis.pfadd(key(:sessions, experiment, bucket), session_id)
17
+ end
15
18
  end
16
19
 
17
- def track_conversion!(experiment, bucket)
18
- redis.hincrby(key(:conversions), field(experiment, bucket), 1)
20
+ def track_success!(experiment, bucket, session_id)
21
+ redis.pipelined do
22
+ redis.incr(key(:successes, experiment, bucket))
23
+ redis.pfadd(key(:conversions, experiment, bucket), session_id)
24
+ end
19
25
  end
20
26
 
21
27
  def views(experiment, bucket)
22
- (redis.hget(key(:views), field(experiment, bucket)) || 0).to_i
28
+ (redis.get(key(:views, experiment, bucket)) || 0).to_i
29
+ end
30
+
31
+ def sessions(experiment, bucket)
32
+ (redis.pfcount(key(:sessions, experiment, bucket)) || 0).to_i
33
+ end
34
+
35
+ def successes(experiment, bucket)
36
+ (redis.get(key(:successes, experiment, bucket)) || 0).to_i
23
37
  end
24
38
 
25
39
  def conversions(experiment, bucket)
26
- (redis.hget(key(:conversions), field(experiment, bucket)) || 0).to_i
40
+ (redis.pfcount(key(:conversions, experiment, bucket)) || 0).to_i
27
41
  end
28
42
 
29
- private def key(type)
30
- "#{@key_prefix}:#{type}"
43
+ def counts(experiment, bucket)
44
+ v, s, x, c = nil, nil, nil, nil
45
+ redis.multi do
46
+ v = redis.get(key(:views, experiment, bucket))
47
+ s = redis.pfcount(key(:sessions, experiment, bucket))
48
+ x = redis.get(key(:successes, experiment, bucket))
49
+ c = redis.pfcount(key(:conversions, experiment, bucket))
50
+ end
51
+ {
52
+ views: (v.value || 0).to_i,
53
+ sessions: (s.value || 0).to_i,
54
+ successes: (x.value || 0).to_i,
55
+ conversions: (c.value || 0).to_i
56
+ }
31
57
  end
32
58
 
33
- private def field(experiment, bucket)
34
- "#{experiment}:#{bucket}"
59
+ private def key(type, experiment, bucket)
60
+ "#{@key_prefix}:#{type}:#{experiment}:#{bucket}"
35
61
  end
36
62
  end
37
63
  end
data/lib/ablab/version.rb CHANGED
@@ -1,3 +1,3 @@
1
- module ABLab
2
- VERSION = "0.1.1"
1
+ module Ablab
2
+ VERSION = "0.2.0"
3
3
  end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :ablab do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ describe Ablab::Helper do
5
+ let(:cookies) { Hash.new }
6
+
7
+ let(:controller) do
8
+ Class.new do
9
+ include Ablab::Helper
10
+
11
+ def env
12
+ { 'rack.session' => OpenStruct.new(id: 'abc123') }
13
+ end
14
+ end.new
15
+ end
16
+
17
+ before do
18
+ Ablab.setup do
19
+ experiment :xxx do
20
+ group :a
21
+ group :b
22
+ end
23
+ end
24
+ end
25
+
26
+ it 'calls helper_method if the including class implements it' do
27
+ klass = Class.new do
28
+ def self.helper_method(_); end
29
+ end
30
+ expect(klass).to receive(:helper_method).with :experiment
31
+ expect(klass).to receive(:helper_method).with :ablab_session_id
32
+ klass.send(:include, Ablab::Helper)
33
+ end
34
+
35
+ describe '#experiment' do
36
+ it 'returns a Run for the given experiment name' do
37
+ allow(controller).to receive(:cookies).and_return(cookies)
38
+ run = controller.send(:experiment, :xxx)
39
+ expect(run).to be_a(Ablab::Run)
40
+ expect(run.experiment).to be(Ablab.experiments[:xxx])
41
+ end
42
+
43
+ it 'returns the same Run if called twice with the same name' do
44
+ allow(controller).to receive(:cookies).and_return(cookies)
45
+ run = controller.send(:experiment, :xxx)
46
+ run2 = controller.send(:experiment, :xxx)
47
+ expect(run).to be(run2)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+ require 'ablab/store/store_examples'
3
+
4
+ describe Ablab::Store::Memory do
5
+ let(:store) { Ablab::Store::Memory.new }
6
+ include_examples 'store', Ablab::Store::Memory.new
7
+ end
@@ -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,74 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.shared_examples 'store' do
4
+ describe '#track_view!' do
5
+ it 'tracks views' do
6
+ 5.times { store.track_view!(:foo, :a, 'abc') }
7
+ 7.times { store.track_view!(:foo, :b, 'abc') }
8
+ 3.times { store.track_view!(:bar, :b, 'abc') }
9
+ expect(store.views(:foo, :a)).to eq(5)
10
+ expect(store.views(:foo, :b)).to eq(7)
11
+ expect(store.views(:bar, :b)).to eq(3)
12
+ end
13
+
14
+ it 'tracks unique sessions' do
15
+ 2.times { store.track_view!(:foo, :a, 'foo') }
16
+ 3.times { store.track_view!(:foo, :a, 'bar') }
17
+ 3.times { store.track_view!(:foo, :b, 'foo') }
18
+ 3.times { store.track_view!(:bar, :b, 'bar') }
19
+ expect(store.sessions(:foo, :a)).to eq(2)
20
+ expect(store.sessions(:foo, :b)).to eq(1)
21
+ expect(store.sessions(:bar, :b)).to eq(1)
22
+ end
23
+ end
24
+
25
+ describe '#track_success!' do
26
+ it 'tracks successes' do
27
+ 5.times { store.track_success!(:foo, :a, 'abc') }
28
+ 7.times { store.track_success!(:foo, :b, 'abc') }
29
+ 3.times { store.track_success!(:bar, :b, 'abc') }
30
+ expect(store.successes(:foo, :a)).to eq(5)
31
+ expect(store.successes(:foo, :b)).to eq(7)
32
+ expect(store.successes(:bar, :b)).to eq(3)
33
+ end
34
+
35
+ it 'tracks unique conversions' do
36
+ 5.times { |i| store.track_success!(:foo, :a, i) }
37
+ 2.times { store.track_success!(:foo, :b, 'foo') }
38
+ 3.times { store.track_success!(:foo, :b, 'bar') }
39
+ 3.times { store.track_success!(:bar, :b, 'foo') }
40
+ expect(store.conversions(:foo, :a)).to eq(5)
41
+ expect(store.conversions(:foo, :b)).to eq(2)
42
+ expect(store.conversions(:bar, :b)).to eq(1)
43
+ end
44
+ end
45
+
46
+ describe '#views' do
47
+ it 'returns 0 if nothing was tracked' do
48
+ expect(store.views(:xxx, :a)).to eq(0)
49
+ end
50
+ end
51
+
52
+ describe '#conversions' do
53
+ it 'returns 0 if nothing was tracked' do
54
+ expect(store.conversions(:xxx, :a)).to eq(0)
55
+ end
56
+ end
57
+
58
+ describe '#sessions' do
59
+ it 'returns 0 if nothing was tracked' do
60
+ expect(store.sessions(:xxx, :a)).to eq(0)
61
+ end
62
+ end
63
+
64
+ describe '#counts' do
65
+ it 'returns all counts in one call' do
66
+ 2.times { store.track_view!(:foo, :a, 'foo') }
67
+ 3.times { store.track_view!(:foo, :a, 'bar') }
68
+ 1.times { store.track_success!(:foo, :a, 'foo') }
69
+ 5.times { store.track_success!(:foo, :a, 'bar') }
70
+ 2.times { store.track_success!(:foo, :a, 'baz') }
71
+ expect(store.counts(:foo, :a)).to eq(views: 5, sessions: 2, successes: 8, conversions: 3)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,160 @@
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 '#description' 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 '#group' do
85
+ it 'creates a group' do
86
+ experiment.group :a, description: 'foo bar baz'
87
+ expect(experiment.groups.last).to be_a(Ablab::Group)
88
+ expect(experiment.groups.last.description).to eq('foo bar baz')
89
+ end
90
+
91
+ it 'symbolizes the group name' do
92
+ experiment.group 'yeah'
93
+ expect(experiment.groups.last.name).to eq(:yeah)
94
+ end
95
+ end
96
+
97
+ describe '.results' do
98
+ it 'returns the results of the experiment' do
99
+ experiment.group :x, description: 'a test group'
100
+ allow(Ablab.tracker).to receive(:views) do |_, group|
101
+ { control: 201, x: 238 }[group]
102
+ end
103
+ allow(Ablab.tracker).to receive(:sessions) do |_, group|
104
+ { control: 182, x: 188 }[group]
105
+ end
106
+ allow(Ablab.tracker).to receive(:successes) do |_, group|
107
+ { control: 38, x: 70 }[group]
108
+ end
109
+ allow(Ablab.tracker).to receive(:conversions) do |_, group|
110
+ { control: 35, x: 61 }[group]
111
+ end
112
+ results = experiment.results
113
+ expect(results[:control]).to eq({
114
+ views: 201,
115
+ sessions: 182,
116
+ successes: 38,
117
+ conversions: 35,
118
+ control: true,
119
+ description: 'control group'
120
+ })
121
+ expect(results[:x]).to eq({
122
+ views: 238,
123
+ sessions: 188,
124
+ successes: 70,
125
+ conversions: 61,
126
+ control: false,
127
+ z_score: 2.9410157224928595,
128
+ description: 'a test group'
129
+ })
130
+ end
131
+ end
132
+ end
133
+
134
+ describe Ablab::Run do
135
+ let(:experiment) do
136
+ Ablab::Experiment.new(:foo) do
137
+ group :a
138
+ group :b
139
+ end
140
+ end
141
+
142
+ it 'gets assigned to the right group' do
143
+ run = Ablab::Run.new(experiment, 0)
144
+ allow(run).to receive(:draw).and_return 0
145
+ expect(run).to be_in_group(:control)
146
+ run = Ablab::Run.new(experiment, 0)
147
+ allow(run).to receive(:draw).and_return 334
148
+ expect(run).to be_in_group(:a)
149
+ run = Ablab::Run.new(experiment, 0)
150
+ allow(run).to receive(:draw).and_return 999
151
+ expect(run).to be_in_group(:b)
152
+ end
153
+
154
+ it 'assigns the same session ID to the same group' do
155
+ run1 = Ablab::Run.new(experiment, 'foobar')
156
+ run2 = Ablab::Run.new(experiment, 'foobar')
157
+ expect(run1.group).to eq(run2.group)
158
+ end
159
+ end
160
+ end