vanity 1.3.0 → 1.4.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.
Files changed (99) hide show
  1. data/CHANGELOG +61 -3
  2. data/Gemfile +22 -14
  3. data/README.rdoc +9 -4
  4. data/Rakefile +72 -12
  5. data/bin/vanity +16 -4
  6. data/lib/vanity.rb +7 -5
  7. data/lib/vanity/adapters/abstract_adapter.rb +135 -0
  8. data/lib/vanity/adapters/mock_adapter.rb +157 -0
  9. data/lib/vanity/adapters/mongo_adapter.rb +162 -0
  10. data/lib/vanity/adapters/redis_adapter.rb +154 -0
  11. data/lib/vanity/backport.rb +0 -17
  12. data/lib/vanity/commands/upgrade.rb +34 -0
  13. data/lib/vanity/experiment/ab_test.rb +46 -41
  14. data/lib/vanity/experiment/base.rb +13 -15
  15. data/lib/vanity/frameworks/rails.rb +5 -9
  16. data/lib/vanity/metric/active_record.rb +10 -4
  17. data/lib/vanity/metric/base.rb +46 -23
  18. data/lib/vanity/metric/google_analytics.rb +7 -0
  19. data/lib/vanity/metric/remote.rb +53 -0
  20. data/lib/vanity/playground.rb +133 -49
  21. data/test/{ab_test_test.rb → experiment/ab_test.rb} +47 -3
  22. data/test/{experiment_test.rb → experiment/base_test.rb} +8 -8
  23. data/test/metric/active_record_test.rb +253 -0
  24. data/test/metric/base_test.rb +293 -0
  25. data/test/metric/google_analytics_test.rb +104 -0
  26. data/test/metric/remote_test.rb +108 -0
  27. data/test/myapp/app/controllers/application_controller.rbc +66 -0
  28. data/test/myapp/app/controllers/main_controller.rb +3 -3
  29. data/test/myapp/app/controllers/main_controller.rbc +347 -0
  30. data/test/myapp/config/boot.rbc +2534 -0
  31. data/test/myapp/config/environment.rbc +403 -0
  32. data/test/myapp/config/routes.rbc +174 -0
  33. data/test/myapp/log/production.log +2601 -0
  34. data/test/passenger_test.rb +14 -5
  35. data/test/passenger_test.rbc +0 -0
  36. data/test/playground_test.rbc +256 -0
  37. data/test/rails_test.rb +75 -22
  38. data/test/rails_test.rbc +4086 -0
  39. data/test/test_helper.rb +30 -7
  40. data/test/test_helper.rbc +4297 -0
  41. data/vanity.gemspec +6 -2
  42. metadata +74 -73
  43. data/lib/vanity/commands.rb +0 -2
  44. data/lib/vanity/mock_redis.rb +0 -76
  45. data/test/metric_test.rb +0 -622
  46. data/vendor/cache/RedCloth-4.2.2.gem +0 -0
  47. data/vendor/cache/actionmailer-2.3.5.gem +0 -0
  48. data/vendor/cache/actionpack-2.3.5.gem +0 -0
  49. data/vendor/cache/activerecord-2.3.5.gem +0 -0
  50. data/vendor/cache/activeresource-2.3.5.gem +0 -0
  51. data/vendor/cache/activesupport-2.3.5.gem +0 -0
  52. data/vendor/cache/autotest-4.2.7.gem +0 -0
  53. data/vendor/cache/autotest-fsevent-0.2.1.gem +0 -0
  54. data/vendor/cache/autotest-growl-0.2.0.gem +0 -0
  55. data/vendor/cache/bundler-0.9.7.gem +0 -0
  56. data/vendor/cache/classifier-1.3.1.gem +0 -0
  57. data/vendor/cache/directory_watcher-1.3.1.gem +0 -0
  58. data/vendor/cache/fastthread-1.0.7.gem +0 -0
  59. data/vendor/cache/garb-0.7.0.gem +0 -0
  60. data/vendor/cache/happymapper-0.3.0.gem +0 -0
  61. data/vendor/cache/jekyll-0.5.7.gem +0 -0
  62. data/vendor/cache/libxml-ruby-1.1.3.gem +0 -0
  63. data/vendor/cache/liquid-2.0.0.gem +0 -0
  64. data/vendor/cache/maruku-0.6.0.gem +0 -0
  65. data/vendor/cache/mocha-0.9.8.gem +0 -0
  66. data/vendor/cache/open4-1.0.1.gem +0 -0
  67. data/vendor/cache/passenger-2.2.9.gem +0 -0
  68. data/vendor/cache/rack-1.0.1.gem +0 -0
  69. data/vendor/cache/rails-2.3.5.gem +0 -0
  70. data/vendor/cache/rake-0.8.7.gem +0 -0
  71. data/vendor/cache/rubygems-update-1.3.5.gem +0 -0
  72. data/vendor/cache/shoulda-2.10.3.gem +0 -0
  73. data/vendor/cache/sqlite3-ruby-1.2.5.gem +0 -0
  74. data/vendor/cache/stemmer-1.0.1.gem +0 -0
  75. data/vendor/cache/syntax-1.0.0.gem +0 -0
  76. data/vendor/cache/sys-uname-0.8.4.gem +0 -0
  77. data/vendor/cache/timecop-0.3.4.gem +0 -0
  78. data/vendor/redis-rb/LICENSE +0 -20
  79. data/vendor/redis-rb/README.markdown +0 -36
  80. data/vendor/redis-rb/Rakefile +0 -62
  81. data/vendor/redis-rb/bench.rb +0 -44
  82. data/vendor/redis-rb/benchmarking/suite.rb +0 -24
  83. data/vendor/redis-rb/benchmarking/worker.rb +0 -71
  84. data/vendor/redis-rb/bin/distredis +0 -33
  85. data/vendor/redis-rb/examples/basic.rb +0 -16
  86. data/vendor/redis-rb/examples/incr-decr.rb +0 -18
  87. data/vendor/redis-rb/examples/list.rb +0 -26
  88. data/vendor/redis-rb/examples/sets.rb +0 -36
  89. data/vendor/redis-rb/lib/dist_redis.rb +0 -124
  90. data/vendor/redis-rb/lib/hash_ring.rb +0 -128
  91. data/vendor/redis-rb/lib/pipeline.rb +0 -21
  92. data/vendor/redis-rb/lib/redis.rb +0 -370
  93. data/vendor/redis-rb/lib/redis/raketasks.rb +0 -1
  94. data/vendor/redis-rb/profile.rb +0 -22
  95. data/vendor/redis-rb/redis-rb.gemspec +0 -30
  96. data/vendor/redis-rb/spec/redis_spec.rb +0 -637
  97. data/vendor/redis-rb/spec/spec_helper.rb +0 -4
  98. data/vendor/redis-rb/speed.rb +0 -16
  99. data/vendor/redis-rb/tasks/redis.tasks.rb +0 -140
