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.
- data/Gemfile +13 -0
- data/Gemfile.lock +18 -0
- data/LICENSE +21 -0
- data/README.md +121 -0
- data/Rakefile +29 -0
- data/VERSION +1 -0
- data/init.rb +1 -0
- data/lib/modesty.rb +26 -0
- data/lib/modesty/api.rb +14 -0
- data/lib/modesty/core_ext.rb +5 -0
- data/lib/modesty/core_ext/array.rb +21 -0
- data/lib/modesty/core_ext/fixnum.rb +5 -0
- data/lib/modesty/core_ext/hash.rb +39 -0
- data/lib/modesty/core_ext/string.rb +9 -0
- data/lib/modesty/core_ext/symbol.rb +33 -0
- data/lib/modesty/datastore.rb +51 -0
- data/lib/modesty/datastore/redis.rb +180 -0
- data/lib/modesty/experiment.rb +87 -0
- data/lib/modesty/experiment/base.rb +47 -0
- data/lib/modesty/experiment/builder.rb +48 -0
- data/lib/modesty/experiment/console.rb +4 -0
- data/lib/modesty/experiment/data.rb +75 -0
- data/lib/modesty/experiment/interface.rb +29 -0
- data/lib/modesty/experiment/significance.rb +376 -0
- data/lib/modesty/experiment/stats.rb +163 -0
- data/lib/modesty/frameworks/rails.rb +27 -0
- data/lib/modesty/identity.rb +32 -0
- data/lib/modesty/load.rb +80 -0
- data/lib/modesty/load/load_experiments.rb +14 -0
- data/lib/modesty/load/load_metrics.rb +17 -0
- data/lib/modesty/metric.rb +56 -0
- data/lib/modesty/metric/base.rb +38 -0
- data/lib/modesty/metric/builder.rb +23 -0
- data/lib/modesty/metric/data.rb +133 -0
- data/modesty.gemspec +192 -0
- data/spec/core_ext_spec.rb +17 -0
- data/spec/experiment_spec.rb +239 -0
- data/spec/identity_spec.rb +161 -0
- data/spec/load_spec.rb +87 -0
- data/spec/metric_spec.rb +176 -0
- data/spec/rails_spec.rb +48 -0
- data/spec/redis_spec.rb +29 -0
- data/spec/significance_spec.rb +147 -0
- data/spec/spec.opts +1 -0
- data/test/myapp/config/modesty.yml +9 -0
- data/test/myapp/modesty/experiments/cookbook.rb +4 -0
- data/test/myapp/modesty/metrics/kitchen_metrics.rb +9 -0
- data/test/myapp/modesty/metrics/stove/burner_metrics.rb +2 -0
- data/vendor/.piston.yml +8 -0
- data/vendor/mock_redis/.gitignore +2 -0
- data/vendor/mock_redis/README +8 -0
- data/vendor/mock_redis/lib/mock_redis.rb +10 -0
- data/vendor/mock_redis/lib/mock_redis/hash.rb +61 -0
- data/vendor/mock_redis/lib/mock_redis/list.rb +6 -0
- data/vendor/mock_redis/lib/mock_redis/misc.rb +69 -0
- data/vendor/mock_redis/lib/mock_redis/set.rb +108 -0
- data/vendor/mock_redis/lib/mock_redis/string.rb +32 -0
- data/vendor/redis-rb/.gitignore +8 -0
- data/vendor/redis-rb/LICENSE +20 -0
- data/vendor/redis-rb/README.markdown +129 -0
- data/vendor/redis-rb/Rakefile +155 -0
- data/vendor/redis-rb/benchmarking/logging.rb +62 -0
- data/vendor/redis-rb/benchmarking/pipeline.rb +51 -0
- data/vendor/redis-rb/benchmarking/speed.rb +21 -0
- data/vendor/redis-rb/benchmarking/suite.rb +24 -0
- data/vendor/redis-rb/benchmarking/thread_safety.rb +38 -0
- data/vendor/redis-rb/benchmarking/worker.rb +71 -0
- data/vendor/redis-rb/examples/basic.rb +15 -0
- data/vendor/redis-rb/examples/dist_redis.rb +43 -0
- data/vendor/redis-rb/examples/incr-decr.rb +17 -0
- data/vendor/redis-rb/examples/list.rb +26 -0
- data/vendor/redis-rb/examples/pubsub.rb +31 -0
- data/vendor/redis-rb/examples/sets.rb +36 -0
- data/vendor/redis-rb/examples/unicorn/config.ru +3 -0
- data/vendor/redis-rb/examples/unicorn/unicorn.rb +20 -0
- data/vendor/redis-rb/lib/redis.rb +676 -0
- data/vendor/redis-rb/lib/redis/client.rb +201 -0
- data/vendor/redis-rb/lib/redis/compat.rb +21 -0
- data/vendor/redis-rb/lib/redis/connection.rb +134 -0
- data/vendor/redis-rb/lib/redis/distributed.rb +526 -0
- data/vendor/redis-rb/lib/redis/hash_ring.rb +131 -0
- data/vendor/redis-rb/lib/redis/pipeline.rb +13 -0
- data/vendor/redis-rb/lib/redis/subscribe.rb +79 -0
- data/vendor/redis-rb/redis.gemspec +29 -0
- data/vendor/redis-rb/test/commands_on_hashes_test.rb +46 -0
- data/vendor/redis-rb/test/commands_on_lists_test.rb +50 -0
- data/vendor/redis-rb/test/commands_on_sets_test.rb +78 -0
- data/vendor/redis-rb/test/commands_on_sorted_sets_test.rb +109 -0
- data/vendor/redis-rb/test/commands_on_strings_test.rb +70 -0
- data/vendor/redis-rb/test/commands_on_value_types_test.rb +88 -0
- data/vendor/redis-rb/test/connection_handling_test.rb +87 -0
- data/vendor/redis-rb/test/db/.gitignore +1 -0
- data/vendor/redis-rb/test/distributd_key_tags_test.rb +53 -0
- data/vendor/redis-rb/test/distributed_blocking_commands_test.rb +54 -0
- data/vendor/redis-rb/test/distributed_commands_on_hashes_test.rb +12 -0
- data/vendor/redis-rb/test/distributed_commands_on_lists_test.rb +18 -0
- data/vendor/redis-rb/test/distributed_commands_on_sets_test.rb +85 -0
- data/vendor/redis-rb/test/distributed_commands_on_strings_test.rb +50 -0
- data/vendor/redis-rb/test/distributed_commands_on_value_types_test.rb +73 -0
- data/vendor/redis-rb/test/distributed_commands_requiring_clustering_test.rb +141 -0
- data/vendor/redis-rb/test/distributed_connection_handling_test.rb +25 -0
- data/vendor/redis-rb/test/distributed_internals_test.rb +18 -0
- data/vendor/redis-rb/test/distributed_persistence_control_commands_test.rb +24 -0
- data/vendor/redis-rb/test/distributed_publish_subscribe_test.rb +90 -0
- data/vendor/redis-rb/test/distributed_remote_server_control_commands_test.rb +31 -0
- data/vendor/redis-rb/test/distributed_sorting_test.rb +21 -0
- data/vendor/redis-rb/test/distributed_test.rb +60 -0
- data/vendor/redis-rb/test/distributed_transactions_test.rb +34 -0
- data/vendor/redis-rb/test/encoding_test.rb +16 -0
- data/vendor/redis-rb/test/helper.rb +86 -0
- data/vendor/redis-rb/test/internals_test.rb +27 -0
- data/vendor/redis-rb/test/lint/hashes.rb +90 -0
- data/vendor/redis-rb/test/lint/internals.rb +53 -0
- data/vendor/redis-rb/test/lint/lists.rb +93 -0
- data/vendor/redis-rb/test/lint/sets.rb +66 -0
- data/vendor/redis-rb/test/lint/sorted_sets.rb +132 -0
- data/vendor/redis-rb/test/lint/strings.rb +98 -0
- data/vendor/redis-rb/test/lint/value_types.rb +84 -0
- data/vendor/redis-rb/test/persistence_control_commands_test.rb +22 -0
- data/vendor/redis-rb/test/pipelining_commands_test.rb +78 -0
- data/vendor/redis-rb/test/publish_subscribe_test.rb +151 -0
- data/vendor/redis-rb/test/redis_mock.rb +64 -0
- data/vendor/redis-rb/test/remote_server_control_commands_test.rb +56 -0
- data/vendor/redis-rb/test/sorting_test.rb +44 -0
- data/vendor/redis-rb/test/test.conf +8 -0
- data/vendor/redis-rb/test/thread_safety_test.rb +34 -0
- data/vendor/redis-rb/test/transactions_test.rb +91 -0
- data/vendor/redis-rb/test/unknown_commands_test.rb +14 -0
- data/vendor/redis-rb/test/url_param_test.rb +52 -0
- metadata +277 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
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.
|
data/README.md
ADDED
|
@@ -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!
|
data/Rakefile
ADDED
|
@@ -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')
|
data/lib/modesty.rb
ADDED
|
@@ -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
|
data/lib/modesty/api.rb
ADDED
|
@@ -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,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,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
|