modesty 0.1.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.
Files changed (130) hide show
  1. data/Gemfile +13 -0
  2. data/Gemfile.lock +18 -0
  3. data/LICENSE +21 -0
  4. data/README.md +121 -0
  5. data/Rakefile +29 -0
  6. data/VERSION +1 -0
  7. data/init.rb +1 -0
  8. data/lib/modesty.rb +26 -0
  9. data/lib/modesty/api.rb +14 -0
  10. data/lib/modesty/core_ext.rb +5 -0
  11. data/lib/modesty/core_ext/array.rb +21 -0
  12. data/lib/modesty/core_ext/fixnum.rb +5 -0
  13. data/lib/modesty/core_ext/hash.rb +39 -0
  14. data/lib/modesty/core_ext/string.rb +9 -0
  15. data/lib/modesty/core_ext/symbol.rb +33 -0
  16. data/lib/modesty/datastore.rb +51 -0
  17. data/lib/modesty/datastore/redis.rb +180 -0
  18. data/lib/modesty/experiment.rb +87 -0
  19. data/lib/modesty/experiment/base.rb +47 -0
  20. data/lib/modesty/experiment/builder.rb +48 -0
  21. data/lib/modesty/experiment/console.rb +4 -0
  22. data/lib/modesty/experiment/data.rb +75 -0
  23. data/lib/modesty/experiment/interface.rb +29 -0
  24. data/lib/modesty/experiment/significance.rb +376 -0
  25. data/lib/modesty/experiment/stats.rb +163 -0
  26. data/lib/modesty/frameworks/rails.rb +27 -0
  27. data/lib/modesty/identity.rb +32 -0
  28. data/lib/modesty/load.rb +80 -0
  29. data/lib/modesty/load/load_experiments.rb +14 -0
  30. data/lib/modesty/load/load_metrics.rb +17 -0
  31. data/lib/modesty/metric.rb +56 -0
  32. data/lib/modesty/metric/base.rb +38 -0
  33. data/lib/modesty/metric/builder.rb +23 -0
  34. data/lib/modesty/metric/data.rb +133 -0
  35. data/modesty.gemspec +192 -0
  36. data/spec/core_ext_spec.rb +17 -0
  37. data/spec/experiment_spec.rb +239 -0
  38. data/spec/identity_spec.rb +161 -0
  39. data/spec/load_spec.rb +87 -0
  40. data/spec/metric_spec.rb +176 -0
  41. data/spec/rails_spec.rb +48 -0
  42. data/spec/redis_spec.rb +29 -0
  43. data/spec/significance_spec.rb +147 -0
  44. data/spec/spec.opts +1 -0
  45. data/test/myapp/config/modesty.yml +9 -0
  46. data/test/myapp/modesty/experiments/cookbook.rb +4 -0
  47. data/test/myapp/modesty/metrics/kitchen_metrics.rb +9 -0
  48. data/test/myapp/modesty/metrics/stove/burner_metrics.rb +2 -0
  49. data/vendor/.piston.yml +8 -0
  50. data/vendor/mock_redis/.gitignore +2 -0
  51. data/vendor/mock_redis/README +8 -0
  52. data/vendor/mock_redis/lib/mock_redis.rb +10 -0
  53. data/vendor/mock_redis/lib/mock_redis/hash.rb +61 -0
  54. data/vendor/mock_redis/lib/mock_redis/list.rb +6 -0
  55. data/vendor/mock_redis/lib/mock_redis/misc.rb +69 -0
  56. data/vendor/mock_redis/lib/mock_redis/set.rb +108 -0
  57. data/vendor/mock_redis/lib/mock_redis/string.rb +32 -0
  58. data/vendor/redis-rb/.gitignore +8 -0
  59. data/vendor/redis-rb/LICENSE +20 -0
  60. data/vendor/redis-rb/README.markdown +129 -0
  61. data/vendor/redis-rb/Rakefile +155 -0
  62. data/vendor/redis-rb/benchmarking/logging.rb +62 -0
  63. data/vendor/redis-rb/benchmarking/pipeline.rb +51 -0
  64. data/vendor/redis-rb/benchmarking/speed.rb +21 -0
  65. data/vendor/redis-rb/benchmarking/suite.rb +24 -0
  66. data/vendor/redis-rb/benchmarking/thread_safety.rb +38 -0
  67. data/vendor/redis-rb/benchmarking/worker.rb +71 -0
  68. data/vendor/redis-rb/examples/basic.rb +15 -0
  69. data/vendor/redis-rb/examples/dist_redis.rb +43 -0
  70. data/vendor/redis-rb/examples/incr-decr.rb +17 -0
  71. data/vendor/redis-rb/examples/list.rb +26 -0
  72. data/vendor/redis-rb/examples/pubsub.rb +31 -0
  73. data/vendor/redis-rb/examples/sets.rb +36 -0
  74. data/vendor/redis-rb/examples/unicorn/config.ru +3 -0
  75. data/vendor/redis-rb/examples/unicorn/unicorn.rb +20 -0
  76. data/vendor/redis-rb/lib/redis.rb +676 -0
  77. data/vendor/redis-rb/lib/redis/client.rb +201 -0
  78. data/vendor/redis-rb/lib/redis/compat.rb +21 -0
  79. data/vendor/redis-rb/lib/redis/connection.rb +134 -0
  80. data/vendor/redis-rb/lib/redis/distributed.rb +526 -0
  81. data/vendor/redis-rb/lib/redis/hash_ring.rb +131 -0
  82. data/vendor/redis-rb/lib/redis/pipeline.rb +13 -0
  83. data/vendor/redis-rb/lib/redis/subscribe.rb +79 -0
  84. data/vendor/redis-rb/redis.gemspec +29 -0
  85. data/vendor/redis-rb/test/commands_on_hashes_test.rb +46 -0
  86. data/vendor/redis-rb/test/commands_on_lists_test.rb +50 -0
  87. data/vendor/redis-rb/test/commands_on_sets_test.rb +78 -0
  88. data/vendor/redis-rb/test/commands_on_sorted_sets_test.rb +109 -0
  89. data/vendor/redis-rb/test/commands_on_strings_test.rb +70 -0
  90. data/vendor/redis-rb/test/commands_on_value_types_test.rb +88 -0
  91. data/vendor/redis-rb/test/connection_handling_test.rb +87 -0
  92. data/vendor/redis-rb/test/db/.gitignore +1 -0
  93. data/vendor/redis-rb/test/distributd_key_tags_test.rb +53 -0
  94. data/vendor/redis-rb/test/distributed_blocking_commands_test.rb +54 -0
  95. data/vendor/redis-rb/test/distributed_commands_on_hashes_test.rb +12 -0
  96. data/vendor/redis-rb/test/distributed_commands_on_lists_test.rb +18 -0
  97. data/vendor/redis-rb/test/distributed_commands_on_sets_test.rb +85 -0
  98. data/vendor/redis-rb/test/distributed_commands_on_strings_test.rb +50 -0
  99. data/vendor/redis-rb/test/distributed_commands_on_value_types_test.rb +73 -0
  100. data/vendor/redis-rb/test/distributed_commands_requiring_clustering_test.rb +141 -0
  101. data/vendor/redis-rb/test/distributed_connection_handling_test.rb +25 -0
  102. data/vendor/redis-rb/test/distributed_internals_test.rb +18 -0
  103. data/vendor/redis-rb/test/distributed_persistence_control_commands_test.rb +24 -0
  104. data/vendor/redis-rb/test/distributed_publish_subscribe_test.rb +90 -0
  105. data/vendor/redis-rb/test/distributed_remote_server_control_commands_test.rb +31 -0
  106. data/vendor/redis-rb/test/distributed_sorting_test.rb +21 -0
  107. data/vendor/redis-rb/test/distributed_test.rb +60 -0
  108. data/vendor/redis-rb/test/distributed_transactions_test.rb +34 -0
  109. data/vendor/redis-rb/test/encoding_test.rb +16 -0
  110. data/vendor/redis-rb/test/helper.rb +86 -0
  111. data/vendor/redis-rb/test/internals_test.rb +27 -0
  112. data/vendor/redis-rb/test/lint/hashes.rb +90 -0
  113. data/vendor/redis-rb/test/lint/internals.rb +53 -0
  114. data/vendor/redis-rb/test/lint/lists.rb +93 -0
  115. data/vendor/redis-rb/test/lint/sets.rb +66 -0
  116. data/vendor/redis-rb/test/lint/sorted_sets.rb +132 -0
  117. data/vendor/redis-rb/test/lint/strings.rb +98 -0
  118. data/vendor/redis-rb/test/lint/value_types.rb +84 -0
  119. data/vendor/redis-rb/test/persistence_control_commands_test.rb +22 -0
  120. data/vendor/redis-rb/test/pipelining_commands_test.rb +78 -0
  121. data/vendor/redis-rb/test/publish_subscribe_test.rb +151 -0
  122. data/vendor/redis-rb/test/redis_mock.rb +64 -0
  123. data/vendor/redis-rb/test/remote_server_control_commands_test.rb +56 -0
  124. data/vendor/redis-rb/test/sorting_test.rb +44 -0
  125. data/vendor/redis-rb/test/test.conf +8 -0
  126. data/vendor/redis-rb/test/thread_safety_test.rb +34 -0
  127. data/vendor/redis-rb/test/transactions_test.rb +91 -0
  128. data/vendor/redis-rb/test/unknown_commands_test.rb +14 -0
  129. data/vendor/redis-rb/test/url_param_test.rb +52 -0
  130. metadata +277 -0
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source 'http://rubygems.org'
2
+
3
+
4
+ gem 'redis'
5
+
6
+ group :development do
7
+ gem 'rake'
8
+ gem 'jeweler'
9
+ end
10
+
11
+ group :test do
12
+ gem 'rspec', '~> 1'
13
+ end
@@ -0,0 +1,18 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ git (1.2.5)
5
+ jeweler (1.5.2)
6
+ bundler (~> 1.0.0)
7
+ git (>= 1.2.5)
8
+ rake
9
+ rake (0.8.7)
10
+ rspec (1.3.2)
11
+
12
+ PLATFORMS
13
+ ruby
14
+
15
+ DEPENDENCIES
16
+ jeweler
17
+ rake
18
+ rspec (~> 1)
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ #MIT license. See http://www.opensource.org/licenses/mit-license.php
2
+
3
+ Copyright (c) 2010 Philotic, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,121 @@
1
+ # Modesty
2
+
3
+ Modesty is a really simple metrics and a/b testing framework that doesn't really do all that much. It was inspired by assaf's Vanity (github.com/assaf/vanity).
4
+
5
+ ## Metrics
6
+ A metric keeps track of things that happen, and ties in and disaggregates many different types of data. Define a metric with:
7
+
8
+ Modesty.new_metric :foo
9
+
10
+ Your metric will now be available as `Modesty.metrics[:foo]`.
11
+ You can track it with
12
+
13
+ Modesty.track! :foo`,
14
+
15
+ You can get a raw count with
16
+
17
+ Modesty.metrics[:foo].count`.
18
+
19
+ You can track multiple counts with `Modesty.track! :foo, 7`,
20
+ or, if you prefer, `Modesty.track! :foo, :count => 7`.
21
+
22
+ Simple, huh?
23
+
24
+ You can also pass in any sort of data when you track your metric. For example,
25
+
26
+ Modesty.track! :product_page_viewed, :with => {:product_id => 500, :seller_id => 278}
27
+
28
+ This provides you with a few more granular methods.
29
+
30
+ m = Modesty.metrics[:product_page_viewed]
31
+ m.unique :product_ids # => number of unique product_ids that were tracked
32
+ m.all :product_ids # => the actual ids that were tracked
33
+ m.aggregate_by :product_ids # => a hash of {product_id => tracks for this product id}
34
+ m.distribution_by :product_ids # => equivalent to aggregate_by(:product_ids).values.histogram
35
+
36
+ TODO: submetrics
37
+
38
+ ## Identity
39
+ To save you some hassle, Modesty keeps around a global identity in `Modesty.identity`.
40
+ You can set the identity with `Modesty.identify! id`,
41
+ where `id` is either an integer (i.e. the id of the current user) or `nil` for guests.
42
+ When the identity is present, all metrics tracked will get a `:user` parameter passed in.
43
+ This makes it really easy to call `m.unique :users` and such,
44
+ without having to pass it in every time.
45
+ To override this, just call `Modesty.track! :metric, :with => {:user => other_user}`.
46
+
47
+ If you're using Rails, I recommend putting a `before_filter` on `ApplicationController` that does something akin to `Modesty.identify! viewer.id`.
48
+
49
+ ## Experiments
50
+ Experiments are really the point of Modesty.
51
+ With an experiment, you separate your users into experiment groups
52
+ and track how each group performs on a given set of metrics.
53
+ Modesty will ensure that
54
+ * each group contains roughly the same number of users
55
+ * Each user has a consistent experience
56
+ * All specified metrics can be disaggregated by experiment group.
57
+
58
+ Here's how you make an experiment:
59
+
60
+ Modesty.new_experiment :button_size do |e|
61
+ e.metrics :conversion, :view
62
+ e.alternatives :huge, :medium, :small
63
+ end
64
+
65
+ Then, you can do something like this (in a controller, say)
66
+
67
+ button_size = Modesty.experiment :button_size do |e|
68
+ e.group :huge do
69
+ 9001
70
+ end
71
+ e.group :medium do
72
+ 1337
73
+ end
74
+ e.group :small do
75
+ 2
76
+ end
77
+ end
78
+
79
+ This code will use `Modesty.identity` to determine the appropriate experiment group,
80
+ and run the corresponding block.
81
+
82
+ All your tracking data will automagically be disaggregated into
83
+
84
+ Modesty.metrics[:conversion/:button_size/:huge]
85
+ Modesty.metrics[:conversion/:button_size/:medium]
86
+ Modesty.metrics[:conversion/:button_size/:small]
87
+
88
+ ## Statistics and reporting
89
+
90
+ TODO.
91
+
92
+ ##Datastores and config
93
+
94
+ Right now there are two datastores available: Redis and a sweet mock Redis for testing. To switch between them, use
95
+
96
+ Modesty.data = :redis
97
+ Modesty.data = :mock
98
+
99
+ If you need to pass in more options, use
100
+
101
+ Modesty.set_store :redis, :port => 8888, :host => '123.123.123.1'
102
+
103
+ ## Rails
104
+
105
+ By default, Modesty looks in configy/modesty.yml for something like:
106
+
107
+ datastore:
108
+ type: redis
109
+ port: 6739
110
+ host: localhost
111
+
112
+ paths:
113
+ experiments: modesty/experiments
114
+ metrics: modesty/metrics
115
+
116
+ In this, the default setup, Modesty will look for experiments
117
+ in #{Rails.root}/modesty/experiments, and metrics in #{Rails.root}/modesty/metrics.
118
+ If no config file is found, or you omit something, Modesty will use these settings.
119
+ Everything in the `datastore:` stanza (sans type: redis) will be passed as options to Redis.new.
120
+
121
+ Have fun!
@@ -0,0 +1,29 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'spec/rake/spectask'
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gem|
8
+ gem.name = "modesty"
9
+ gem.summary = %Q{Modesty is simple and scalable split testing and event tracking framework.}
10
+ gem.description = %Q{Modesty is simple and scalable split testing and event tracking framework. It was inspired by assaf's Vanity (github.com/assaf/vanity).}
11
+ gem.email = "jay@causes.com"
12
+ gem.homepage = "http://github.com/causes/modesty"
13
+ gem.authors = ["Jay Adkisson", "Kevin Ball", "Kristján Pétursson"]
14
+ gem.add_development_dependency "rspec"
15
+ gem.add_runtime_dependency "redis"
16
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
+ end
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
20
+ end
21
+
22
+ Jeweler::RubygemsDotOrgTasks.new
23
+
24
+
25
+ Spec::Rake::SpecTask.new('spec') do |t|
26
+ t.spec_files = FileList['spec/**/*_spec.rb']
27
+ t.ruby_opts = ['-Ilib']
28
+ t.spec_opts = ['-O spec/spec.opts']
29
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), 'lib', 'modesty.rb')
@@ -0,0 +1,26 @@
1
+ require 'yaml'
2
+ require 'rubygems'
3
+ require 'active_support'
4
+
5
+
6
+ module Modesty
7
+ LIB = File.dirname(__FILE__)
8
+ ROOT = File.expand_path(File.join(LIB, '..'))
9
+ VENDOR = File.expand_path(File.join(ROOT, 'vendor'))
10
+ TEST = File.expand_path(File.join(ROOT, 'test'))
11
+ end
12
+
13
+ $:.unshift Modesty::LIB
14
+ require 'modesty/core_ext.rb'
15
+ require 'modesty/api.rb'
16
+ require 'modesty/datastore.rb'
17
+ require 'modesty/identity.rb'
18
+ require 'modesty/metric.rb'
19
+ require 'modesty/experiment.rb'
20
+ require 'modesty/load.rb'
21
+
22
+ if defined? Rails
23
+ require 'modesty/frameworks/rails'
24
+ end
25
+
26
+ $:.shift
@@ -0,0 +1,14 @@
1
+ module Modesty
2
+ class API
3
+ end
4
+
5
+ class << self
6
+ def api
7
+ @api ||= API.new
8
+ end
9
+
10
+ def method_missing(meth, *args, &blk)
11
+ self.api.send(meth, *args, &blk)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ require 'modesty/core_ext/array.rb'
2
+ require 'modesty/core_ext/fixnum.rb'
3
+ require 'modesty/core_ext/hash.rb'
4
+ require 'modesty/core_ext/symbol.rb'
5
+ require 'modesty/core_ext/string.rb'
@@ -0,0 +1,21 @@
1
+ class Array
2
+ unless method_defined? :histogram
3
+ def histogram
4
+ hsh = Hash.new(0)
5
+ self.each do |e|
6
+ hsh[e] += 1
7
+ end
8
+ hsh
9
+ end
10
+ end
11
+
12
+ def to_h
13
+ Hash[self]
14
+ end
15
+
16
+ def hashmap
17
+ self.map do |e|
18
+ [e, yield(e)]
19
+ end.to_h
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ class Fixnum
2
+ def to_i?
3
+ self
4
+ end
5
+ end
@@ -0,0 +1,39 @@
1
+ class Hash
2
+ # addition for histogram aggregation
3
+ # >> {:a=>1, :b=>2} + {:a=>3, :c=>4}
4
+ # => {:a=>4, :b=>2, :c=>4}
5
+ def +(other)
6
+ hash = self.dup
7
+ other.each do |k, v|
8
+ if hash.include? k
9
+ hash[k] += v
10
+ else
11
+ hash[k] = v
12
+ end
13
+ end
14
+ hash
15
+ end
16
+
17
+ def map_values!(&blk)
18
+ self.each do |k,v|
19
+ self[k] = yield(v)
20
+ end
21
+ end
22
+
23
+ def map_values(&blk)
24
+ self.dup.map_values!(&blk)
25
+ end
26
+
27
+ def map!
28
+ self.keys.each do |k|
29
+ v = self.delete(k)
30
+ new_k, new_v = yield [k,v]
31
+ self[new_k] = new_v
32
+ end
33
+ self
34
+ end
35
+
36
+ def to_h
37
+ self
38
+ end
39
+ end
@@ -0,0 +1,9 @@
1
+ class String
2
+ # casts to an integer if applicable,
3
+ # else leaves it alone.
4
+ def to_i?
5
+ Integer(self)
6
+ rescue
7
+ self
8
+ end
9
+ end
@@ -0,0 +1,33 @@
1
+ class Symbol
2
+ unless method_defined? :/
3
+ def /(other)
4
+ :"#{self}/#{other}"
5
+ end
6
+ end
7
+
8
+ alias __old_inspect inspect
9
+ def inspect
10
+ s = self.to_s
11
+
12
+ #some things should not use this.
13
+ if (
14
+ s[0..0] == '/' ||
15
+ s[-1..-1] == '/' ||
16
+ s.include?("//") ||
17
+ s.include?(":")
18
+ )
19
+ return self.__old_inspect
20
+ end
21
+
22
+ begin
23
+ inspected = self.to_s.split(/\//).map { |s| ":#{s}"}.join('/')
24
+ return inspected
25
+ rescue
26
+ return self.__old_inspect
27
+ end
28
+ end
29
+
30
+ def <=>(other)
31
+ self.to_s <=> other.to_s
32
+ end
33
+ end
@@ -0,0 +1,51 @@
1
+ module Modesty
2
+ class Datastore
3
+ class ConnectionError < StandardError; end
4
+ def connected?
5
+ self.ping!
6
+ true
7
+ rescue ConnectionError
8
+ false
9
+ end
10
+
11
+ attr_reader :store
12
+
13
+ class MetricData
14
+ def initialize(metric)
15
+ @metric = metric
16
+ end
17
+ end
18
+
19
+ class ExperimentData
20
+ def initialize(exp)
21
+ @experiment = exp
22
+ end
23
+ end
24
+ end
25
+
26
+ module DatastoreMethods
27
+ def set_store(type, opts={})
28
+ @data = case type.to_s
29
+ when 'redis'
30
+ require File.join(Modesty::LIB, 'modesty', 'datastore', 'redis')
31
+ RedisData.new(opts)
32
+ else
33
+ puts "Unrecognized datastore #{type}. Defaulting to MockRedis."
34
+ self.set_store :redis, :mock => true
35
+ end
36
+ end
37
+ alias data= set_store
38
+
39
+ def data
40
+ @data || set_store(:redis, :mock => true)
41
+ end
42
+
43
+ def handle_error(e)
44
+ raise e
45
+ end
46
+ end
47
+
48
+ class API
49
+ include DatastoreMethods
50
+ end
51
+ end
@@ -0,0 +1,180 @@
1
+ module Modesty
2
+ class RedisData < Datastore
3
+ def self.date_key(date)
4
+ "%04d-%02d-%02d" % [date.year, date.month, date.day]
5
+ end
6
+
7
+ def initialize(options={})
8
+ if options[:redis]
9
+ @store = options[:redis]
10
+ elsif options['mock'] || options[:mock]
11
+ require File.join(
12
+ Modesty::VENDOR, 'mock_redis', 'lib', 'mock_redis.rb'
13
+ )
14
+ @store = MockRedis.new
15
+ else
16
+ $:.push(File.join(Modesty::VENDOR, 'redis-rb', 'lib'))
17
+ require 'redis'
18
+ $:.pop
19
+
20
+ @store = Redis.new(options)
21
+ end
22
+ end
23
+
24
+ def ping!
25
+ self.ping
26
+ end
27
+
28
+ def method_missing(name, *args, &blk)
29
+ @store.send(name, *args, &blk)
30
+ rescue Exception => e
31
+ raise ConnectionError, e.to_s
32
+ end
33
+
34
+ def self.keyify(*args)
35
+ args.map {|a| a.to_s}.map do |a|
36
+ (a.is_a?(Date)) ? date_key(a) : a
37
+ end.map do |k|
38
+ k.gsub(/[^\w\-\/]/,'_')
39
+ end.unshift('modesty').join(':')
40
+ end
41
+
42
+ class MetricData < Datastore::MetricData
43
+ def key(*args)
44
+ RedisData.keyify('metrics', @metric.slug, *args)
45
+ end
46
+
47
+ def data
48
+ Modesty.data
49
+ end
50
+
51
+ def key_for_with(*args)
52
+ key(:with, *args)
53
+ end
54
+
55
+ # -*- raw counts -*- #
56
+ def count_key(date)
57
+ key(date, :count)
58
+ end
59
+
60
+ def add_counts(date, count)
61
+ data.incrby(count_key(date), count)
62
+ end
63
+
64
+ def count(date)
65
+ data.get(count_key(date)).to_i
66
+ end
67
+
68
+ def count_range(range)
69
+ keys = range.map { |d| count_key(d) }
70
+ data.mget(keys).map(&:to_i?)
71
+ end
72
+
73
+ # -*- unidentified users -*- #
74
+ def count_unidentified_user(date)
75
+ data.incr(unidentified_users_key(date))
76
+ end
77
+
78
+ def unidentified_users_key(date)
79
+ key_for_with(:users, date, :unidentified)
80
+ end
81
+
82
+ def unidentified_users(date = :all)
83
+ data.get(unidentified_users_key(date)).to_i
84
+ end
85
+
86
+ def unidentified_users_range(range)
87
+ keys = range.map { |d| unidentified_users_key(d) }
88
+ data.mget(keys).map(&:to_i?)
89
+ end
90
+
91
+ # -*- :with => params -*- #
92
+ def add_param_counts(date, count, param, id)
93
+ #puts "data.hincrby(#{key_for_with(param, date).inspect}, #{id.inspect}, #{count.inspect})"
94
+ data.hincrby(key_for_with(param, date), id, count)
95
+ end
96
+
97
+ def unique(param, date)
98
+ data.hlen(key_for_with(param, date))
99
+ end
100
+
101
+ def all(param, date)
102
+ data.hkeys(key_for_with(param, date)).map(&:to_i?)
103
+ end
104
+
105
+ def distribution_by(param, date)
106
+ dist = data.hvals(key_for_with(param, date)).histogram
107
+ dist.map! { |k,v| [k,v].map(&:to_i?) }
108
+ dist
109
+ end
110
+
111
+ def aggregate_by(param, date)
112
+ agg = data.hgetall(key_for_with(param, date))
113
+ agg.map! { |k,v| [k,v].map(&:to_i?) }
114
+ agg
115
+ end
116
+
117
+ def track!(count, with_args)
118
+ data.pipelined do
119
+ [:all, Date.today].each do |date|
120
+ self.add_counts(date, count)
121
+
122
+ self.count_unidentified_user(date) unless with_args[:user]
123
+
124
+ with_args.each do |param, id|
125
+ self.add_param_counts(date, count, param, id)
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ end
132
+
133
+ class ExperimentData < Datastore::ExperimentData
134
+ def data
135
+ Modesty.data
136
+ end
137
+
138
+ def key(*args)
139
+ RedisData.keyify(:experiments, @experiment.slug, *args)
140
+ end
141
+
142
+ def register!(alt, identity)
143
+ old_alt = self.get_cached_alternative(identity)
144
+ if old_alt
145
+ data.srem(self.key(old_alt), identity)
146
+ end
147
+ data.sadd(self.key(alt), identity)
148
+ return alt
149
+ end
150
+
151
+ def get_cached_alternative(identity)
152
+ @experiment.alternatives.each do |alt|
153
+ if data.sismember(self.key(alt), identity)
154
+ return alt
155
+ end
156
+ end
157
+ return nil
158
+ end
159
+
160
+ def users(alt=nil)
161
+ if alt.nil? #return the union
162
+ data.sunion(*@experiment.alternatives.map {|a| self.key(a) })
163
+ else
164
+ data.smembers(self.key(alt))
165
+ end.map(&:to_i)
166
+ end
167
+
168
+ def num_users(alt=nil)
169
+ if alt.nil?
170
+ @experiment.alternatives.map do |alt|
171
+ data.scard(self.key(alt)).to_i
172
+ end.sum
173
+ else
174
+ data.scard(self.key(alt)).to_i
175
+ end
176
+ end
177
+
178
+ end
179
+ end
180
+ end