@@ -0,0 +1,157 @@
1
+ module Vanity
2
+ module Adapters
3
+ class << self
4
+ # Creates and returns new MockAdapter.
5
+ #
6
+ # @since 1.4.0
7
+ def mock_connection(spec)
8
+ MockAdapter.new(spec)
9
+ end
10
+ end
11
+
12
+ # Mock adapter. You can use this when running in test environment, staging,
13
+ # wherever you don't care for collecting metrics. Doesn't store anything.
14
+ #
15
+ # @since 1.4.0
16
+ class MockAdapter < AbstractAdapter
17
+ def initialize(options)
18
+ @metrics = @@metrics ||= {}
19
+ @experiments = @@experiments ||= {}
20
+ end
21
+
22
+ def active?
23
+ !!@metrics
24
+ end
25
+
26
+ def disconnect!
27
+ @metrics = nil
28
+ @experiments = nil
29
+ end
30
+
31
+ def reconnect!
32
+ @metrics = @@metrics
33
+ @experiments = @@experiments
34
+ end
35
+
36
+ def to_s
37
+ "mock:/"
38
+ end
39
+
40
+ def flushdb
41
+ @metrics.clear
42
+ @experiments.clear
43
+ end
44
+
45
+
46
+ # -- Metrics --
47
+
48
+ def get_metric_last_update_at(metric)
49
+ @metrics[metric] && @metrics[metric][:last_update_at]
50
+ end
51
+
52
+ def metric_track(metric, timestamp, identity, values)
53
+ @metrics[metric] ||= {}
54
+ current = @metrics[metric][timestamp.to_date] ||= []
55
+ values.each_with_index do |v,i|
56
+ current[i] = (current[i] || 0) + v || 0
57
+ end
58
+ @metrics[metric][:last_update_at] = Time.now
59
+ end
60
+
61
+ def metric_values(metric, from, to)
62
+ hash = @metrics[metric] || {}
63
+ (from.to_date..to.to_date).map { |date| hash[date] || [] }
64
+ end
65
+
66
+ def destroy_metric(metric)
67
+ @metrics.delete metric
68
+ end
69
+
70
+
71
+ # -- Experiments --
72
+
73
+ def set_experiment_created_at(experiment, time)
74
+ @experiments[experiment] ||= {}
75
+ @experiments[experiment][:created_at] ||= time
76
+ end
77
+
78
+ def get_experiment_created_at(experiment)
79
+ @experiments[experiment] && @experiments[experiment][:created_at]
80
+ end
81
+
82
+ def set_experiment_completed_at(experiment, time)
83
+ @experiments[experiment] ||= {}
84
+ @experiments[experiment][:completed_at] ||= time
85
+ end
86
+
87
+ def get_experiment_completed_at(experiment)
88
+ @experiments[experiment] && @experiments[experiment][:completed_at]
89
+ end
90
+
91
+ def is_experiment_completed?(experiment)
92
+ @experiments[experiment] && @experiments[experiment][:completed_at]
93
+ end
94
+
95
+ def ab_counts(experiment, alternative)
96
+ @experiments[experiment] ||= {}
97
+ @experiments[experiment][:alternatives] ||= {}
98
+ alt = @experiments[experiment][:alternatives][alternative] ||= {}
99
+ { :participants => alt[:participants] ? alt[:participants].size : 0,
100
+ :converted => alt[:converted] ? alt[:converted].size : 0,
101
+ :conversions => alt[:conversions] || 0 }
102
+ end
103
+
104
+ def ab_show(experiment, identity, alternative)
105
+ @experiments[experiment] ||= {}
106
+ @experiments[experiment][:showing] ||= {}
107
+ @experiments[experiment][:showing][identity] = alternative
108
+ end
109
+
110
+ def ab_showing(experiment, identity)
111
+ @experiments[experiment] && @experiments[experiment][:showing] && @experiments[experiment][:showing][identity]
112
+ end
113
+
114
+ def ab_not_showing(experiment, identity)
115
+ @experiments[experiment][:showing].delete identity if @experiments[experiment] && @experiments[experiment][:showing]
116
+ end
117
+
118
+ def ab_add_participant(experiment, alternative, identity)
119
+ @experiments[experiment] ||= {}
120
+ @experiments[experiment][:alternatives] ||= {}
121
+ alt = @experiments[experiment][:alternatives][alternative] ||= {}
122
+ alt[:participants] ||= Set.new
123
+ alt[:participants] << identity
124
+ end
125
+
126
+ def ab_add_conversion(experiment, alternative, identity, count = 1, implicit = false)
127
+ @experiments[experiment] ||= {}
128
+ @experiments[experiment][:alternatives] ||= {}
129
+ alt = @experiments[experiment][:alternatives][alternative] ||= {}
130
+ alt[:participants] ||= Set.new
131
+ alt[:converted] ||= Set.new
132
+ alt[:conversions] ||= 0
133
+ if implicit
134
+ alt[:participants] << identity
135
+ else
136
+ participating = alt[:participants].include?(identity)
137
+ end
138
+ alt[:converted] << identity if implicit || participating
139
+ alt[:conversions] += count
140
+ end
141
+
142
+ def ab_get_outcome(experiment)
143
+ @experiments[experiment] ||= {}
144
+ @experiments[experiment][:outcome]
145
+ end
146
+
147
+ def ab_set_outcome(experiment, alternative = 0)
148
+ @experiments[experiment] ||= {}
149
+ @experiments[experiment][:outcome] = alternative
150
+ end
151
+
152
+ def destroy_experiment(experiment)
153
+ @experiments.delete experiment
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,162 @@
1
+ module Vanity
2
+ module Adapters
3
+ class << self
4
+ # Creates new connection to MongoDB and returns MongoAdapter.
5
+ #
6
+ # @since 1.4.0
7
+ def mongo_connection(spec)
8
+ require "mongo"
9
+ MongodbAdapter.new(spec)
10
+ end
11
+ alias :mongodb_connection :mongo_connection
12
+ end
13
+
14
+ # MongoDB adapter.
15
+ #
16
+ # @since 1.4.0
17
+ class MongodbAdapter < AbstractAdapter
18
+ def initialize(options)
19
+ @mongo = Mongo::Connection.new(options[:host], options[:port], :connect=>false)
20
+ @options = options.clone
21
+ @options[:database] ||= (@options[:path] && @options[:path].split("/")[1])
22
+ connect!
23
+ end
24
+
25
+ def active?
26
+ @mongo.connected?
27
+ end
28
+
29
+ def disconnect!
30
+ @mongo.close rescue nil if @mongo
31
+ @metrics, @experiments = nil
32
+ @mongo = nil
33
+ end
34
+
35
+ def reconnect!
36
+ disconnect!
37
+ connect!
38
+ end
39
+
40
+ def connect!
41
+ @mongo.connect_to_master
42
+ database = @mongo.db(@options[:database])
43
+ database.authenticate @options[:username], @options[:password], true if @options[:username]
44
+ @metrics = database.collection("vanity.metrics")
45
+ @experiments = database.collection("vanity.experiments")
46
+ @participants = database.collection("vanity.participants")
47
+ @participants.create_index [[:experiment, 1], [:identity, 1]], :unique=>true
48
+ end
49
+
50
+ def to_s
51
+ userinfo = @options.values_at(:username, :password).join(":") if @options[:username]
52
+ URI::Generic.build(:scheme=>"mongo", :userinfo=>userinfo, :host=>@options[:host], :port=>@options[:port], :path=>"/#{@options[:database]}").to_s
53
+ end
54
+
55
+ def flushdb
56
+ @metrics.drop
57
+ @experiments.drop
58
+ @participants.drop
59
+ end
60
+
61
+
62
+ # -- Metrics --
63
+
64
+ def get_metric_last_update_at(metric)
65
+ record = @metrics.find_one(:_id=>metric)
66
+ record && record["last_update_at"]
67
+ end
68
+
69
+ def metric_track(metric, timestamp, identity, values)
70
+ inc = {}
71
+ values.each_with_index do |v,i|
72
+ inc["data.#{timestamp.to_date}.#{i}"] = v
73
+ end
74
+ @metrics.update({ :_id=>metric }, { "$inc"=>inc, "$set"=>{ :last_update_at=>Time.now } }, :upsert=>true)
75
+ end
76
+
77
+ def metric_values(metric, from, to)
78
+ record = @metrics.find_one(:_id=>metric)
79
+ data = record && record["data"] || {}
80
+ (from.to_date..to.to_date).map { |date| (data[date.to_s] || {}).values }
81
+ end
82
+
83
+ def destroy_metric(metric)
84
+ @metrics.remove :_id=>metric
85
+ end
86
+
87
+
88
+ # -- Experiments --
89
+
90
+ def set_experiment_created_at(experiment, time)
91
+ @experiments.insert :_id=>experiment, :created_at=>time
92
+ end
93
+
94
+ def get_experiment_created_at(experiment)
95
+ record = @experiments.find_one({ :_id=>experiment }, { :fields=>[:created_at] })
96
+ record && record["created_at"]
97
+ end
98
+
99
+ def set_experiment_completed_at(experiment, time)
100
+ @experiments.update({ :_id=>experiment }, { "$set"=>{ :completed_at=>time } }, :upsert=>true)
101
+ end
102
+
103
+ def get_experiment_completed_at(experiment)
104
+ record = @experiments.find_one({ :_id=>experiment }, { :fields=>[:completed_at] })
105
+ record && record["completed_at"]
106
+ end
107
+
108
+ def is_experiment_completed?(experiment)
109
+ !!@experiments.find_one(:_id=>experiment, :completed_at=>{ "$exists"=>true })
110
+ end
111
+
112
+ def ab_counts(experiment, alternative)
113
+ record = @experiments.find_one({ :_id=>experiment }, { :fields=>[:conversions] })
114
+ conversions = record && record["conversions"]
115
+ { :participants => @participants.find({ :experiment=>experiment, :seen=>alternative }, { :fields=>[] }).count,
116
+ :converted => @participants.find({ :experiment=>experiment, :converted=>alternative }, { :fields=>[] }).count,
117
+ :conversions => conversions && conversions[alternative.to_s] || 0 }
118
+ end
119
+
120
+ def ab_show(experiment, identity, alternative)
121
+ @participants.update({ :experiment=>experiment, :identity=>identity }, { "$set"=>{ :show=>alternative } }, :upsert=>true)
122
+ end
123
+
124
+ def ab_showing(experiment, identity)
125
+ participant = @participants.find_one({ :experiment=>experiment, :identity=>identity }, { :fields=>[:show] })
126
+ participant && participant["show"]
127
+ end
128
+
129
+ def ab_not_showing(experiment, identity)
130
+ @participants.update({ :experiment=>experiment, :identity=>identity }, { "$unset"=>:show })
131
+ end
132
+
133
+ def ab_add_participant(experiment, alternative, identity)
134
+ @participants.update({ :experiment=>experiment, :identity=>identity }, { "$set"=>{ :seen=>alternative } }, :upsert=>true)
135
+ end
136
+
137
+ def ab_add_conversion(experiment, alternative, identity, count = 1, implicit = false)
138
+ if implicit
139
+ @participants.update({ :experiment=>experiment, :identity=>identity }, { "$set"=>{ :seen=>alternative } }, :upsert=>true)
140
+ else
141
+ participating = @participants.find_one(:experiment=>experiment, :identity=>identity, :seen=>alternative)
142
+ end
143
+ @participants.update({ :experiment=>experiment, :identity=>identity }, { "$set"=>{ :converted=>alternative } }, :upsert=>true) if implicit || participating
144
+ @experiments.update({ :_id=>experiment }, { "$inc"=>{ "conversions.#{alternative}"=>count } }, :upsert=>true)
145
+ end
146
+
147
+ def ab_get_outcome(experiment)
148
+ experiment = @experiments.find_one({ :_id=>experiment }, { :fields=>[:outcome] })
149
+ experiment && experiment["outcome"]
150
+ end
151
+
152
+ def ab_set_outcome(experiment, alternative = 0)
153
+ @experiments.update({ :_id=>experiment }, { "$set"=>{ :outcome=>alternative } }, :upsert=>true)
154
+ end
155
+
156
+ def destroy_experiment(experiment)
157
+ @experiments.remove :_id=>experiment
158
+ @participants.remove :experiment=>experiment
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,154 @@
1
+ module Vanity
2
+ module Adapters
3
+ class << self
4
+ # Creates new connection to Redis and returns RedisAdapter.
5
+ #
6
+ # @since 1.4.0
7
+ def redis_connection(spec)
8
+ require "redis/namespace"
9
+ RedisAdapter.new(spec)
10
+ end
11
+ end
12
+
13
+ # Redis adapter.
14
+ #
15
+ # @since 1.4.0
16
+ class RedisAdapter < AbstractAdapter
17
+ def initialize(options)
18
+ @options = options.clone
19
+ @options[:db] = @options[:database] || (@options[:path] && @options[:path].split("/")[1].to_i)
20
+ @options[:thread_safe] = true
21
+ connect!
22
+ end
23
+
24
+ def active?
25
+ !!@redis
26
+ end
27
+
28
+ def disconnect!
29
+ @redis.quit rescue nil if @redis
30
+ @redis = nil
31
+ end
32
+
33
+ def reconnect!
34
+ disconnect!
35
+ connect!
36
+ end
37
+
38
+ def connect!
39
+ @redis = @options[:redis] || Redis.new(@options)
40
+ @metrics = Redis::Namespace.new("vanity:metrics", :redis=>@redis)
41
+ @experiments = Redis::Namespace.new("vanity:experiments", :redis=>@redis)
42
+ end
43
+
44
+ def to_s
45
+ @redis.id
46
+ end
47
+
48
+ def redis
49
+ @redis
50
+ end
51
+
52
+ def flushdb
53
+ @redis.flushdb
54
+ end
55
+
56
+ # -- Metrics --
57
+
58
+ def get_metric_last_update_at(metric)
59
+ last_update_at = @metrics["#{metric}:last_update_at"]
60
+ last_update_at && Time.at(last_update_at.to_i)
61
+ end
62
+
63
+ def metric_track(metric, timestamp, identity, values)
64
+ values.each_with_index do |v,i|
65
+ @metrics.incrby "#{metric}:#{timestamp.to_date}:value:#{i}", v
66
+ end
67
+ @metrics["#{metric}:last_update_at"] = Time.now.to_i
68
+ end
69
+
70
+ def metric_values(metric, from, to)
71
+ single = @metrics.mget(*(from.to_date..to.to_date).map { |date| "#{metric}:#{date}:value:0" }) || []
72
+ single.map { |v| [v] }
73
+ end
74
+
75
+ def destroy_metric(metric)
76
+ @metrics.del *@metrics.keys("#{metric}:*")
77
+ end
78
+
79
+
80
+ # -- Experiments --
81
+
82
+ def set_experiment_created_at(experiment, time)
83
+ @experiments.setnx "#{experiment}:created_at", time.to_i
84
+ end
85
+
86
+ def get_experiment_created_at(experiment)
87
+ created_at = @experiments["#{experiment}:created_at"]
88
+ created_at && Time.at(created_at.to_i)
89
+ end
90
+
91
+ def set_experiment_completed_at(experiment, time)
92
+ @experiments.setnx "#{experiment}:completed_at", time.to_i
93
+ end
94
+
95
+ def get_experiment_completed_at(experiment)
96
+ completed_at = @experiments["#{experiment}:completed_at"]
97
+ completed_at && Time.at(completed_at.to_i)
98
+ end
99
+
100
+ def is_experiment_completed?(experiment)
101
+ @experiments.exists("#{experiment}:completed_at")
102
+ end
103
+
104
+ def ab_counts(experiment, alternative)
105
+ { :participants => @experiments.scard("#{experiment}:alts:#{alternative}:participants").to_i,
106
+ :converted => @experiments.scard("#{experiment}:alts:#{alternative}:converted").to_i,
107
+ :conversions => @experiments["#{experiment}:alts:#{alternative}:conversions"].to_i }
108
+ end
109
+
110
+ def ab_show(experiment, identity, alternative)
111
+ @experiments["#{experiment}:participant:#{identity}:show"] = alternative
112
+ end
113
+
114
+ def ab_showing(experiment, identity)
115
+ alternative = @experiments["#{experiment}:participant:#{identity}:show"]
116
+ alternative && alternative.to_i
117
+ end
118
+
119
+ def ab_not_showing(experiment, identity)
120
+ @experiments.del "#{experiment}:participant:#{identity}:show"
121
+ end
122
+
123
+ def ab_add_participant(experiment, alternative, identity)
124
+ @experiments.sadd "#{experiment}:alts:#{alternative}:participants", identity
125
+ end
126
+
127
+ def ab_add_conversion(experiment, alternative, identity, count = 1, implicit = false)
128
+ if implicit
129
+ @experiments.sadd "#{experiment}:alts:#{alternative}:participants", identity
130
+ else
131
+ participating = @experiments.sismember("#{experiment}:alts:#{alternative}:participants", identity)
132
+ end
133
+ @experiments.sadd "#{experiment}:alts:#{alternative}:converted", identity if implicit || participating
134
+ @experiments.incrby "#{experiment}:alts:#{alternative}:conversions", count
135
+ end
136
+
137
+ def ab_get_outcome(experiment)
138
+ alternative = @experiments["#{experiment}:outcome"]
139
+ alternative && alternative.to_i
140
+ end
141
+
142
+ def ab_set_outcome(experiment, alternative = 0)
143
+ @experiments.setnx "#{experiment}:outcome", alternative
144
+ end
145
+
146
+ def destroy_experiment(experiment)
147
+ @experiments.del "#{experiment}:outcome", "#{experiment}:created_at", "#{experiment}:completed_at"
148
+ alternatives = @experiments.keys("#{experiment}:alts:*")
149
+ @experiments.del *alternatives unless alternatives.empty?
150
+ end
151
+
152
+ end
153
+ end
154
+ end