vanity 1.4.0 → 1.5.0.beta

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,22 @@
1
+ == 1.5.0 (2010-10-12)
2
+
3
+ Note: MongoDB URI scheme changed from mongo: to mongodb: to be consistent with
4
+ the rest of the civilized world. If you get an error because Vanity can't find
5
+ the MongoDB adapter, fear not! It's still there, just need to update your
6
+ config/vanity.yml file.
7
+
8
+
9
+ Rails 3 support (Adam Keys, Stephen Celis, Brian Leonard, Ryan Carver)
10
+
11
+ Changed MongoDB URI scheme from 'mongo://' to the standard 'mongodb://' and
12
+ renamed the mongodb adapter filename to make it match (JS Boulanger)
13
+
14
+ Added support for ERB in YAML configuration files (JS Boulanger)
15
+
16
+ Fixed initialization of playground's Rails defaults such that they can be
17
+ overriden in the environment (JS Boulanger)
18
+
19
+
1
20
  == 1.4.0 (2010-08-06)
2
21
 
3
22
  Note: Run this command to upgrade your database to 1.4, or you will not have
data/Gemfile CHANGED
@@ -1,6 +1,5 @@
1
1
  source "http://rubygems.org/"
2
- gem "redis", "~>2.0"
3
- gem "redis-namespace", "~>0.7"
2
+ gemspec
4
3
 
5
4
  group :development do
6
5
  gem "jekyll"
@@ -20,5 +19,6 @@ group :test do
20
19
  gem "shoulda"
21
20
  gem "sqlite3-ruby", "1.2.5" # 1.3.0 doesn't like Ruby 1.9.1
22
21
  gem "timecop"
22
+ #gem "SystemTimer"
23
23
  gem "webmock"
24
24
  end
data/README.rdoc CHANGED
@@ -55,33 +55,14 @@ And:
55
55
  vanity report --output vanity.html
56
56
 
57
57
 
58
- == Building From Source
59
-
60
- To run the test suite for the first time:
61
-
62
- $ rake test:setup test
63
-
64
- Vanity is tested against multiple Ruby implementations. To run the full test
65
- suite against all Ruby implementations and bundled adapters:
66
-
67
- $ rake test:rubies
68
- $ # only ruby 1.9.2
69
- $ rake test:rubies[1.9.2]
70
- $ # only redis
71
- $ rake test:adapters[redis]
72
-
73
- To build the documentation:
74
-
75
- $ rake docs
76
- $ open html/index.html
77
-
78
- To clean up after yourself:
79
-
80
- $ rake clobber
81
-
82
- To package Vanity as a gem and install on your machine:
83
-
84
- $ rake install
58
+ == Contributing
59
+
60
+ * Fork the project
61
+ * Please use a topic branch to make your changes, it's easier to test them that way
62
+ * Fix, patch, enhance, document, improve, sprinkle pixie dust
63
+ * At minimum run rake test, if possible, please run rake test:all
64
+ * Tests. Please. Run rake test, of if you can, rake test:all
65
+ * Send a pull request on GitHub
85
66
 
86
67
 
87
68
  == Credits/License
data/Rakefile CHANGED
@@ -2,7 +2,7 @@ require "rake/testtask"
2
2
 
3
3
  # -- Building stuff --
4
4
 
5
- spec = Gem::Specification.load(File.expand_path("vanity.gemspec", File.dirname(__FILE__)))
5
+ spec = Gem::Specification.load(Dir["*.gemspec"].first)
6
6
 
7
7
  desc "Build the Gem"
8
8
  task :build do
@@ -16,7 +16,7 @@ task :install=>:build do
16
16
  end
17
17
 
18
18
  desc "Push new release to gemcutter and git tag"
19
- task :push=>["test:rubies", "build"] do
19
+ task :push=>["test:all", "build"] do
20
20
  sh "git push"
21
21
  puts "Tagging version #{spec.version} .."
22
22
  sh "git tag v#{spec.version}"
@@ -28,8 +28,11 @@ end
28
28
 
29
29
  # -- Testing stuff --
30
30
 
31
+ desc "Test everything"
32
+ task "test:all"=>"test:rubies"
33
+
31
34
  # Ruby versions we're testing with.
32
- RUBIES = %w{1.8.7 1.9.1 1.9.2}
35
+ RUBIES = %w{1.8.7 1.9.2}
33
36
 
34
37
  # Use rake test:rubies to run all combination of tests (see test:adapters) using
