vanity 2.2.6 → 2.2.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -2
- data/CHANGELOG +9 -0
- data/Gemfile.lock +1 -1
- data/doc/ab_testing.textile +5 -0
- data/gemfiles/rails32.gemfile.lock +1 -1
- data/gemfiles/rails41.gemfile.lock +1 -1
- data/gemfiles/rails42.gemfile.lock +1 -1
- data/gemfiles/rails42_protected_attributes.gemfile.lock +1 -1
- data/gemfiles/rails5.gemfile.lock +1 -1
- data/lib/vanity/adapters/abstract_adapter.rb +10 -0
- data/lib/vanity/adapters/active_record_adapter.rb +6 -4
- data/lib/vanity/adapters/mock_adapter.rb +33 -9
- data/lib/vanity/adapters/mongodb_adapter.rb +5 -3
- data/lib/vanity/adapters/redis_adapter.rb +9 -7
- data/lib/vanity/connection.rb +49 -37
- data/lib/vanity/experiment/ab_test.rb +2 -2
- data/lib/vanity/frameworks/rails.rb +2 -2
- data/lib/vanity/version.rb +1 -1
- data/test/adapters/active_record_adapter_test.rb +19 -0
- data/test/adapters/mock_adapter_test.rb +16 -0
- data/test/adapters/mongodb_adapter_test.rb +19 -0
- data/test/adapters/redis_adapter_test.rb +12 -0
- data/test/adapters/shared_tests.rb +336 -0
- data/test/test_helper.rb +4 -2
- data/test/vanity_test.rb +9 -5
- metadata +11 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 30ab64490eb076e3145a45d198b65778073979d6
|
4
|
+
data.tar.gz: 318343bcd0ba889d85cef2c4a3e8cf7b95ac8d78
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 08ac7419f74bb5282753cf8f5303de34eea8b6bb91c9c9a2483d40d9b23475864940db3f64f27887409ff7bb98676a0c685e0847a6064f068c4402e366fe222a
|
7
|
+
data.tar.gz: 044f52924d4c36026360db12f36585517267ddeb3149a0858a8204c906966252144c6a9c4360aa6a03aba5aabf41ee0710e021bc026c927bac1a0b1045714b71
|
data/.gitignore
CHANGED
data/CHANGELOG
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
== Unreleased
|
2
|
+
|
3
|
+
== 2.2.7 (2016-12-21)
|
4
|
+
* Fix 'base 17' typo, correct id bucketing (#308, @bazzargh)
|
5
|
+
* Update docs about `rebalance_frequency` (#310, @sdhull)
|
6
|
+
* Resolve Rails 5 deprecation warning for Mime::HTML (#313, @terracatta)
|
7
|
+
* Update MockAdapter and testing using it (#316, #309 @urbanautomaton)
|
8
|
+
* Deprecate calling #ab_seen with alternative instance (#317, @urbanautomaton)
|
9
|
+
|
1
10
|
== 2.2.6 (2016-10-04)
|
2
11
|
* Fixes for `Vanity.ab_test()` vs. view helper `ab_test` (@phillbaker)
|
3
12
|
|
data/Gemfile.lock
CHANGED
data/doc/ab_testing.textile
CHANGED
@@ -162,9 +162,14 @@ ab_test "noodle_test" do
|
|
162
162
|
alternatives "spaghetti", "linguine"
|
163
163
|
metrics :signup
|
164
164
|
score_method :bayes_bandit_score
|
165
|
+
rebalance_frequency 100
|
165
166
|
end
|
166
167
|
</pre>
|
167
168
|
|
169
|
+
Note: Setting the score method to `bayes_bandit_score` won't adjust alternative probabilities (ie, you won't get the benefit of maximizing conversions) unless you also set `rebalance_frequency`, which controls how many impressions must pass before rebalancing alternative probabilities.
|
170
|
+
|
171
|
+
Also note that impressions count is stored in-memory and is therefore reset upon restart of your app.
|
172
|
+
|
168
173
|
h3(#test). A/B Testing and Code Testing
|
169
174
|
|
170
175
|
If you're presenting more than one alternative to visitors of your site, you'll want to test more than one alternative. Don't let A/B testing become A/broken.
|
@@ -142,6 +142,16 @@ module Vanity
|
|
142
142
|
fail "Not implemented"
|
143
143
|
end
|
144
144
|
|
145
|
+
private
|
146
|
+
|
147
|
+
def with_ab_seen_deprecation(experiment, identity, alternative)
|
148
|
+
if alternative.respond_to?(:id)
|
149
|
+
Vanity.configuration.logger.warn(%q{Deprecated: #ab_seen should be passed the alternative id, not an Alternative instance})
|
150
|
+
yield experiment, identity, alternative.id
|
151
|
+
else
|
152
|
+
yield experiment, identity, alternative
|
153
|
+
end
|
154
|
+
end
|
145
155
|
end
|
146
156
|
end
|
147
157
|
end
|
@@ -255,9 +255,11 @@ module Vanity
|
|
255
255
|
end
|
256
256
|
|
257
257
|
# Determines if a participant already has seen this alternative in this experiment.
|
258
|
-
def ab_seen(experiment, identity,
|
259
|
-
|
260
|
-
|
258
|
+
def ab_seen(experiment, identity, alternative_or_id)
|
259
|
+
with_ab_seen_deprecation(experiment, identity, alternative_or_id) do |expt, ident, alt_id|
|
260
|
+
participant = VanityParticipant.retrieve(expt, ident, false)
|
261
|
+
participant && participant.seen == alt_id
|
262
|
+
end
|
261
263
|
end
|
262
264
|
|
263
265
|
# Returns the participant's seen alternative in this experiment, if it exists
|
@@ -273,7 +275,7 @@ module Vanity
|
|
273
275
|
# previously recorded as participating in this experiment.
|
274
276
|
def ab_add_conversion(experiment, alternative, identity, count = 1, implicit = false)
|
275
277
|
participant = VanityParticipant.retrieve(experiment, identity, false)
|
276
|
-
VanityParticipant.retrieve(experiment, identity, implicit, :converted => alternative)
|
278
|
+
VanityParticipant.retrieve(experiment, identity, implicit, :converted => alternative, :seen => alternative)
|
277
279
|
VanityExperiment.retrieve(experiment).increment_conversion(alternative, count)
|
278
280
|
end
|
279
281
|
|
@@ -111,9 +111,7 @@ module Vanity
|
|
111
111
|
end
|
112
112
|
|
113
113
|
def ab_counts(experiment, alternative)
|
114
|
-
|
115
|
-
@experiments[experiment][:alternatives] ||= {}
|
116
|
-
alt = @experiments[experiment][:alternatives][alternative] ||= {}
|
114
|
+
alt = alternative(experiment, alternative)
|
117
115
|
{ :participants => alt[:participants] ? alt[:participants].size : 0,
|
118
116
|
:converted => alt[:converted] ? alt[:converted].size : 0,
|
119
117
|
:conversions => alt[:conversions] || 0 }
|
@@ -134,17 +132,29 @@ module Vanity
|
|
134
132
|
end
|
135
133
|
|
136
134
|
def ab_add_participant(experiment, alternative, identity)
|
137
|
-
|
138
|
-
@experiments[experiment][:alternatives] ||= {}
|
139
|
-
alt = @experiments[experiment][:alternatives][alternative] ||= {}
|
135
|
+
alt = alternative(experiment, alternative)
|
140
136
|
alt[:participants] ||= Set.new
|
141
137
|
alt[:participants] << identity
|
142
138
|
end
|
143
139
|
|
140
|
+
def ab_seen(experiment, identity, alternative_or_id)
|
141
|
+
with_ab_seen_deprecation(experiment, identity, alternative_or_id) do |expt, ident, alt_id|
|
142
|
+
if ab_assigned(expt, ident) == alt_id
|
143
|
+
alt_id
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def ab_assigned(experiment, identity)
|
149
|
+
alternatives_for(experiment).each do |alt_id, alt_state|
|
150
|
+
return alt_id if alt_state[:participants].include?(identity)
|
151
|
+
end
|
152
|
+
|
153
|
+
nil
|
154
|
+
end
|
155
|
+
|
144
156
|
def ab_add_conversion(experiment, alternative, identity, count = 1, implicit = false)
|
145
|
-
|
146
|
-
@experiments[experiment][:alternatives] ||= {}
|
147
|
-
alt = @experiments[experiment][:alternatives][alternative] ||= {}
|
157
|
+
alt = alternative(experiment, alternative)
|
148
158
|
alt[:participants] ||= Set.new
|
149
159
|
alt[:converted] ||= Set.new
|
150
160
|
alt[:conversions] ||= 0
|
@@ -170,6 +180,20 @@ module Vanity
|
|
170
180
|
def destroy_experiment(experiment)
|
171
181
|
@experiments.delete experiment
|
172
182
|
end
|
183
|
+
|
184
|
+
private
|
185
|
+
|
186
|
+
def alternative(experiment, alternative)
|
187
|
+
alternatives_for(experiment)[alternative] ||= {}
|
188
|
+
alternatives_for(experiment)[alternative]
|
189
|
+
end
|
190
|
+
|
191
|
+
def alternatives_for(experiment)
|
192
|
+
@experiments[experiment] ||= {}
|
193
|
+
@experiments[experiment][:alternatives] ||= {}
|
194
|
+
|
195
|
+
@experiments[experiment][:alternatives]
|
196
|
+
end
|
173
197
|
end
|
174
198
|
end
|
175
199
|
end
|
@@ -200,9 +200,11 @@ module Vanity
|
|
200
200
|
end
|
201
201
|
|
202
202
|
# Determines if a participant already has seen this alternative in this experiment.
|
203
|
-
def ab_seen(experiment, identity,
|
204
|
-
|
205
|
-
|
203
|
+
def ab_seen(experiment, identity, alternative_or_id)
|
204
|
+
with_ab_seen_deprecation(experiment, identity, alternative_or_id) do |expt, ident, alt_id|
|
205
|
+
participant = @participants.find(:experiment=>expt, :identity=>ident).limit(1).projection(:seen=>1).first
|
206
|
+
participant && participant["seen"].first == alt_id
|
207
|
+
end
|
206
208
|
end
|
207
209
|
|
208
210
|
# Returns the participant's seen alternative in this experiment, if it exists
|
@@ -87,7 +87,7 @@ module Vanity
|
|
87
87
|
|
88
88
|
def metric_values(metric, from, to)
|
89
89
|
single = @metrics.mget(*(from.to_date..to.to_date).map { |date| "#{metric}:#{date}:value:0" }) || []
|
90
|
-
single.map { |v| [v] }
|
90
|
+
single.map { |v| [v.to_i] }
|
91
91
|
end
|
92
92
|
|
93
93
|
def destroy_metric(metric)
|
@@ -175,12 +175,14 @@ module Vanity
|
|
175
175
|
end
|
176
176
|
end
|
177
177
|
|
178
|
-
def ab_seen(experiment, identity,
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
178
|
+
def ab_seen(experiment, identity, alternative_or_id)
|
179
|
+
with_ab_seen_deprecation(experiment, identity, alternative_or_id) do |expt, ident, alt_id|
|
180
|
+
call_redis_with_failover(expt, ident, alt_id) do
|
181
|
+
if @experiments.sismember "#{expt}:alts:#{alt_id}:participants", ident
|
182
|
+
alt_id
|
183
|
+
else
|
184
|
+
nil
|
185
|
+
end
|
184
186
|
end
|
185
187
|
end
|
186
188
|
end
|
data/lib/vanity/connection.rb
CHANGED
@@ -31,7 +31,7 @@ module Vanity
|
|
31
31
|
# )
|
32
32
|
# @since 2.0.0
|
33
33
|
def initialize(specification=nil)
|
34
|
-
@specification = specification || DEFAULT_SPECIFICATION
|
34
|
+
@specification = Specification.new(specification || DEFAULT_SPECIFICATION).to_h
|
35
35
|
|
36
36
|
if Autoconnect.playground_should_autoconnect?
|
37
37
|
@adapter = setup_connection(@specification)
|
@@ -54,47 +54,59 @@ module Vanity
|
|
54
54
|
|
55
55
|
private
|
56
56
|
|
57
|
-
def setup_connection(
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
57
|
+
def setup_connection(specification)
|
58
|
+
establish_connection(specification)
|
59
|
+
end
|
60
|
+
|
61
|
+
def establish_connection(spec)
|
62
|
+
Adapters.establish_connection(spec)
|
63
|
+
end
|
64
|
+
|
65
|
+
class Specification
|
66
|
+
def initialize(spec)
|
67
|
+
case spec
|
68
|
+
when String
|
69
|
+
@spec = build_specification_hash_from_url(spec)
|
70
|
+
when Hash
|
71
|
+
if spec[:redis]
|
72
|
+
@spec = {
|
73
|
+
adapter: :redis,
|
74
|
+
redis: spec[:redis]
|
75
|
+
}
|
76
|
+
else
|
77
|
+
@spec = spec
|
78
|
+
end
|
69
79
|
else
|
70
|
-
|
80
|
+
raise InvalidSpecification.new("Unsupported connection specification: #{spec.inspect}")
|
71
81
|
end
|
72
|
-
|
73
|
-
|
82
|
+
|
83
|
+
validate_specification_hash(@spec)
|
74
84
|
end
|
75
|
-
end
|
76
85
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
{
|
81
|
-
adapter: uri.scheme,
|
82
|
-
username: uri.user,
|
83
|
-
password: uri.password,
|
84
|
-
host: uri.host,
|
85
|
-
port: uri.port,
|
86
|
-
path: uri.path,
|
87
|
-
params: params
|
88
|
-
}
|
89
|
-
end
|
86
|
+
def to_h
|
87
|
+
@spec
|
88
|
+
end
|
90
89
|
|
91
|
-
|
92
|
-
all_symbol_keys = spec.keys.all? { |key| key.is_a?(Symbol) }
|
93
|
-
raise InvalidSpecification unless all_symbol_keys
|
94
|
-
end
|
90
|
+
private
|
95
91
|
|
96
|
-
|
97
|
-
|
92
|
+
def validate_specification_hash(spec)
|
93
|
+
all_symbol_keys = spec.keys.all? { |key| key.is_a?(Symbol) }
|
94
|
+
raise InvalidSpecification unless all_symbol_keys
|
95
|
+
end
|
96
|
+
|
97
|
+
def build_specification_hash_from_url(connection_url)
|
98
|
+
uri = URI.parse(connection_url)
|
99
|
+
params = CGI.parse(uri.query) if uri.query
|
100
|
+
{
|
101
|
+
adapter: uri.scheme,
|
102
|
+
username: uri.user,
|
103
|
+
password: uri.password,
|
104
|
+
host: uri.host,
|
105
|
+
port: uri.port,
|
106
|
+
path: uri.path,
|
107
|
+
params: params
|
108
|
+
}
|
109
|
+
end
|
98
110
|
end
|
99
111
|
end
|
100
|
-
end
|
112
|
+
end
|
@@ -610,7 +610,7 @@ module Vanity
|
|
610
610
|
end
|
611
611
|
end
|
612
612
|
|
613
|
-
Digest::MD5.hexdigest("#{name}/#{identity}").to_i(
|
613
|
+
Digest::MD5.hexdigest("#{name}/#{identity}").to_i(16) % @alternatives.size
|
614
614
|
end
|
615
615
|
|
616
616
|
# Saves the assignment of an alternative to a person and performs the
|
@@ -635,7 +635,7 @@ module Vanity
|
|
635
635
|
# if we have an on_assignment block, call it on new assignments
|
636
636
|
if defined?(@on_assignment_block) && @on_assignment_block
|
637
637
|
assignment = alternatives[index]
|
638
|
-
if !connection.ab_seen @id, identity,
|
638
|
+
if !connection.ab_seen @id, identity, index
|
639
639
|
@on_assignment_block.call(Vanity.context, identity, assignment, self)
|
640
640
|
end
|
641
641
|
end
|
@@ -348,7 +348,7 @@ module Vanity
|
|
348
348
|
# Step 3: Open your browser to http://localhost:3000/vanity
|
349
349
|
module Dashboard
|
350
350
|
def index
|
351
|
-
render :file=>Vanity.template("_report"),:content_type=>Mime
|
351
|
+
render :file=>Vanity.template("_report"),:content_type=>Mime[:html], :locals=>{
|
352
352
|
:experiments=>Vanity.playground.experiments,
|
353
353
|
:experiments_persisted=>Vanity.playground.experiments_persisted?,
|
354
354
|
:metrics=>Vanity.playground.metrics
|
@@ -356,7 +356,7 @@ module Vanity
|
|
356
356
|
end
|
357
357
|
|
358
358
|
def participant
|
359
|
-
render :file=>Vanity.template("_participant"), :locals=>{:participant_id => params[:id], :participant_info => Vanity.playground.participant_info(params[:id])}, :content_type=>Mime
|
359
|
+
render :file=>Vanity.template("_participant"), :locals=>{:participant_id => params[:id], :participant_info => Vanity.playground.participant_info(params[:id])}, :content_type=>Mime[:html]
|
360
360
|
end
|
361
361
|
|
362
362
|
def complete
|
data/lib/vanity/version.rb
CHANGED
@@ -0,0 +1,19 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
require 'vanity/adapters/active_record_adapter'
|
3
|
+
require 'adapters/shared_tests'
|
4
|
+
|
5
|
+
describe Vanity::Adapters::ActiveRecordAdapter do
|
6
|
+
|
7
|
+
def specification
|
8
|
+
Vanity::Connection::Specification.new(VanityTestHelpers::DATABASE_OPTIONS["active_record"])
|
9
|
+
end
|
10
|
+
|
11
|
+
def adapter
|
12
|
+
Vanity::Adapters::ActiveRecordAdapter.new(specification.to_h)
|
13
|
+
end
|
14
|
+
|
15
|
+
if ENV["DB"] == "active_record"
|
16
|
+
include Vanity::Adapters::SharedTests
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'adapters/shared_tests'
|
3
|
+
|
4
|
+
describe Vanity::Adapters::MockAdapter do
|
5
|
+
|
6
|
+
def specification
|
7
|
+
Vanity::Connection::Specification.new(VanityTestHelpers::DATABASE_OPTIONS["mock"])
|
8
|
+
end
|
9
|
+
|
10
|
+
def adapter
|
11
|
+
Vanity::Adapters::MockAdapter.new({})
|
12
|
+
end
|
13
|
+
|
14
|
+
include Vanity::Adapters::SharedTests
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
require 'vanity/adapters/mongodb_adapter'
|
3
|
+
require 'adapters/shared_tests'
|
4
|
+
|
5
|
+
describe Vanity::Adapters::MongodbAdapter do
|
6
|
+
|
7
|
+
def specification
|
8
|
+
Vanity::Connection::Specification.new(VanityTestHelpers::DATABASE_OPTIONS["mongodb"])
|
9
|
+
end
|
10
|
+
|
11
|
+
def adapter
|
12
|
+
Vanity::Adapters::MongodbAdapter.new(specification.to_h)
|
13
|
+
end
|
14
|
+
|
15
|
+
if ENV["DB"] == "mongodb"
|
16
|
+
include Vanity::Adapters::SharedTests
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -1,4 +1,6 @@
|
|
1
1
|
require "test_helper"
|
2
|
+
require 'vanity/adapters/redis_adapter'
|
3
|
+
require 'adapters/shared_tests'
|
2
4
|
|
3
5
|
describe Vanity::Adapters::RedisAdapter do
|
4
6
|
before do
|
@@ -6,6 +8,16 @@ describe Vanity::Adapters::RedisAdapter do
|
|
6
8
|
require "redis/namespace"
|
7
9
|
end
|
8
10
|
|
11
|
+
def specification
|
12
|
+
Vanity::Connection::Specification.new(VanityTestHelpers::DATABASE_OPTIONS["redis"])
|
13
|
+
end
|
14
|
+
|
15
|
+
def adapter
|
16
|
+
Vanity::Adapters::RedisAdapter.new(specification.to_h)
|
17
|
+
end
|
18
|
+
|
19
|
+
include Vanity::Adapters::SharedTests
|
20
|
+
|
9
21
|
it "warns on disconnect error" do
|
10
22
|
if defined?(Redis)
|
11
23
|
assert_silent do
|
@@ -0,0 +1,336 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Vanity::Adapters::SharedTests
|
4
|
+
|
5
|
+
DummyAlternative = Struct.new(:id)
|
6
|
+
|
7
|
+
def identity
|
8
|
+
'test-identity'
|
9
|
+
end
|
10
|
+
|
11
|
+
def experiment
|
12
|
+
:experiment
|
13
|
+
end
|
14
|
+
|
15
|
+
def alternative
|
16
|
+
1
|
17
|
+
end
|
18
|
+
|
19
|
+
def metric_name
|
20
|
+
:purchases
|
21
|
+
end
|
22
|
+
|
23
|
+
def run_experiment
|
24
|
+
@subject.ab_add_participant(experiment, alternative, identity)
|
25
|
+
@subject.ab_add_participant(experiment, alternative, 'other-identity')
|
26
|
+
@subject.ab_add_conversion(experiment, alternative, identity, 2)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.included(base)
|
30
|
+
base.instance_eval do
|
31
|
+
before do
|
32
|
+
@subject = adapter
|
33
|
+
@subject.flushdb
|
34
|
+
|
35
|
+
metric "purchases"
|
36
|
+
|
37
|
+
new_ab_test "experiment" do
|
38
|
+
alternatives :control, :test
|
39
|
+
default :control
|
40
|
+
metrics :purchases
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe 'metrics methods' do
|
45
|
+
describe '#get_metric_last_update_at' do
|
46
|
+
it 'is nil if the metric has never been tracked' do
|
47
|
+
refute(@subject.get_metric_last_update_at(metric_name))
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'returns the time of the last tracked event if present' do
|
51
|
+
time = Time.now
|
52
|
+
@subject.metric_track(metric_name, time, identity, [1])
|
53
|
+
|
54
|
+
assert_in_delta(
|
55
|
+
time,
|
56
|
+
@subject.get_metric_last_update_at(metric_name),
|
57
|
+
1.0
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe '#metric_values' do
|
63
|
+
it 'returns the tracked metrics from the given range, binned by date' do
|
64
|
+
time_1 = Time.iso8601("2016-01-01T00:00:00+00:00")
|
65
|
+
time_2 = Time.iso8601("2016-01-01T12:00:00+00:00")
|
66
|
+
time_3 = Time.iso8601("2016-01-02T00:00:00+00:00")
|
67
|
+
time_4 = Time.iso8601("2016-01-02T12:00:00+00:00")
|
68
|
+
time_5 = Time.iso8601("2016-01-03T12:00:00+00:00")
|
69
|
+
|
70
|
+
@subject.metric_track(metric_name, time_1, identity, [1])
|
71
|
+
@subject.metric_track(metric_name, time_2, identity, [2])
|
72
|
+
@subject.metric_track(metric_name, time_3, identity, [4])
|
73
|
+
@subject.metric_track(metric_name, time_4, identity, [8])
|
74
|
+
@subject.metric_track(metric_name, time_5, identity, [16])
|
75
|
+
|
76
|
+
assert_equal(
|
77
|
+
[[3], [12]],
|
78
|
+
@subject.metric_values(metric_name, time_1, time_4)
|
79
|
+
)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe '#destroy_metric' do
|
84
|
+
it 'removes all data related to the metric' do
|
85
|
+
@subject.metric_track(metric_name, Time.now, identity, [1])
|
86
|
+
|
87
|
+
@subject.destroy_metric(metric_name)
|
88
|
+
|
89
|
+
refute(@subject.get_metric_last_update_at(metric_name))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe 'generic experiment methods' do
|
95
|
+
describe '#experiment_persisted?' do
|
96
|
+
it 'returns false if the experiment is unknown' do
|
97
|
+
refute(@subject.experiment_persisted?("other_experiment"))
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'returns true if the experiment has been created' do
|
101
|
+
@subject.set_experiment_created_at("other_experiment", Time.now)
|
102
|
+
|
103
|
+
assert(@subject.experiment_persisted?("other_experiment"))
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe '#set_experiment_created_at' do
|
108
|
+
it 'sets the experiment creation date' do
|
109
|
+
time = Time.now
|
110
|
+
@subject.set_experiment_created_at(experiment, time)
|
111
|
+
|
112
|
+
assert_in_delta(
|
113
|
+
time,
|
114
|
+
@subject.get_experiment_created_at(experiment),
|
115
|
+
1.0
|
116
|
+
)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
describe '#destroy_experiment' do
|
121
|
+
it 'removes all information about the experiment' do
|
122
|
+
run_experiment
|
123
|
+
@subject.destroy_experiment(experiment)
|
124
|
+
|
125
|
+
refute(@subject.experiment_persisted?(experiment))
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe '#is_experiment_enabled?' do
|
130
|
+
def with_experiments_start_enabled(enabled)
|
131
|
+
begin
|
132
|
+
original_value = Vanity.configuration.experiments_start_enabled
|
133
|
+
Vanity.configuration.experiments_start_enabled = enabled
|
134
|
+
yield
|
135
|
+
ensure
|
136
|
+
Vanity.configuration.experiments_start_enabled = original_value
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
describe 'when experiments start enabled' do
|
141
|
+
it 'is true when the enabled value is unset' do
|
142
|
+
with_experiments_start_enabled(true) do
|
143
|
+
assert(@subject.is_experiment_enabled?("other_experiment"))
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'is false when the enabled value is set to false' do
|
148
|
+
with_experiments_start_enabled(true) do
|
149
|
+
@subject.set_experiment_enabled("other_experiment", false)
|
150
|
+
|
151
|
+
refute(@subject.is_experiment_enabled?("other_experiment"))
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
describe 'when experiments do not start enabled' do
|
157
|
+
it 'is false when the enabled value is unset' do
|
158
|
+
with_experiments_start_enabled(false) do
|
159
|
+
refute(@subject.is_experiment_enabled?("other_experiment"))
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'is true when the enabled value is set to true' do
|
164
|
+
with_experiments_start_enabled(false) do
|
165
|
+
@subject.set_experiment_enabled("other_experiment", true)
|
166
|
+
|
167
|
+
assert(@subject.is_experiment_enabled?("other_experiment"))
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe '#is_experiment_completed?' do
|
174
|
+
it 'is true if the completion date is set' do
|
175
|
+
@subject.set_experiment_completed_at(experiment, Time.now)
|
176
|
+
|
177
|
+
assert(@subject.is_experiment_completed?(experiment))
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'is false if the completion date is unset' do
|
181
|
+
refute(@subject.is_experiment_completed?(experiment))
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
describe 'A/B test methods' do
|
187
|
+
describe '#ab_add_conversion' do
|
188
|
+
it 'adds the participant and the conversion when implicit=true' do
|
189
|
+
@subject.ab_add_conversion(experiment, alternative, identity, 1, true)
|
190
|
+
|
191
|
+
assert_equal(
|
192
|
+
{:participants => 1, :conversions => 1, :converted => 1},
|
193
|
+
@subject.ab_counts(experiment, alternative)
|
194
|
+
)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
describe '#ab_add_participant' do
|
199
|
+
it 'adds the participant to the specified alternative' do
|
200
|
+
@subject.ab_add_participant(experiment, alternative, identity)
|
201
|
+
|
202
|
+
assert_equal(
|
203
|
+
{:participants => 1, :conversions => 0, :converted => 0},
|
204
|
+
@subject.ab_counts(experiment, alternative)
|
205
|
+
)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
describe '#ab_seen' do
|
210
|
+
describe 'called with an Alternative instance' do
|
211
|
+
def capture_logs
|
212
|
+
require 'stringio'
|
213
|
+
original_logger = Vanity.configuration.logger
|
214
|
+
log_output = StringIO.new
|
215
|
+
Vanity.configuration.logger = Logger.new(log_output)
|
216
|
+
yield
|
217
|
+
log_output.string
|
218
|
+
ensure
|
219
|
+
Vanity.configuration.logger = original_logger
|
220
|
+
end
|
221
|
+
|
222
|
+
before do
|
223
|
+
@alternative_instance = DummyAlternative.new(alternative)
|
224
|
+
end
|
225
|
+
|
226
|
+
it 'emits a deprecation warning' do
|
227
|
+
@subject.ab_add_participant(experiment, alternative, identity)
|
228
|
+
|
229
|
+
out = capture_logs do
|
230
|
+
@subject.ab_seen(experiment, identity, @alternative_instance)
|
231
|
+
end
|
232
|
+
|
233
|
+
assert_match(/Deprecated/, out)
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'returns a truthy value if the identity is assigned to the alternative' do
|
237
|
+
@subject.ab_add_participant(experiment, alternative, identity)
|
238
|
+
|
239
|
+
assert(@subject.ab_seen(experiment, identity, @alternative_instance))
|
240
|
+
end
|
241
|
+
|
242
|
+
it 'returns a falsey value if the identity is not assigned to the alternative' do
|
243
|
+
@subject.ab_add_participant(experiment, alternative, identity)
|
244
|
+
|
245
|
+
refute(@subject.ab_seen(experiment, identity, DummyAlternative.new(2)))
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
describe 'called with an alternative id' do
|
250
|
+
it 'returns a truthy value if the identity is assigned to the alternative' do
|
251
|
+
@subject.ab_add_participant(experiment, alternative, identity)
|
252
|
+
|
253
|
+
assert(@subject.ab_seen(experiment, identity, alternative))
|
254
|
+
end
|
255
|
+
|
256
|
+
it 'returns a falsey value if the identity is not assigned to the alternative' do
|
257
|
+
@subject.ab_add_participant(experiment, alternative, identity)
|
258
|
+
|
259
|
+
refute(@subject.ab_seen(experiment, identity, 2))
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
describe '#ab_assigned' do
|
265
|
+
it 'returns the assigned alternative if present' do
|
266
|
+
@subject.ab_add_participant(experiment, alternative, identity)
|
267
|
+
|
268
|
+
assert_equal(
|
269
|
+
alternative,
|
270
|
+
@subject.ab_assigned(experiment, identity)
|
271
|
+
)
|
272
|
+
end
|
273
|
+
|
274
|
+
it 'returns nil if the identity has no assignment' do
|
275
|
+
assert_equal(
|
276
|
+
nil,
|
277
|
+
@subject.ab_assigned(experiment, identity)
|
278
|
+
)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
describe '#ab_counts' do
|
283
|
+
it 'returns the counts of participants, conversions and converted for the alternative' do
|
284
|
+
run_experiment
|
285
|
+
|
286
|
+
assert_equal(
|
287
|
+
{:participants => 2, :conversions => 2, :converted => 1},
|
288
|
+
@subject.ab_counts(experiment, alternative)
|
289
|
+
)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
describe '#ab_get_outcome' do
|
294
|
+
it 'returns the outcome if one is set' do
|
295
|
+
@subject.ab_set_outcome(experiment, alternative)
|
296
|
+
|
297
|
+
assert_equal(
|
298
|
+
alternative,
|
299
|
+
@subject.ab_get_outcome(experiment)
|
300
|
+
)
|
301
|
+
end
|
302
|
+
|
303
|
+
it 'returns nil otherwise' do
|
304
|
+
assert_equal(
|
305
|
+
nil,
|
306
|
+
@subject.ab_get_outcome(experiment)
|
307
|
+
)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
describe '#ab_show' do
|
312
|
+
it 'forces an alternative to be shown to the given identity' do
|
313
|
+
@subject.ab_show(experiment, identity, alternative)
|
314
|
+
|
315
|
+
assert_equal(
|
316
|
+
alternative,
|
317
|
+
@subject.ab_showing(experiment, identity)
|
318
|
+
)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
describe '#ab_not_showing' do
|
323
|
+
it 'cancels a previously-set showing alternative' do
|
324
|
+
@subject.ab_show(experiment, identity, alternative)
|
325
|
+
@subject.ab_not_showing(experiment, identity)
|
326
|
+
|
327
|
+
assert_equal(
|
328
|
+
nil,
|
329
|
+
@subject.ab_showing(experiment, identity)
|
330
|
+
)
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -43,12 +43,14 @@ module VanityTestHelpers
|
|
43
43
|
# We go destructive on the database at the end of each run, so make sure we
|
44
44
|
# don't use databases you care about. For Redis, we pick database 15
|
45
45
|
# (default is 0).
|
46
|
-
|
46
|
+
DATABASE_OPTIONS = {
|
47
47
|
"redis" => "redis://localhost/15",
|
48
48
|
"mongodb" => "mongodb://localhost/vanity",
|
49
49
|
"active_record" => { :adapter => "active_record", :active_record_adapter =>"default" },
|
50
50
|
"mock" => "mock:/"
|
51
|
-
}
|
51
|
+
}
|
52
|
+
|
53
|
+
DATABASE = DATABASE_OPTIONS[ENV["DB"]] or raise "No support yet for #{ENV["DB"]}"
|
52
54
|
|
53
55
|
TEST_DATA_FILES = Dir[File.expand_path("../data/*", __FILE__)]
|
54
56
|
VANITY_CONFIGS = TEST_DATA_FILES.each.with_object({}) do |path, hash|
|
data/test/vanity_test.rb
CHANGED
@@ -108,12 +108,16 @@ describe Vanity do
|
|
108
108
|
f.write(connection_config)
|
109
109
|
end
|
110
110
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
111
|
+
out, _err = capture_io do
|
112
|
+
Vanity.reset!
|
113
|
+
Vanity.configuration.logger = Logger.new($stdout)
|
114
|
+
Vanity.disconnect!
|
115
|
+
Vanity::Connection.stubs(:new)
|
116
|
+
Vanity.connect!
|
117
|
+
end
|
115
118
|
|
116
119
|
assert_equal false, Vanity.configuration.collecting
|
120
|
+
assert_match(/Deprecated/, out)
|
117
121
|
end
|
118
122
|
end
|
119
123
|
end
|
@@ -178,4 +182,4 @@ describe Vanity do
|
|
178
182
|
refute_same original_configuration, Vanity.reload!
|
179
183
|
end
|
180
184
|
end
|
181
|
-
end
|
185
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: vanity
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.2.
|
4
|
+
version: 2.2.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Assaf Arkin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-12-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: i18n
|
@@ -224,7 +224,11 @@ files:
|
|
224
224
|
- lib/vanity/templates/vanity.js
|
225
225
|
- lib/vanity/vanity.rb
|
226
226
|
- lib/vanity/version.rb
|
227
|
+
- test/adapters/active_record_adapter_test.rb
|
228
|
+
- test/adapters/mock_adapter_test.rb
|
229
|
+
- test/adapters/mongodb_adapter_test.rb
|
227
230
|
- test/adapters/redis_adapter_test.rb
|
231
|
+
- test/adapters/shared_tests.rb
|
228
232
|
- test/autoconnect_test.rb
|
229
233
|
- test/cli_test.rb
|
230
234
|
- test/commands/report_test.rb
|
@@ -286,7 +290,7 @@ metadata: {}
|
|
286
290
|
post_install_message: To get started run vanity --help
|
287
291
|
rdoc_options:
|
288
292
|
- "--title"
|
289
|
-
- Vanity 2.2.
|
293
|
+
- Vanity 2.2.7
|
290
294
|
- "--main"
|
291
295
|
- README.md
|
292
296
|
- "--webcvs"
|
@@ -310,7 +314,11 @@ signing_key:
|
|
310
314
|
specification_version: 4
|
311
315
|
summary: Experience Driven Development framework for Ruby
|
312
316
|
test_files:
|
317
|
+
- test/adapters/active_record_adapter_test.rb
|
318
|
+
- test/adapters/mock_adapter_test.rb
|
319
|
+
- test/adapters/mongodb_adapter_test.rb
|
313
320
|
- test/adapters/redis_adapter_test.rb
|
321
|
+
- test/adapters/shared_tests.rb
|
314
322
|
- test/autoconnect_test.rb
|
315
323
|
- test/cli_test.rb
|
316
324
|
- test/commands/report_test.rb
|