vanity 1.4.0 → 1.5.0.beta

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.
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