35
38
  # all the versions of Ruby specified in RUBIES. Or to test a specific version of
data/lib/vanity.rb CHANGED
@@ -2,6 +2,8 @@ require "date"
2
2
  require "time"
3
3
  require "logger"
4
4
  require "cgi"
5
+ require "erb"
6
+ require "yaml"
5
7
 
6
8
  # All the cool stuff happens in other places.
7
9
  # @see Vanity::Helper
@@ -10,17 +12,9 @@ require "cgi"
10
12
  # @see Vanity::Metric
11
13
  # @see Vanity::Experiment
12
14
  module Vanity
13
- # Version number.
14
- module Version
15
- version = Gem::Specification.load(File.expand_path("../vanity.gemspec", File.dirname(__FILE__))).version.to_s.split(".").map { |i| i.to_i }
16
- MAJOR = version[0]
17
- MINOR = version[1]
18
- PATCH = version[2]
19
- STRING = "#{MAJOR}.#{MINOR}.#{PATCH}"
20
- end
21
-
22
15
  end
23
16
 
17
+ require "vanity/version"
24
18
  require "vanity/backport" if RUBY_VERSION < "1.9"
25
19
  # Metrics.
26
20
  require "vanity/metric/base"
@@ -33,7 +27,7 @@ require "vanity/experiment/ab_test"
33
27
  # Database adapters
34
28
  require "vanity/adapters/abstract_adapter"
35
29
  require "vanity/adapters/redis_adapter"
36
- require "vanity/adapters/mongo_adapter"
30
+ require "vanity/adapters/mongodb_adapter"
37
31
  require "vanity/adapters/mock_adapter"
38
32
  # Playground.
39
33
  require "vanity/playground"
@@ -0,0 +1,304 @@
1
+ module Vanity
2
+ module Adapters
3
+ class << self
4
+ # Creates new ActiveRecord connection and returns ActiveRecordAdapter.
5
+ def active_record_connection(spec)
6
+ require 'active_record'
7
+ ActiveRecordAdapter.new(spec)
8
+ end
9
+ end
10
+
11
+ # ActiveRecord adapter
12
+ class ActiveRecordAdapter < AbstractAdapter
13
+ # Base model, stores connection and defines schema
14
+ class VanityRecord < ActiveRecord::Base
15
+ def self.define_schema
16
+ # Create a schema table to store the schema version
17
+ unless connection.tables.include?('vanity_schema')
18
+ connection.create_table :vanity_schema do |t|
19
+ t.integer :version
20
+ end
21
+ end
22
+
23
+ # Migrate
24
+ unless VanitySchema.find_by_version(1)
25
+ connection.create_table :vanity_metrics do |t|
26
+ t.string :metric_id
27
+ t.datetime :updated_at
28
+ end
29
+ connection.add_index :vanity_metrics, [:metric_id]
30
+
31
+ connection.create_table :vanity_metric_values do |t|
32
+ t.integer :vanity_metric_id
33
+ t.integer :index
34
+ t.integer :value
35
+ t.string :date
36
+ end
37
+ connection.add_index :vanity_metric_values, [:vanity_metric_id]
38
+
39
+ connection.create_table :vanity_experiments do |t|
40
+ t.string :experiment_id
41
+ t.integer :outcome
42
+ t.datetime :created_at
43
+ t.datetime :completed_at
44
+ end
45
+ connection.add_index :vanity_experiments, [:experiment_id]
46
+
47
+ connection.create_table :vanity_conversions do |t|
48
+ t.integer :vanity_experiment_id
49
+ t.integer :alternative
50
+ t.integer :conversions
51
+ end
52
+ connection.add_index :vanity_conversions, [:vanity_experiment_id, :alternative]
53
+
54
+ connection.create_table :vanity_participants do |t|
55
+ t.string :experiment_id
56
+ t.string :identity
57
+ t.integer :shown
58
+ t.integer :seen
59
+ t.integer :converted
60
+ end
61
+ connection.add_index :vanity_participants, [:experiment_id]
62
+ connection.add_index :vanity_participants, [:experiment_id, :identity]
63
+ connection.add_index :vanity_participants, [:experiment_id, :shown]
64
+ connection.add_index :vanity_participants, [:experiment_id, :seen]
65
+ connection.add_index :vanity_participants, [:experiment_id, :converted]
66
+
67
+ VanitySchema.create(:version => 1)
68
+ end
69
+ end
70
+ end
71
+
72
+ # Schema model
73
+ class VanitySchema < VanityRecord
74
+ set_table_name :vanity_schema
75
+ end
76
+
77
+ # Metric model
78
+ class VanityMetric < VanityRecord
79
+ set_table_name :vanity_metrics
80
+ has_many :vanity_metric_values
81
+
82
+ def self.retrieve(metric)
83
+ find_or_create_by_metric_id(metric.to_s)
84
+ end
85
+ end
86
+
87
+ # Metric value
88
+ class VanityMetricValue < VanityRecord
89
+ set_table_name :vanity_metric_values
90
+ belongs_to :vanity_metric
91
+ end
92
+
93
+ # Experiment model
94
+ class VanityExperiment < VanityRecord
95
+ set_table_name :vanity_experiments
96
+ has_many :vanity_conversions, :dependent => :destroy
97
+
98
+ # Finds or creates the experiment
99
+ def self.retrieve(experiment)
100
+ find_or_create_by_experiment_id(experiment.to_s)
101
+ end
102
+
103
+ def increment_conversion(alternative, count = 1)
104
+ record = vanity_conversions.find_or_create_by_alternative(alternative)
105
+ record.increment!(:conversions, count)
106
+ end
107
+ end
108
+
109
+ # Conversion model
110
+ class VanityConversion < VanityRecord
111
+ set_table_name :vanity_conversions
112
+ belongs_to :vanity_experiment
113
+ end
114
+
115
+ # Participant model
116
+ class VanityParticipant < VanityRecord
117
+ set_table_name :vanity_participants
118
+
119
+ # Finds the participant by experiment and identity. If
120
+ # create is true then it will create the participant
121
+ # if not found. If a hash is passed then this will be
122
+ # passed to create if creating, or will be used to
123
+ # update the found participant.
124
+ def self.retrieve(experiment, identity, create = true, update_with = nil)
125
+ record = VanityParticipant.first(
126
+ :conditions =>
127
+ {:experiment_id => experiment.to_s, :identity => identity})
128
+
129
+ if record
130
+ record.update_attributes(update_with) if update_with
131
+ elsif create
132
+ record = VanityParticipant.create(
133
+ {:experiment_id => experiment.to_s,
134
+ :identity => identity}.merge(update_with || {}))
135
+ end
136
+
137
+ record
138
+ end
139
+ end
140
+
141
+ def initialize(options)
142
+ options[:adapter] = options[:active_record_adapter] if options[:active_record_adapter]
143
+
144
+ VanityRecord.establish_connection(options)
145
+ VanityRecord.define_schema
146
+ end
147
+
148
+ def active?
149
+ VanityRecord.connected?
150
+ end
151
+
152
+ def disconnect!
153
+ VanityRecord.connection.disconnect! if active?
154
+ end
155
+
156
+ def reconnect!
157
+ VanityRecord.connection.reconnect!
158
+ end
159
+
160
+ def flushdb
161
+ [VanityExperiment, VanityMetric, VanityParticipant, VanityMetricValue, VanityConversion].each do |klass|
162
+ klass.delete_all
163
+ end
164
+ end
165
+
166
+ def get_metric_last_update_at(metric)
167
+ record = VanityMetric.find_by_metric_id(metric.to_s)
168
+ record && record.updated_at
169
+ end
170
+
171
+ def metric_track(metric, timestamp, identity, values)
172
+ record = VanityMetric.retrieve(metric)
173
+
174
+ values.each_with_index do |value, index|
175
+ record.vanity_metric_values.create(:date => timestamp.to_date.to_s, :index => index, :value => value)
176
+ end
177
+
178
+ record.updated_at = Time.now
179
+ record.save
180
+ end
181
+
182
+ def metric_values(metric, from, to)
183
+ connection = VanityMetric.connection
184
+ record = VanityMetric.retrieve(metric)
185
+ dates = (from.to_date..to.to_date).map(&:to_s)
186
+ conditions = [connection.quote_column_name('date') + ' IN (?)', dates]
187
+ order = "#{connection.quote_column_name('date')}"
188
+ select = "sum(#{connection.quote_column_name('value')}) value, #{connection.quote_column_name('date')}"
189
+ group_by = "#{connection.quote_column_name('date')}"
190
+
191
+ values = record.vanity_metric_values.all(
192
+ :select => select,
193
+ :conditions => conditions,
194
+ :order => order,
195
+ :group => group_by
196
+ )
197
+
198
+ dates.map do |date|
199
+ value = values.detect{|v| v.date == date }
200
+ [(value && value.value) || 0]
201
+ end
202
+ end
203
+
204
+ def destroy_metric(metric)
205
+ record = VanityMetric.find_by_metric_id(metric.to_s)
206
+ record && record.destroy
207
+ end
208
+
209
+ # Store when experiment was created (do not write over existing value).
210
+ def set_experiment_created_at(experiment, time)
211
+ record = VanityExperiment.find_by_experiment_id(experiment.to_s) ||
212
+ VanityExperiment.new(:experiment_id => experiment.to_s)
213
+ record.created_at ||= time
214
+ record.save
215
+ end
216
+
217
+ # Return when experiment was created.
218
+ def get_experiment_created_at(experiment)
219
+ record = VanityExperiment.retrieve(experiment)
220
+ record && record.created_at
221
+ end
222
+
223
+ def set_experiment_completed_at(experiment, time)
224
+ VanityExperiment.retrieve(experiment).update_attribute(:completed_at, time)
225
+ end
226
+
227
+ def get_experiment_completed_at(experiment)
228
+ VanityExperiment.retrieve(experiment).completed_at
229
+ end
230
+
231
+ # Returns true if experiment completed.
232
+ def is_experiment_completed?(experiment)
233
+ !!VanityExperiment.retrieve(experiment).completed_at
234
+ end
235
+
236
+ # Returns counts for given A/B experiment and alternative (by index).
237
+ # Returns hash with values for the keys :participants, :converted and
238
+ # :conversions.
239
+ def ab_counts(experiment, alternative)
240
+ record = VanityExperiment.retrieve(experiment)
241
+ participants = VanityParticipant.count(:conditions => {:experiment_id => experiment.to_s, :seen => alternative})
242
+ converted = VanityParticipant.count(:conditions => {:experiment_id => experiment.to_s, :converted => alternative})
243
+ conversions = record.vanity_conversions.sum(:conversions, :conditions => {:alternative => alternative})
244
+
245
+ {
246
+ :participants => participants,
247
+ :converted => converted,
248
+ :conversions => conversions
249
+ }
250
+ end
251
+
252
+ # Pick particular alternative (by index) to show to this particular
253
+ # participant (by identity).
254
+ def ab_show(experiment, identity, alternative)
255
+ VanityParticipant.retrieve(experiment, identity, true, :shown => alternative)
256
+ end
257
+
258
+ # Indicates which alternative to show to this participant. See #ab_show.
259
+ def ab_showing(experiment, identity)
260
+ participant = VanityParticipant.retrieve(experiment, identity, false)
261
+ participant && participant.shown
262
+ end
263
+
264
+ # Cancels previously set association between identity and alternative. See
265
+ # #ab_show.
266
+ def ab_not_showing(experiment, identity)
267
+ VanityParticipant.retrieve(experiment, identity, true, :shown => nil)
268
+ end
269
+
270
+ # Records a participant in this experiment for the given alternative.
271
+ def ab_add_participant(experiment, alternative, identity)
272
+ VanityParticipant.retrieve(experiment, identity, true, :seen => alternative)
273
+ end
274
+
275
+ # Records a conversion in this experiment for the given alternative.
276
+ # Associates a value with the conversion (default to 1). If implicit is
277
+ # true, add particpant if not already recorded for this experiment. If
278
+ # implicit is false (default), only add conversion is participant
279
+ # previously recorded as participating in this experiment.
280
+ def ab_add_conversion(experiment, alternative, identity, count = 1, implicit = false)
281
+ VanityParticipant.retrieve(experiment, identity, implicit, :converted => alternative)
282
+ VanityExperiment.retrieve(experiment).increment_conversion(alternative, count)
283
+ end
284
+
285
+ # Returns the outcome of this experiment (if set), the index of a
286
+ # particular alternative.
287
+ def ab_get_outcome(experiment)
288
+ VanityExperiment.retrieve(experiment).outcome
289
+ end
290
+
291
+ # Sets the outcome of this experiment to a particular alternative.
292
+ def ab_set_outcome(experiment, alternative = 0)
293
+ VanityExperiment.retrieve(experiment).update_attribute(:outcome, alternative)
294
+ end
295
+
296
+ # Deletes all information about this experiment.
297
+ def destroy_experiment(experiment)
298
+ VanityParticipant.delete_all(:experiment_id => experiment.to_s)
299
+ record = VanityExperiment.find_by_experiment_id(experiment.to_s)
300
+ record && record.destroy
301
+ end
302
+ end
303
+ end
304
+ end